ggdgsdbsdbbb / webhook.v
211 lines · 190 sloc · 4.7 KB · acf8873d8ea8d46d14acc6993957982d945d2b29
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import time
6import net.http
7import crypto.hmac
8import crypto.sha256
9import encoding.hex
10import json
11
12pub struct WebhookIssuePayload {
13 action string
14 repo string
15 title string
16 author string
17}
18
19pub struct WebhookPrPayload {
20 action string
21 repo string
22 number int
23 title string
24 author string
25 head string
26 base string
27}
28
29pub struct WebhookCommentPayload {
30 action string
31 repo string
32 target string // 'issue' or 'pr'
33 number int
34 author string
35 text string
36}
37
38pub struct WebhookReleasePayload {
39 action string
40 repo string
41 tag string
42 author string
43}
44
45pub struct WebhookPushPayload {
46 repo string
47 ref string
48 author string
49}
50
51struct Webhook {
52 id int @[primary; sql: serial]
53mut:
54 repo_id int
55 url string
56 secret string
57 events string // comma-separated: push,issue,pr,comment,release
58 is_active bool
59 created_at int
60 last_status int
61 last_delivery int
62}
63
64struct WebhookDelivery {
65 id int @[primary; sql: serial]
66mut:
67 webhook_id int
68 event string
69 status_code int
70 response_body string
71 created_at int
72}
73
74fn (w &Webhook) has_event(name string) bool {
75 if w.events == '' {
76 return true
77 }
78 for ev in w.events.split(',') {
79 if ev.trim_space() == name {
80 return true
81 }
82 }
83 return false
84}
85
86fn (w &Webhook) event_list() []string {
87 mut out := []string{}
88 for ev in w.events.split(',') {
89 t := ev.trim_space()
90 if t != '' {
91 out << t
92 }
93 }
94 return out
95}
96
97fn (mut app App) add_webhook(repo_id int, url string, secret string, events string) ! {
98 wh := Webhook{
99 repo_id: repo_id
100 url: url
101 secret: secret
102 events: events
103 is_active: true
104 created_at: int(time.now().unix())
105 }
106 sql app.db {
107 insert wh into Webhook
108 }!
109}
110
111fn (mut app App) list_repo_webhooks(repo_id int) []Webhook {
112 return sql app.db {
113 select from Webhook where repo_id == repo_id order by id desc
114 } or { []Webhook{} }
115}
116
117fn (mut app App) find_webhook_by_id(id int) ?Webhook {
118 rows := sql app.db {
119 select from Webhook where id == id limit 1
120 } or { []Webhook{} }
121 if rows.len == 0 {
122 return none
123 }
124 return rows.first()
125}
126
127fn (mut app App) delete_webhook(id int) ! {
128 sql app.db {
129 delete from Webhook where id == id
130 }!
131 sql app.db {
132 delete from WebhookDelivery where webhook_id == id
133 }!
134}
135
136fn (mut app App) delete_repo_webhooks(repo_id int) ! {
137 whs := app.list_repo_webhooks(repo_id)
138 for wh in whs {
139 app.delete_webhook(wh.id) or {}
140 }
141}
142
143fn (mut app App) toggle_webhook(id int, active bool) ! {
144 sql app.db {
145 update Webhook set is_active = active where id == id
146 }!
147}
148
149fn (mut app App) record_webhook_delivery(webhook_id int, event string, status int, body string) {
150 d := WebhookDelivery{
151 webhook_id: webhook_id
152 event: event
153 status_code: status
154 response_body: body
155 created_at: int(time.now().unix())
156 }
157 sql app.db {
158 insert d into WebhookDelivery
159 } or { return }
160 sql app.db {
161 update Webhook set last_status = status, last_delivery = d.created_at where id == webhook_id
162 } or { return }
163}
164
165fn (mut app App) recent_webhook_deliveries(webhook_id int, limit int) []WebhookDelivery {
166 return sql app.db {
167 select from WebhookDelivery where webhook_id == webhook_id order by id desc limit limit
168 } or { []WebhookDelivery{} }
169}
170
171// dispatch_webhook fires a webhook delivery in a background spawn.
172// payload is any serializable value; it's JSON-encoded with `json.encode`.
173fn (mut app App) dispatch_webhook[T](repo_id int, event string, payload T) {
174 body := json.encode(payload)
175 app.fan_out_webhook(repo_id, event, body)
176}
177
178fn (mut app App) fan_out_webhook(repo_id int, event string, body string) {
179 hooks := app.list_repo_webhooks(repo_id)
180 for wh in hooks {
181 if !wh.is_active {
182 continue
183 }
184 if !wh.has_event(event) {
185 continue
186 }
187 spawn app.deliver_webhook(wh, event, body)
188 }
189}
190
191fn (mut app App) deliver_webhook(wh Webhook, event string, body string) {
192 mut signature := ''
193 if wh.secret != '' {
194 sig_bytes := hmac.new(wh.secret.bytes(), body.bytes(), sha256.sum, sha256.block_size)
195 signature = 'sha256=' + hex.encode(sig_bytes)
196 }
197 mut req := http.new_request(.post, wh.url, body)
198 req.header.add(.content_type, 'application/json')
199 req.header.add_custom('X-Gitly-Event', event) or {}
200 if signature != '' {
201 req.header.add_custom('X-Gitly-Signature', signature) or {}
202 }
203 req.read_timeout = 10 * time.second
204 req.write_timeout = 10 * time.second
205 resp := req.do() or {
206 app.record_webhook_delivery(wh.id, event, 0, err.str())
207 return
208 }
209 preview := if resp.body.len > 500 { resp.body[..500] } else { resp.body }
210 app.record_webhook_delivery(wh.id, event, resp.status_code, preview)
211}
212