Added webhooks handling

This commit is contained in:
Helge-Mikael Nordgård 2024-08-19 02:18:27 +02:00
parent 1b53815a33
commit a65037713c
4 changed files with 204 additions and 12 deletions

View File

@ -1,8 +1,6 @@
# Devops Bot
A bot made for use with the matrix chat and communications system that pulls information from a gitea instance. For now it only retrieves and makes issues in the issue database,
but future functions will also include running automated tests through another (probably
custom) API and displaying the results of the tests in the chat.
A bot made for use with the matrix chat and communications system that pulls information from a gitea instance. For now it only retrieves and makes issues in the issue database, and display push messages to the repository via webhooks. But future functions will also include running automated tests through another (probably custom) API and displaying the results of the tests in the chat.
## License

View File

@ -1,11 +1,13 @@
gitea_instance: git.arcticsoftware.no
gitea_token: ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC123A
webhook_token: ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC123A
gitea_owner: someowner
gitea_repo: somerepo
gitea_bug_label: 1
gitea_feature_label: 1
feature_issue_room: '!someroom:matrix.server.com'
bug_issue_room: '!someotherrroom:matrix.server.com'
webhook_announcement_room: '!announcementroom:matrix.server.com'
about_text: |
Jeg er ment som en bro mellom Arctic Software sitt kodedeponi og
matrix chatten. Du kan sende forskjellige kommandoer til meg og
@ -44,7 +46,7 @@ about_text: |
<code>Eksempel: "!sak 83"</code><br /><br />
Denne botten er lagd/kodet av Helge-Mikael Nordgård og
Denne botten er lagd/utviklet av Helge-Mikael Nordgård og
eventuelle spørsmål om bruk/utvikling av den kan rettes til
meg <a href="mailto:surface-fancy-deem@duck.com">per mail</a>.<br /><br />
@ -71,10 +73,15 @@ issue_disclaimer_text: |
Denne saken ble videresendt fra dev bot fra [matrise serveren](https://chat.zuul.no)
Brukeren ({nick}) som opprettet saken, kan kontaktes der ({userId})
issue_details_text: |
🚩 <em>Sak nr. {saksnr}</em> 🚩 ({type})
<h3>{title}</h3>
<blockquote>{description}</blockquote>
Saken på kodedeponiet kan leses 🔗 <a href="{link}">her</a>
command_new_help: |
**"!Ny" Kommando hjelp:**
**"!ny" Kommando hjelp:**
Skriv ny [type] "[tittel]" "[Beskrivelse]" for å opprette en ny sak.
Skriv !ny [type] "[tittel]" "[Beskrivelse]" for å opprette en ny sak.
**Eksempel:**
```!ny bug "Jeg fant en bug" "Dette er en lengre beskrivelse av buggen"```
@ -90,5 +97,23 @@ command_pm_help: |
Har sendt deg en PM med informasjon om meg selv og hvordan du sender kommandoer til meg
command_pm_success: |
Har sendt deg en PM med resultatet fra forespørselen din
command_case_error: |
**"!sak" Kommando hjelp:**
Skriv !sak [saksnr] for å hente informasjon om en sak fra kodedeponiet
**Eksempel:**
```!sak 44```
For å hente informasjon om sak med saksnr 44
{error}
command_success: |
Forespørselen din ble utført. {result}
Forespørselen din ble utført. {result}
webhook_announce_push: |
<h3>🛠️ Push til kodedeponi {repo} 🛠️</h3>
{user} pushet ny kode til {branch} med melding:<br /><br />
<blockquote>{message}</blockquote>

View File

@ -4,8 +4,11 @@ import json
import time
import requests
import asyncio
import hmac
import hashlib
import shlex
import aiohttp
from aiohttp.web import Request, Response, json_response
from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream
from mautrix.types import (Event, StateEvent, EventID, UserID, EventType,
@ -13,27 +16,32 @@ from mautrix.types import (Event, StateEvent, EventID, UserID, EventType,
Membership)
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent
from maubot.handlers import command, event
from maubot.handlers import command, event, web
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("gitea_instance")
helper.copy("gitea_token")
helper.copy("webhook_token")
helper.copy("gitea_owner")
helper.copy("gitea_repo")
helper.copy("gitea_bug_label")
helper.copy("gitea_feature_label")
helper.copy("feature_issue_room")
helper.copy("bug_issue_room")
helper.copy("webhook_announcement_room")
helper.copy("about_text")
helper.copy("issue_bug_text")
helper.copy("issue_feature_text")
helper.copy("issue_disclaimer_text")
helper.copy("issue_details_text")
helper.copy("command_pm_error")
helper.copy("command_pm_help")
helper.copy("command_pm_success")
helper.copy("command_new_help")
helper.copy("command_case_error")
helper.copy("command_success")
helper.copy("webhook_announce_push")
class DevopsBot(Plugin):
async def start(self) -> None:
@ -48,6 +56,9 @@ class DevopsBot(Plugin):
def g_token(self) -> str:
return self.config["gitea_token"]
def g_wh_token(self) -> str:
return self.config["webhook_token"]
def g_owner(self) -> str:
return self.config["gitea_owner"]
@ -63,6 +74,9 @@ class DevopsBot(Plugin):
def g_bug_room(self) -> str:
return self.config["bug_issue_room"]
def g_webhook_room(self) -> str:
return self.config["webhook_announcement_room"]
def g_feature_room(self) -> str:
return self.config["feature_issue_room"]
@ -77,6 +91,9 @@ class DevopsBot(Plugin):
def g_disclaimer(self) -> str:
return self.config["issue_disclaimer_text"]
def g_issue_details(self) -> str:
return self.config["issue_details_text"]
def g_c_self_error(self) -> str:
return self.config["command_pm_error"]
@ -90,8 +107,14 @@ class DevopsBot(Plugin):
def g_c_new_help(self) -> str:
return self.config["command_new_help"]
def g_c_case_error(self) -> str:
return self.config["command_case_error"]
def g_c_command_success(self) -> str:
return self.config["command_success"]
def g_w_announce_push(self) -> str:
return self.config["webhook_announce_push"]
# Match all the registered pm rooms in pm_rooms with
# the username and return the room id
@ -143,6 +166,106 @@ class DevopsBot(Plugin):
async with session.post(url, headers=headers, json=issue_data) as response:
response.raise_for_status()
return await response.json()
# Fetch a single issue from the configured repository
async def get_issue(self, case) -> list:
url = f"https://{self.g_instance()}/api/v1/repos/{self.g_owner()}/{self.g_repo()}/issues/{case}"
headers = {
"Authorization": f"token {self.g_token()}",
"Accept": "application/json"
}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
response.raise_for_status()
return await response.json()
@command.new(name="sak", aliases=["case"])
@command.argument("saksnr", pass_raw=False, required=False)
async def sak(self, evt: MessageEvent, saksnr: str = None) -> None:
# We want to see the commands ran in a open room, so
# we can get a sense of it's usage
if evt.room_id in self.pm_rooms:
await evt.reply(self.g_c_self_error())
return
# Check correct formatting of the arguments first
if not saksnr:
await evt.reply(self.g_c_case_error().format(
error="Feil: saksnr er påkrevd"
))
return
elif not saksnr.isdigit():
await evt.reply(self.g_c_case_error().format(
error="Feil: saksnr må være et heltall"
))
return
elif not int(saksnr) > 0:
await evt.reply(self.g_c_case_error().format(
error="Feil: saksnr må ha et tall høyere enn 0"
))
return
try:
issue = await self.get_issue(saksnr)
# Determine what type of issue we're dealing with
issueType = 'Ukjent'
for label in issue["labels"]:
if label["id"] == self.g_bug_label():
issueType = 'Bug'
break
if label["id"] == self.g_feature_label():
issueType = 'Forbedring'
break
if issueType == 'Bug':
issueContent = self.g_issue_details().format(
saksnr=issue["number"],
type="🪲 Bug",
title=issue["title"],
description=issue["body"],
link=issue["html_url"]
)
elif issueType == 'Forbedring':
issueContent = self.g_issue_details().format(
saksnr=issue["number"],
type="🎫 Forbedring",
title=issue["title"],
description=issue["body"],
link=issue["html_url"]
)
else:
issueContent = self.g_issue_details().format(
saksnr=issue["number"],
type="❓ Ukjent",
title=issue["title"],
description=issue["body"],
link=issue["html_url"]
)
# Check if the bot already has a PM session with the user
# so we don't stack up invites to new rooms for each
# command output
pm_room = await self.has_pm_room(evt.sender)
if pm_room != 'NONE':
await self.client.send_text(pm_room, html=issueContent)
else:
pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender])
self.pm_rooms.add(pm_room)
await self.client.send_text(pm_room, html=issueContent)
await evt.reply(self.g_c_self_success())
except aiohttp.ClientError as e:
await evt.reply(f"Feil ved forespørsel: {e}")
except aiohttp.ContentTypeError as e:
await evt.reply(f"Feil ved syntaktisk analyse av JSON forespørsel: {e}")
except KeyError as e:
await evt.reply(f"En feil oppstod ved forsøk på å hente saksdata fra ny sak: {e}")
except Exception as e:
await evt.reply(f"En uventet feil oppstod: {e}")
@command.new(name="ny", aliases=["new"])
@command.argument("args", pass_raw=True, required=False)
@ -267,7 +390,7 @@ class DevopsBot(Plugin):
# command output
pm_room = await self.has_pm_room(evt.sender)
if pm_room is not 'NONE':
if pm_room != 'NONE':
await self.client.send_text(pm_room, html=output)
else:
pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender])
@ -300,7 +423,7 @@ class DevopsBot(Plugin):
# command output
pm_room = await self.has_pm_room(evt.sender)
if pm_room is not 'NONE':
if pm_room != 'NONE':
await self.client.send_text(pm_room, html=pm)
else:
pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender])
@ -308,6 +431,51 @@ class DevopsBot(Plugin):
await self.client.send_text(pm_room, html=pm)
await evt.reply(self.g_c_self_help())
@web.post("/webhook")
async def post_webhook(self, request: Request) -> Response:
content_type = request.headers.get('Content-Type', '').lower().strip()
if 'application/json' not in content_type:
return Response(status=415, text="Unsupported or no media type in header")
try:
payload = await request.text()
except Exception as e:
self.log.warning(msg=f"Devbot recieved invalid payload {str(e)}")
return Response(status=400, text="Bad request")
if not payload:
return Response(status=400, text="Bad request")
header_signature = request.headers.get('X-Gitea-Signature', '')
if not header_signature:
self.log.warning(msg=f"Devbot got a signature without gitea header signature")
return Response(status=400, text="Bad request")
payload_signature = hmac.new(self.g_wh_token().encode(), payload.encode(), hashlib.sha256).hexdigest()
if header_signature != payload_signature:
self.log.warning(msg=f"Devbot got a webhook signal with an invalid signature")
return Response(status=403, text="Forbidden")
try:
decoded = json.loads(payload)
except json.JSONDecodeError as e:
self.log.warning(msg=f"Devbot cannot parse the payload from json - {str(e)}")
return Response(status=400, text="Bad request")
event_type = request.headers.get("X-Gitea-Event")
if event_type == 'push':
await self.client.send_notice(self.g_webhook_room(), html=self.g_w_announce_push().format(
repo=decoded['repository']['full_name'],
user=decoded['pusher']['full_name'],
branch=decoded['ref'].split('/')[-1],
message=decoded['commits'][0]['message']
))
return Response(status=200, text="Request OK")
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:

View File

@ -1,6 +1,6 @@
maubot: 0.1.0
id: no.arcticsoftware.devopsbot
version: 0.1.34
version: 0.1.4
license: AGPL-3.0-or-later
modules:
- devops_bot
@ -8,4 +8,5 @@ config: true
extra_files:
- base-config.yaml
main_class: DevopsBot
database: false
database: false
webapp: true