Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
executable file
·212 lines (170 loc) · 5.66 KB
/
Copy pathapp.py
File metadata and controls
executable file
·212 lines (170 loc) · 5.66 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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env python3
import hashlib
import hmac
import json
import logging
import os
import uuid
from contextlib import asynccontextmanager
from typing import Annotated
import blibs
import httpx
import yaml
from asgi_logger.middleware import AccessLoggerMiddleware
from fastapi import FastAPI
from fastapi import Header
from fastapi import HTTPException
from fastapi import Request
from fastapi import status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware import Middleware
from fastapi.responses import JSONResponse
from fastapi.responses import RedirectResponse
from jinja2 import Environment
from jinja2 import FileSystemLoader
from jinja2 import Template
from config import DefaultConfig
from jira_model import JiraIssue
from jira_model import JiraIssueEnvelope
# from fastapi.middleware.cors import CORSMiddleware
config = DefaultConfig()
# Configure logging
blibs.init_root_logger()
logger = logging.getLogger(__name__)
logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger("msrest").setLevel(logging.ERROR)
logging.getLogger("msal").setLevel(logging.ERROR)
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("starting app version %s", app.version)
yield
app: FastAPI = FastAPI(
title="Teams Notifier jira-api",
version=os.environ.get("VERSION", "v0.0.0-dev"),
lifespan=lifespan,
middleware=[
Middleware(
AccessLoggerMiddleware, # type: ignore
format='%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(L)ss', # noqa # type: ignore
)
],
)
env = Environment(loader=FileSystemLoader("./cards/"))
templ: Template = env.get_template("changes.yaml.j2")
client: httpx.AsyncClient = httpx.AsyncClient()
# For 422 debugging purpose :)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
logging.error(f"{request}: {exc_str}")
with open("OUT_error.json", "w") as fp:
json.dump(exc.args, fp, indent=2)
content = {"status_code": 10422, "message": exc_str, "data": None}
return JSONResponse(content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@app.get("/", response_class=RedirectResponse, status_code=302)
async def root():
return "/docs"
def validate_shared_secret_token(token: str) -> None:
if not config.is_valid_token(token):
raise HTTPException(status_code=403, detail=f"Invalid shared secret token {token}")
def validate_uuid(val: str) -> str | None:
try:
return str(uuid.UUID(str(val)))
except ValueError:
return None
@app.post("/api/v1/issue")
async def api_v1_issue(
payload: JiraIssue | JiraIssueEnvelope,
x_shared_secret_token: Annotated[str, Header()],
x_conversation_token: Annotated[str, Header()],
):
validate_shared_secret_token(x_shared_secret_token)
issue: JiraIssue
if isinstance(payload, JiraIssueEnvelope):
issue = payload.issue
else:
issue = payload
conversation_tokens = list(
filter(
None,
[validate_uuid(ct.strip()) for ct in x_conversation_token.split(",")],
)
)
return await send_issue_to_conversations(issue, conversation_tokens)
async def send_issue_to_conversations(
issue: JiraIssue,
conversation_tokens: list[str],
):
try:
fallback = f"{issue.fields.summary} [{issue.key}]"
output = templ.render(
fallback=fallback,
issue=issue,
)
# with open("OUT_changes.yaml", "w") as fp:
# fp.write(output)
# with open("OUT_issue.json", "w") as fp:
# fp.write(json.dumps(issue.model_dump(), indent=2))
for conversation_token in conversation_tokens:
response = await client.post(
config.ACTIVITY_API + "api/v1/message",
json={
"conversation_token": conversation_token,
"card": yaml.safe_load(
output,
),
"summary": fallback,
},
)
response.raise_for_status()
return {"status": "ok"}
except httpx.HTTPStatusError as exc:
raise HTTPException(
status_code=exc.response.status_code,
detail=exc.response.json(),
)
async def get_body(request: Request):
return await request.body()
@app.post("/api/v1/webhook/issue")
async def api_v1_webhook_issue(
request: Request,
x_hub_signature: Annotated[str, Header()],
conversation_token: str,
key_name: str = "",
):
algo, signature = x_hub_signature.split("=")
body: bytes = await request.body()
computed_sig = hmac.new(
config.get_key_by_name(key_name),
msg=body,
digestmod=hashlib.sha256,
).hexdigest()
if computed_sig != signature:
raise HTTPException(
403,
detail="invalid signature",
)
conversation_tokens = list(
filter(
None,
[validate_uuid(ct.strip()) for ct in conversation_token.split(",")],
)
)
return await send_issue_to_conversations(
JiraIssueEnvelope(**json.loads(body)).issue,
conversation_tokens=conversation_tokens,
)
@app.get("/healthz", include_in_schema=False)
async def healthcheck():
return {"ok": True}
if __name__ == "__main__":
# fmt: off
print(
"use fastapi cli to run this app\n"
"- fastapi run # for prod\n"
"- fastapi dev # for dev :)\n"
)
# fmt: on
# for debug entry point
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)