Added webhooks handling
This commit is contained in:
parent
1b53815a33
commit
a65037713c
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
174
devops_bot.py
174
devops_bot.py
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
@ -77,6 +91,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,8 +107,14 @@ 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
|
||||||
|
|
@ -143,6 +166,106 @@ class DevopsBot(Plugin):
|
||||||
async with session.post(url, headers=headers, json=issue_data) as response:
|
async with session.post(url, headers=headers, json=issue_data) as response:
|
||||||
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)
|
||||||
|
|
@ -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])
|
||||||
|
|
@ -308,6 +431,51 @@ class DevopsBot(Plugin):
|
||||||
await self.client.send_text(pm_room, html=pm)
|
await self.client.send_text(pm_room, html=pm)
|
||||||
|
|
||||||
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]:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -8,4 +8,5 @@ config: true
|
||||||
extra_files:
|
extra_files:
|
||||||
- base-config.yaml
|
- base-config.yaml
|
||||||
main_class: DevopsBot
|
main_class: DevopsBot
|
||||||
database: false
|
database: false
|
||||||
|
webapp: true
|
||||||
Loading…
Reference in New Issue
Block a user