use onetaskgraph_plugin_api::{SourceError, Visibility, WriteTarget};
use secrecy::ExposeSecret;
use serde_json::{Value, json};
use crate::{GitHubProjectsSource, RepositoryTarget, accounting, graphql};
impl GitHubProjectsSource {
pub(crate) async fn write_visibility(
&self,
target: &WriteTarget<'_>,
) -> Result<Visibility, SourceError> {
let repository = self.visibility_repository(target).await?;
let (board, repository) = tokio::join!(self.board_visibility(), async {
match &repository {
Some(repository) => self.repository_visibility(repository).await,
None => Ok(Visibility::Private),
}
});
Ok(match (board?, repository?) {
(Visibility::Private, Visibility::Private) => Visibility::Private,
(Visibility::Unknown, _) | (_, Visibility::Unknown) => Visibility::Unknown,
_ => Visibility::Public,
})
}
async fn visibility_repository(
&self,
target: &WriteTarget<'_>,
) -> Result<Option<RepositoryTarget>, SourceError> {
let own = |item: Option<crate::Resolved>| {
item.and_then(|item| item.own_repository)
.map(|origin| origin_target(&origin))
.transpose()
};
match target {
WriteTarget::Existing(id) => own(self.bound_item(id).await?),
WriteTarget::New {
repositories,
project,
} => {
if let [named] = repositories {
return origin_target(named).map(Some);
}
if let Some(project) = project
&& let Some(repository) = own(self.bound_item(project).await?)?
{
return Ok(Some(repository));
}
self.configured_repository().cloned().map(Some)
}
}
}
async fn board_visibility(&self) -> Result<Visibility, SourceError> {
let answer = self
.graphql(
graphql::PROJECT_VISIBILITY,
json!({"owner": self.owner, "number": self.project_number}),
)
.await
.map_err(|error| match error {
SourceError::Auth { message } if message.contains("read:project") => {
SourceError::Auth {
message: format!(
"source {} cannot read whether its project is public: the credential \
in {} lacks the `read:project` scope that read needs; next: grant that \
scope to that credential",
self.name, self.credential_name
),
}
}
other => other,
})?;
match answer.pointer("/visibility/projectV2/public") {
Some(Value::Bool(true)) => Ok(Visibility::Public),
Some(Value::Bool(false)) => Ok(Visibility::Private),
_ => Err(SourceError::Malformed {
message: format!(
"source {} read no `public` for project {} of {}",
self.name, self.project_number, self.owner
),
}),
}
}
async fn repository_visibility(
&self,
repository: &RepositoryTarget,
) -> Result<Visibility, SourceError> {
let mut url = self.endpoint.clone();
url.set_path(&format!("/repos/{}/{}", repository.owner, repository.name));
url.set_query(None);
if !self.loopback() {
url.set_host(Some("api.github.com"))
.map_err(|error| SourceError::Config {
message: format!("cannot select GitHub API host: {error}"),
})?;
}
let endpoint =
accounting::Endpoint::parse(accounting::Method::Get, "/repos/{owner}/{repo}")
.expect("a literal REST endpoint template");
let unreadable = |why: String| SourceError::Unavailable {
message: format!(
"source {} could not read whether repository {} is public: {why}",
self.name,
repository.slug()
),
};
let response = self
.client
.get(url)
.bearer_auth(self.token.expose_secret())
.header("accept", "application/vnd.github+json")
.send()
.await;
let response = match response {
Ok(response) => response,
Err(error) => {
self.ledger
.record(accounting::Request::rest(endpoint).finished(
accounting::Outcome::Refused,
accounting::RateLimit::default(),
));
return Err(unreadable(error.to_string()));
}
};
let status = response.status();
let limits = accounting::RateLimit::read(|header| {
response
.headers()
.get(header)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
});
let body = response.bytes().await;
self.ledger
.record(accounting::Request::rest(endpoint).finished(
if status.is_success() {
accounting::Outcome::Answered
} else {
accounting::Outcome::Refused
},
limits,
));
let body = body.map_err(|error| unreadable(error.to_string()))?;
if !status.is_success() {
return Err(unreadable(format!("GitHub answered HTTP {status}")));
}
let value: Value =
serde_json::from_slice(&body).map_err(|error| unreadable(error.to_string()))?;
match (value.get("visibility"), value.get("private")) {
(Some(Value::String(named)), _) => match named.as_str() {
"public" => Ok(Visibility::Public),
"private" | "internal" => Ok(Visibility::Private),
other => Err(unreadable(format!(
"its answer named the visibility {other:?}, which is none of public, private \
and internal"
))),
},
(None, Some(Value::Bool(false))) => Ok(Visibility::Public),
(None, Some(Value::Bool(true))) => Ok(Visibility::Private),
_ => Err(unreadable("its answer named no visibility".to_owned())),
}
}
}
fn origin_target(
origin: &onetaskgraph_plugin_api::Repository,
) -> Result<RepositoryTarget, SourceError> {
RepositoryTarget::from_origin(origin).map_err(|message| SourceError::Refused { message })
}