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 # 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, 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.
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 ## License

View File

@ -1,11 +1,13 @@
gitea_instance: git.arcticsoftware.no gitea_instance: git.arcticsoftware.no
gitea_token: ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC123A gitea_token: ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC123A
webhook_token: ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC1ABC123ABC123A
gitea_owner: someowner gitea_owner: someowner
gitea_repo: somerepo gitea_repo: somerepo
gitea_bug_label: 1 gitea_bug_label: 1
gitea_feature_label: 1 gitea_feature_label: 1
feature_issue_room: '!someroom:matrix.server.com' feature_issue_room: '!someroom:matrix.server.com'
bug_issue_room: '!someotherrroom:matrix.server.com' bug_issue_room: '!someotherrroom:matrix.server.com'
webhook_announcement_room: '!announcementroom:matrix.server.com'
about_text: | about_text: |
Jeg er ment som en bro mellom Arctic Software sitt kodedeponi og Jeg er ment som en bro mellom Arctic Software sitt kodedeponi og
matrix chatten. Du kan sende forskjellige kommandoer til meg og matrix chatten. Du kan sende forskjellige kommandoer til meg og
@ -44,7 +46,7 @@ about_text: |
<code>Eksempel: "!sak 83"</code><br /><br /> <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 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 /> 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) Denne saken ble videresendt fra dev bot fra [matrise serveren](https://chat.zuul.no)
Brukeren ({nick}) som opprettet saken, kan kontaktes der ({userId}) 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: | 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:** **Eksempel:**
```!ny bug "Jeg fant en bug" "Dette er en lengre beskrivelse av buggen"``` ```!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 Har sendt deg en PM med informasjon om meg selv og hvordan du sender kommandoer til meg
command_pm_success: | command_pm_success: |
Har sendt deg en PM med resultatet fra forespørselen din 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: | 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 time
import requests import requests
import asyncio import asyncio
import hmac
import hashlib
import shlex import shlex
import aiohttp import aiohttp
from aiohttp.web import Request, Response, json_response
from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream
from mautrix.types import (Event, StateEvent, EventID, UserID, EventType, from mautrix.types import (Event, StateEvent, EventID, UserID, EventType,
@ -13,27 +16,32 @@ from mautrix.types import (Event, StateEvent, EventID, UserID, EventType,
Membership) Membership)
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent from maubot import Plugin, MessageEvent
from maubot.handlers import command, event from maubot.handlers import command, event, web
class Config(BaseProxyConfig): class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None: def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("gitea_instance") helper.copy("gitea_instance")
helper.copy("gitea_token") helper.copy("gitea_token")
helper.copy("webhook_token")
helper.copy("gitea_owner") helper.copy("gitea_owner")
helper.copy("gitea_repo") helper.copy("gitea_repo")
helper.copy("gitea_bug_label") helper.copy("gitea_bug_label")
helper.copy("gitea_feature_label") helper.copy("gitea_feature_label")
helper.copy("feature_issue_room") helper.copy("feature_issue_room")
helper.copy("bug_issue_room") helper.copy("bug_issue_room")
helper.copy("webhook_announcement_room")
helper.copy("about_text") helper.copy("about_text")
helper.copy("issue_bug_text") helper.copy("issue_bug_text")
helper.copy("issue_feature_text") helper.copy("issue_feature_text")
helper.copy("issue_disclaimer_text") helper.copy("issue_disclaimer_text")
helper.copy("issue_details_text")
helper.copy("command_pm_error") helper.copy("command_pm_error")
helper.copy("command_pm_help") helper.copy("command_pm_help")
helper.copy("command_pm_success") helper.copy("command_pm_success")
helper.copy("command_new_help") helper.copy("command_new_help")
helper.copy("command_case_error")
helper.copy("command_success") helper.copy("command_success")
helper.copy("webhook_announce_push")
class DevopsBot(Plugin): class DevopsBot(Plugin):
async def start(self) -> None: async def start(self) -> None:
@ -48,6 +56,9 @@ class DevopsBot(Plugin):
def g_token(self) -> str: def g_token(self) -> str:
return self.config["gitea_token"] return self.config["gitea_token"]
def g_wh_token(self) -> str:
return self.config["webhook_token"]
def g_owner(self) -> str: def g_owner(self) -> str:
return self.config["gitea_owner"] return self.config["gitea_owner"]
@ -63,6 +74,9 @@ class DevopsBot(Plugin):
def g_bug_room(self) -> str: def g_bug_room(self) -> str:
return self.config["bug_issue_room"] return self.config["bug_issue_room"]
def g_webhook_room(self) -> str:
return self.config["webhook_announcement_room"]
def g_feature_room(self) -> str: def g_feature_room(self) -> str:
return self.config["feature_issue_room"] return self.config["feature_issue_room"]
@ -78,6 +92,9 @@ class DevopsBot(Plugin):
def g_disclaimer(self) -> str: def g_disclaimer(self) -> str:
return self.config["issue_disclaimer_text"] 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: def g_c_self_error(self) -> str:
return self.config["command_pm_error"] return self.config["command_pm_error"]
@ -90,9 +107,15 @@ class DevopsBot(Plugin):
def g_c_new_help(self) -> str: def g_c_new_help(self) -> str:
return self.config["command_new_help"] 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: def g_c_command_success(self) -> str:
return self.config["command_success"] 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 # Match all the registered pm rooms in pm_rooms with
# the username and return the room id # the username and return the room id
async def has_pm_room(self, user) -> str: async def has_pm_room(self, user) -> str:
@ -144,6 +167,106 @@ class DevopsBot(Plugin):
response.raise_for_status() response.raise_for_status()
return await response.json() 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.new(name="ny", aliases=["new"])
@command.argument("args", pass_raw=True, required=False) @command.argument("args", pass_raw=True, required=False)
async def ny(self, evt: MessageEvent, args: str = None) -> None: async def ny(self, evt: MessageEvent, args: str = None) -> None:
@ -267,7 +390,7 @@ class DevopsBot(Plugin):
# command output # command output
pm_room = await self.has_pm_room(evt.sender) 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) await self.client.send_text(pm_room, html=output)
else: else:
pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender]) pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender])
@ -300,7 +423,7 @@ class DevopsBot(Plugin):
# command output # command output
pm_room = await self.has_pm_room(evt.sender) 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) await self.client.send_text(pm_room, html=pm)
else: else:
pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender]) pm_room = await self.client.create_room(is_direct=True, invitees=[evt.sender])
@ -309,6 +432,51 @@ class DevopsBot(Plugin):
await evt.reply(self.g_c_self_help()) 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 @classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]: def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config return Config

View File

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