-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebhook.py
More file actions
95 lines (75 loc) · 3.09 KB
/
Copy pathwebhook.py
File metadata and controls
95 lines (75 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Optional HTTP webhook to push projects onto a board from outside Discord.
Disabled unless WEBHOOK_SECRET is set. When on, it exposes:
POST /task (or /task/<board-key> to target a specific board)
Header: Authorization: Bearer <secret> (or X-Webhook-Secret: <secret>)
Body: {"name": "...", "cat": "...", "loc": "...",
"link": "...", "desc": "...", "priority": "high|normal|standby|low"}
Only "name" is required.
GET /health -> {"ok": true}
Anyone with the secret can add projects, so treat it like a password: only
enable it behind HTTPS / a reverse proxy, and rotate it if it leaks.
"""
import hmac
from aiohttp import web
import config
import board
def _authorized(request):
given = request.headers.get("X-Webhook-Secret")
if not given:
auth = request.headers.get("Authorization", "")
if auth.lower().startswith("bearer "):
given = auth[7:]
if not given:
return False
return hmac.compare_digest(given, config.WEBHOOK_SECRET)
def _default_kind():
return next(iter(config.BOARDS))
def build_app(client):
routes = web.RouteTableDef()
@routes.get("/health")
async def health(request):
return web.json_response({"ok": True, "boards": list(config.BOARDS)})
async def _create(request, kind):
if not _authorized(request):
return web.json_response({"error": "unauthorized"}, status=401)
if kind not in config.BOARDS:
return web.json_response(
{"error": "unknown board", "boards": list(config.BOARDS)}, status=404)
try:
payload = await request.json()
except Exception:
return web.json_response({"error": "invalid JSON body"}, status=400)
name = (payload.get("name") or "").strip()
if not name:
return web.json_response({"error": "'name' is required"}, status=400)
d = board.load(kind)
p = board.add_project(
d, name,
cat=payload.get("cat") or "",
loc=payload.get("loc") or "",
link=payload.get("link") or "",
desc=payload.get("desc") or "",
priority=payload.get("priority") or "normal",
by=None,
by_name=payload.get("by") or "webhook",
)
board.save(kind, d)
client.loop.create_task(board.refresh(client, kind))
return web.json_response({"ok": True, "id": p["id"], "board": kind}, status=201)
@routes.post("/task")
async def create_default(request):
return await _create(request, _default_kind())
@routes.post("/task/{kind}")
async def create_for(request):
return await _create(request, request.match_info["kind"])
app = web.Application()
app.add_routes(routes)
return app
async def start(client):
app = build_app(client)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, config.WEBHOOK_HOST, config.WEBHOOK_PORT)
await site.start()
print("[webhook] listening on {}:{}".format(config.WEBHOOK_HOST, config.WEBHOOK_PORT))
return runner