OXYGENHUB
# ff6569530173 oxygenhub / crates / hub / src / web / webhooks.rs
5.0 KiB · Rust · last changed Organisation webhooks under the organisation's settings, sharing the repository webhook code; an admin storage page with sizes, loose objects and packs, and maintenance (oxy gc) per repository or for all 1 month ago source blame history raw
1
//! Webhooks of a repository or an organisation (hub spec §12.2): the
2
//! settings form, deliveries and redelivery, shared by both scopes.
3
4
use super::{filters, Error, Page, RepoShell, Shell};
5
use crate::auth::Viewer;
6
use crate::db::{self, accounts};
7
use crate::Shared;
8
use askama::Template;
9
use serde::Deserialize;
10
11
/// Whose webhooks.
12
pub enum Scope {
13
    Repo { id: i64, slug: String },
14
    Org { id: i64, name: String },
15
}
16
17
impl Scope {
18
    fn owns(&self, h: &db::hooks::Hook) -> bool {
19
        match self {
20
            Scope::Repo { id, .. } => h.repo_id == Some(*id),
21
            Scope::Org { id, .. } => h.org_id == Some(*id),
22
        }
23
    }
24
25
    /// Where the settings page is.
26
    pub fn settings_url(&self) -> String {
27
        match self {
28
            Scope::Repo { slug, .. } => format!("/{slug}/settings"),
29
            Scope::Org { name, .. } => format!("/orgs/{name}/settings"),
30
        }
31
    }
32
33
    fn label(&self) -> &str {
34
        match self {
35
            Scope::Repo { slug, .. } => slug,
36
            Scope::Org { name, .. } => name,
37
        }
38
    }
39
}
40
41
#[derive(Deserialize)]
42
pub struct HookForm {
43
    #[serde(rename = "_csrf")]
44
    pub csrf_token: String,
45
    #[serde(default)]
46
    url: String,
47
    #[serde(default)]
48
    secret: String,
49
    #[serde(default = "star")]
50
    events: String,
51
    #[serde(default)]
52
    id: Option<i64>,
53
    #[serde(default)]
54
    action: String,
55
}
56
57
fn star() -> String {
58
    "*".into()
59
}
60
61
/// Apply the webhook form: `Ok(notice)` or `Err(message for the user)`.
62
pub async fn apply(app: &Shared, viewer: &Viewer, scope: &Scope, f: HookForm) -> sqlx::Result<Result<String, String>> {
63
    let owned = |h: Option<db::hooks::Hook>| h.filter(|h| scope.owns(h));
64
    Ok(Ok(match (f.action.as_str(), f.id) {
65
        ("delete", Some(id)) => {
66
            if owned(db::hooks::hook(&app.db, id).await?).is_some() {
67
                db::hooks::delete_hook(&app.db, id).await?;
68
                accounts::audit(&app.db, viewer.account(), "hook.deleted", scope.label(), "").await.ok();
69
            }
70
            "Webhook deleted.".to_string()
71
        }
72
        ("toggle", Some(id)) => {
73
            if let Some(h) = owned(db::hooks::hook(&app.db, id).await?) {
74
                db::hooks::set_hook_active(&app.db, id, !h.active).await?;
75
            }
76
            "Webhook updated.".to_string()
77
        }
78
        ("ping", Some(id)) => {
79
            if let Some(h) = owned(db::hooks::hook(&app.db, id).await?) {
80
                let delivery = crate::auth::random_hex(16);
81
                let env = hub_api::WebhookEnvelope {
82
                    event: "ping".into(),
83
                    delivery: delivery.clone(),
84
                    repository: None,
85
                    actor: viewer.account().map(|a| a.username.clone()).unwrap_or_default(),
86
                    at_ms: db::now_ms(),
87
                    data: serde_json::json!({ "zen": "changes commute" }),
88
                };
89
                db::hooks::queue_delivery(&app.db, h.id, &delivery, "ping", &serde_json::to_string(&env).unwrap_or_default()).await?;
90
                app.events.notify_deliveries();
91
            }
92
            "Ping queued.".to_string()
93
        }
94
        _ => {
95
            if !(f.url.starts_with("http://") || f.url.starts_with("https://")) {
96
                return Ok(Err("A webhook URL starts with http:// or https://.".into()));
97
            }
98
            let (repo_id, org_id) = match scope {
99
                Scope::Repo { id, .. } => (Some(*id), None),
100
                Scope::Org { id, .. } => (None, Some(*id)),
101
            };
102
            db::hooks::create_hook(&app.db, repo_id, org_id, f.url.trim(), f.secret.trim(), f.events.trim()).await?;
103
            accounts::audit(&app.db, viewer.account(), "hook.created", scope.label(), f.url.trim()).await.ok();
104
            "Webhook added.".to_string()
105
        }
106
    }))
107
}
108
109
#[derive(Template)]
110
#[template(path = "deliveries.html")]
111
pub struct DeliveriesPage {
112
    shell: Shell,
113
    /// The repository's tabs, for a repository's webhook.
114
    repo: Option<RepoShell>,
115
    hook: db::hooks::Hook,
116
    rows: Vec<db::hooks::Delivery>,
117
    settings_url: String,
118
}
119
120
pub async fn deliveries_page(
121
    app: &Shared,
122
    shell: Shell,
123
    scope: &Scope,
124
    repo: Option<RepoShell>,
125
    id: i64,
126
) -> Result<Page<DeliveriesPage>, Error> {
127
    let hook = db::hooks::hook(&app.db, id)
128
        .await
129
        .map_err(|e| Error::from_db(shell.clone(), e))?
130
        .filter(|h| scope.owns(h))
131
        .ok_or_else(|| Error::not_found(shell.clone()))?;
132
    let rows = db::hooks::deliveries_for_hook(&app.db, id, 100).await.map_err(|e| Error::from_db(shell.clone(), e))?;
133
    Ok(Page(DeliveriesPage { shell, repo, hook, rows, settings_url: scope.settings_url() }))
134
}
135
136
pub async fn redeliver(app: &Shared, scope: &Scope, id: i64, delivery: i64) -> sqlx::Result<()> {
137
    let mine = db::hooks::hook(&app.db, id).await?.is_some_and(|h| scope.owns(&h));
138
    if mine && db::hooks::delivery(&app.db, delivery).await?.is_some_and(|d| d.hook_id == id) {
139
        db::hooks::redeliver(&app.db, delivery).await?;
140
        app.events.notify_deliveries();
141
    }
142
    Ok(())
143
}