diff --git a/Cargo.toml b/Cargo.toml index 4c78bea1..663f5c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "octocrab" -version = "0.27.0" +version = "0.28.0" authors = ["XAMPPRocky "] edition = "2018" readme = "README.md" diff --git a/src/models/events.rs b/src/models/events.rs index c60f8fe3..79b26a4e 100644 --- a/src/models/events.rs +++ b/src/models/events.rs @@ -1,6 +1,6 @@ pub mod payload; -use crate::models::events::payload::EventInstallationPayload; +use crate::models::events::payload::EventInstallation; use self::payload::{ CommitCommentEventPayload, CreateEventPayload, DeleteEventPayload, EventPayload, @@ -15,6 +15,10 @@ use serde::{de::Error, Deserialize, Serialize}; use url::Url; /// A GitHub event. +/// +/// If you want to deserialize a webhook payload received in a Github Application, you +/// must directly deserialize the body into a [`WrappedEventPayload`](WrappedEventPayload). +/// For webhooks, the event type is stored in the `X-GitHub-Event` header. #[derive(Debug, Clone, PartialEq, Serialize)] #[non_exhaustive] pub struct Event { @@ -139,7 +143,7 @@ impl<'de> Deserialize<'de> for Event { } #[derive(Deserialize)] struct IntermediatePayload { - installation: Option, + installation: Option, organization: Option, repository: Option, sender: Option, @@ -230,8 +234,13 @@ mod test { let event: Event = serde_json::from_str(json).unwrap(); assert_eq!(event.r#type, EventType::WorkflowRunEvent); assert_eq!( - event.payload.unwrap().installation.unwrap().id, - crate::models::InstallationId(18995746) + event.payload.unwrap().installation.unwrap(), + crate::models::events::payload::EventInstallation::Minimal(Box::new( + crate::models::events::payload::EventInstallationId { + id: 18995746.into(), + node_id: "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTg5OTU3NDY=".to_string() + } + )) ) } diff --git a/src/models/events/payload.rs b/src/models/events/payload.rs index ddd81806..58f2d8a2 100644 --- a/src/models/events/payload.rs +++ b/src/models/events/payload.rs @@ -3,6 +3,9 @@ mod create; mod delete; mod fork; mod gollum; +mod installation; +mod installation_repositories; +mod installation_target; mod issue_comment; mod issues; mod member; @@ -12,12 +15,14 @@ mod pull_request_review_comment; mod push; mod workflow_run; -use crate::models::{repos::CommitAuthor, InstallationId}; pub use commit_comment::*; pub use create::*; pub use delete::*; pub use fork::*; pub use gollum::*; +pub use installation::*; +pub use installation_repositories::*; +pub use installation_target::*; pub use issue_comment::*; pub use issues::*; pub use member::*; @@ -30,16 +35,29 @@ pub use workflow_run::*; use serde::{Deserialize, Serialize}; use url::Url; -use crate::models::{orgs::Organization, Author, Repository}; +use crate::models::{ + orgs::Organization, repos::CommitAuthor, Author, Installation, InstallationId, Repository, + RepositoryId, +}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct EventInstallationPayload { +#[serde(untagged)] +pub enum EventInstallation { + /// A full installation object which is present for `Installation*` related webhook events. + Full(Box), + /// The minimal installation object is present for all other event types. + Minimal(Box), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EventInstallationId { pub id: InstallationId, + pub node_id: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WrappedEventPayload { - pub installation: Option, + pub installation: Option, pub organization: Option, pub repository: Option, pub sender: Option, @@ -59,6 +77,9 @@ pub enum EventPayload { PushEvent(Box), CreateEvent(Box), DeleteEvent(Box), + InstallationEvent(Box), + InstallationRepositoriesEvent(Box), + InstallationTargetEvent(Box), IssuesEvent(Box), IssueCommentEvent(Box), CommitCommentEvent(Box), @@ -82,3 +103,76 @@ pub struct Commit { pub distinct: bool, pub url: Url, } + +/// A repository in installation related webhook events. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationEventRepository { + pub id: RepositoryId, + pub node_id: String, + pub name: String, + pub full_name: String, + pub private: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_deserialize_installation_event() { + // The payload has been extracted as the `payload` key from a webhook installation event. + let json = include_str!("../../../tests/resources/installation_event.json"); + let event: WrappedEventPayload = serde_json::from_str(json).unwrap(); + + let installation = event.installation.unwrap(); + let specific = event.specific.unwrap(); + + match installation { + EventInstallation::Full(install) => { + assert_eq!(install.id, 7777777.into()); + assert_eq!(install.repository_selection.unwrap(), "all"); + } + EventInstallation::Minimal(_) => { + panic!("expected a Full installation payload for the event.") + } + }; + + match specific { + EventPayload::InstallationEvent(install) => { + let repos = install.repositories; + assert_eq!(repos.len(), 3); + assert!( + repos.iter().any(|repo| repo.name == "ViscoElRebound"), + "ViscoElRebound should be in the list of repositories" + ); + assert!( + repos.iter().any(|repo| repo.name == "OSSU"), + "OSSU should be in the list of repositories" + ); + assert!( + repos.iter().any(|repo| repo.name == "octocrab"), + "octocrab should be in the list of repositories" + ); + } + EventPayload::PushEvent(_) + | EventPayload::CreateEvent(_) + | EventPayload::DeleteEvent(_) + | EventPayload::InstallationRepositoriesEvent(_) + | EventPayload::InstallationTargetEvent(_) + | EventPayload::IssuesEvent(_) + | EventPayload::IssueCommentEvent(_) + | EventPayload::CommitCommentEvent(_) + | EventPayload::ForkEvent(_) + | EventPayload::GollumEvent(_) + | EventPayload::MemberEvent(_) + | EventPayload::PullRequestEvent(_) + | EventPayload::PullRequestReviewEvent(_) + | EventPayload::PullRequestReviewCommentEvent(_) + | EventPayload::WorkflowRunEvent(_) + | EventPayload::UnknownEvent(_) => { + panic!("Expected an installation event, got {:?}", specific) + } + } + } +} diff --git a/src/models/events/payload/installation.rs b/src/models/events/payload/installation.rs new file mode 100644 index 00000000..b665b837 --- /dev/null +++ b/src/models/events/payload/installation.rs @@ -0,0 +1,39 @@ +//! This event occurs when there is activity relating to a GitHub App +//! installation. All GitHub Apps receive this event by default. You cannot +//! manually subscribe to this event. + +use serde::{Deserialize, Serialize}; + +use super::InstallationEventRepository; +use crate::models::Author; + +/// The payload in a webhook installation event type. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationEventPayload { + /// The action this event represents. + pub action: InstallationEventAction, + /// An enterprise on GitHub + pub enterprise: Option, + /// An array of repositories that the installation can access + pub repositories: Vec, + /// The initiator of the request, mainly for the [`created`](InstallationAction::Created) action + pub requester: Option, +} + +/// The action on an installation this event corresponds to. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum InstallationEventAction { + /// Someone installed a GitHub App on a user or organization account. + Created, + /// Someone uninstalled a GitHub App on a user or organization account. + Deleted, + /// Someone granted new permissions to a GitHub App. + NewPermissionsAccepted, + /// Someone blocked access by a GitHub App to their user or organization account. + Suspend, + /// A GitHub App that was blocked from accessing a user or organization account was given access the account again. + Unsuspend, +} diff --git a/src/models/events/payload/installation_repositories.rs b/src/models/events/payload/installation_repositories.rs new file mode 100644 index 00000000..a9ffce3a --- /dev/null +++ b/src/models/events/payload/installation_repositories.rs @@ -0,0 +1,45 @@ +//! This event occurs when there is activity relating to which repositories a +//! GitHub App installation can access. All GitHub Apps receive this event by +//! default. You cannot manually subscribe to this event. + +use serde::{Deserialize, Serialize}; + +use super::InstallationEventRepository; +use crate::models::Author; + +/// The payload in a webhook installation_repositories event type. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationRepositoriesEventPayload { + /// The action this event represents. + pub action: InstallationRepositoriesEventAction, + /// An enterprise on GitHub + pub enterprise: Option, + /// An array of repositories, which were added to the installation + pub repositories_added: Vec, + /// An array of repositories, which were removed from the installation + pub repositories_removed: Vec, + /// Describe whether all repositories have been selected or there's a selection involved + pub repository_selection: InstallationRepositoriesEventSelection, + /// The initiator of the request, mainly for the [`created`](InstallationAction::Created) action + pub requester: Option, +} + +/// The action on an installation this event corresponds to. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum InstallationRepositoriesEventAction { + /// A GitHub App installation was granted access to one or more repositories. + Added, + /// Access to one or more repositories was revoked for a GitHub App installation. + Removed, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum InstallationRepositoriesEventSelection { + All, + Selected, +} diff --git a/src/models/events/payload/installation_target.rs b/src/models/events/payload/installation_target.rs new file mode 100644 index 00000000..3216d743 --- /dev/null +++ b/src/models/events/payload/installation_target.rs @@ -0,0 +1,36 @@ +//! This event occurs when there is activity relating to the user or +//! organization account that a GitHub App is installed on. + +use serde::{Deserialize, Serialize}; + +use crate::models::orgs::Organization; + +/// The payload in a webhook installation_target event type. +/// +/// Somebody renamed the user or organization account that a GitHub App is installed on. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationTargetEventPayload { + pub account: Organization, + pub changes: InstallationTargetChanges, + pub target_type: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationTargetChanges { + pub login: InstallationTargetLoginChanges, + pub slug: InstallationTargetSlugChanges, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationTargetLoginChanges { + pub from: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InstallationTargetSlugChanges { + pub from: String, +} diff --git a/tests/resources/installation_event.json b/tests/resources/installation_event.json new file mode 100644 index 00000000..00320735 --- /dev/null +++ b/tests/resources/installation_event.json @@ -0,0 +1,100 @@ +{ + "action": "created", + "installation": { + "id": 7777777, + "account": { + "login": "gagbo", + "id": 88888, + "node_id": "PLACEHOLDER_NODE_ID", + "avatar_url": "https://avatars.githubusercontent.com/u/88888?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/gagbo", + "html_url": "https://github.com/gagbo", + "followers_url": "https://api.github.com/users/gagbo/followers", + "following_url": "https://api.github.com/users/gagbo/following{/other_user}", + "gists_url": "https://api.github.com/users/gagbo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gagbo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gagbo/subscriptions", + "organizations_url": "https://api.github.com/users/gagbo/orgs", + "repos_url": "https://api.github.com/users/gagbo/repos", + "events_url": "https://api.github.com/users/gagbo/events{/privacy}", + "received_events_url": "https://api.github.com/users/gagbo/received_events", + "type": "User", + "site_admin": false + }, + "repository_selection": "all", + "access_tokens_url": "https://api.github.com/app/installations/39593520/access_tokens", + "repositories_url": "https://api.github.com/installation/repositories", + "html_url": "https://github.com/settings/installations/39593520", + "app_id": 360617, + "app_slug": "gagbo-test-app", + "target_id": 88888, + "target_type": "User", + "permissions": { + "issues": "write", + "actions": "write", + "metadata": "read", + "pull_requests": "write" + }, + "events": [ + "issues", + "issue_comment", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "pull_request_review_thread", + "repository" + ], + "created_at": "2023-07-13T11:35:31.000+02:00", + "updated_at": "2023-07-13T11:35:32.000+02:00", + "single_file_name": null, + "has_multiple_single_files": false, + "single_file_paths": [], + "suspended_by": null, + "suspended_at": null + }, + "repositories": [ + { + "id": 29128586, + "node_id": "MDEwOlJlcG9zaXRvcnkyOTEyODU4Ng==", + "name": "ViscoElRebound", + "full_name": "gagbo/ViscoElRebound", + "private": false + }, + { + "id": 109778911, + "node_id": "MDEwOlJlcG9zaXRvcnkxMDk3Nzg5MTE=", + "name": "OSSU", + "full_name": "gagbo/OSSU", + "private": true + }, + { + "id": 665086759, + "node_id": "R_kgDOJ6RrJw", + "name": "octocrab", + "full_name": "gagbo/octocrab", + "private": false + } + ], + "requester": null, + "sender": { + "login": "gagbo", + "id": 88888, + "node_id": "PLACEHOLDER_NODE_ID", + "avatar_url": "https://avatars.githubusercontent.com/u/88888?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/gagbo", + "html_url": "https://github.com/gagbo", + "followers_url": "https://api.github.com/users/gagbo/followers", + "following_url": "https://api.github.com/users/gagbo/following{/other_user}", + "gists_url": "https://api.github.com/users/gagbo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gagbo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gagbo/subscriptions", + "organizations_url": "https://api.github.com/users/gagbo/orgs", + "repos_url": "https://api.github.com/users/gagbo/repos", + "events_url": "https://api.github.com/users/gagbo/events{/privacy}", + "received_events_url": "https://api.github.com/users/gagbo/received_events", + "type": "User", + "site_admin": false + } +}