From 1f3073476af7217dc2ea5b1ec5309e43088c44cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge-Mikael=20Nordg=C3=A5rd?= Date: Tue, 13 Aug 2024 04:26:21 +0200 Subject: [PATCH] Added ability to create new issues --- base-config.yaml | 78 ++++++++--- devops_bot.py | 167 ++++++++++++++++++++++-- maubot.yaml | 2 +- no.arcticsoftware.devopsbot-v0.1.34.mbp | Bin 0 -> 17613 bytes 4 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 no.arcticsoftware.devopsbot-v0.1.34.mbp diff --git a/base-config.yaml b/base-config.yaml index e01a1e8..573f10b 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -2,16 +2,20 @@ gitea_instance: git.arcticsoftware.no gitea_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' about_text: | Jeg er ment som en bro mellom Arctic Software sitt kodedeponi og matrix chatten. Du kan sende forskjellige kommandoer til meg og jeg vil utføre de oppgavene du forespør. Her er en liste over kommandoene du kan bruke:

- "!om" eller "!hjelp":
+ !om eller "!hjelp":
Viser denne teksten

- "!saker" [status]:
+ !saker [status]:
Viser en liste over aktive oppgaver i kodedeponiet. Oppgaver kan være buggs som må løses, ønskede fremtidige funksjoner eller andre forespørsler og saker. Å sende status er valgfritt og vil filtrere @@ -19,36 +23,72 @@ about_text: | 'lukket', eller 'alle' for å velge hva slags saker du ønsker å vise.

- Eksempel: "!saker" eller "!saker åpen"

+ Eksempel: "!saker" eller "!saker lukket"

- "!eldre":
- Viser en liste over ferdige/utgåtte oppgaver i kodedeponiet.

- - "!ny" [type]:
+ !ny [type] "[tittel]" "[beskrivelse]":
Oppretter en ny oppgave. Følg opp kommandoen med hva slags type - oppgave du vil opprette, dette kan være enten "bug" eller - "forbedring".

+ oppgave du vil opprette og en tittel og en lengre beskrivelse. + Type kan være enten "bug" eller "forbedring". Tittelen kan maks + være 250 tegn. Men prøv å hold tittelen så kort og konsis som + mulig.

- Etter at du har kjørt denne kommandoen, vil jeg sende deg en PM - hvor du må fullføre beskrivelsen av oppgaven. Jeg vil så lagre - oppgaven i kodedeponiets sakssystem, samtidig som jeg vil - opprette en tråd på matrix serveren brukere kan delta i, så - de kan chatte og diskutere videre om oppgaven.

+ Jeg vil så lagre oppgaven i kodedeponiets sakssystem, samtidig + som jeg vil opprette en tråd på matrix serveren brukere kan + delta i, for å chatte og diskutere videre om oppgaven.

- Eksempel: "!ny bug"

+ Eksempel: !ny bug "Dette er en passe kort tittel" "Lengre beskrivelse"

- "!sak" [saksnr]:
+ !sak [saksnr]:
Viser detaljer om en oppgave/sak. Følg opp kommandoen med et saksnr.

- Eksempel: "!sak 83"

+ Eksempel: "!sak 83"

Denne botten er lagd/kodet av Helge-Mikael Nordgård og eventuelle spørsmål om bruk/utvikling av den kan rettes til - meg per mail. + meg per mail.

+ + Koden for denne botten er lisensiert under AGPL-3.0 og er fritt + tilgjengelig på mitt private + git deponi. +issue_bug_text: | + Ny 🪲 rapport fra {user} ({userId}):

+ + {title} (Saksnr 🚩 {casenr} 🚩) +
+ {description} +
+ Saken på kodedeponiet kan leses 🔗 her. +issue_feature_text: | + Ny 🎫 forespørsel fra {user} ({userId}):

+ + {title} (Saksnr 🚩 {casenr} 🚩) +
+ {description} +
+ Saken på kodedeponiet kan leses 🔗 her. +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}) +command_new_help: | + **"!Ny" Kommando hjelp:** + + Skriv ny [type] "[tittel]" "[Beskrivelse]" for å opprette en ny sak. + + **Eksempel:** + ```!ny bug "Jeg fant en bug" "Dette er en lengre beskrivelse av buggen"``` + + Husk gåseøyne rundt tittel og beskrivelsen så jeg kan klart tyde og skille + de tre argumentene du trenger å oppgi for å opprette en ny sak. (Ikke bruk + gåseøyne for type sak du oppretter, bare tittel og beskrivelse). + + {error} command_pm_error: | Beklager, men du må kjøre kommandoen fra et rom hvor jeg befinner meg utenom personlige meldinger. Vennligst prøv igjen 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 \ No newline at end of file + Har sendt deg en PM med resultatet fra forespørselen din +command_success: | + Forespørselen din ble utført. {result} \ No newline at end of file diff --git a/devops_bot.py b/devops_bot.py index 80c613c..69f5f8b 100644 --- a/devops_bot.py +++ b/devops_bot.py @@ -4,6 +4,7 @@ import json import time import requests import asyncio +import shlex import aiohttp from mautrix.client import Client, InternalEventType, MembershipEventDispatcher, SyncStream @@ -20,10 +21,19 @@ class Config(BaseProxyConfig): helper.copy("gitea_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("about_text") + helper.copy("issue_bug_text") + helper.copy("issue_feature_text") + helper.copy("issue_disclaimer_text") helper.copy("command_pm_error") helper.copy("command_pm_help") helper.copy("command_pm_success") + helper.copy("command_new_help") + helper.copy("command_success") class DevopsBot(Plugin): async def start(self) -> None: @@ -44,8 +54,29 @@ class DevopsBot(Plugin): def g_repo(self) -> str: return self.config["gitea_repo"] + def g_bug_label(self) -> int: + return self.config["gitea_bug_label"] + + def g_feature_label(self) -> int: + return self.config["gitea_feature_label"] + + def g_bug_room(self) -> str: + return self.config["bug_issue_room"] + + def g_feature_room(self) -> str: + return self.config["feature_issue_room"] + def g_about(self) -> str: return self.config["about_text"] + + def g_bug_text(self) -> str: + return self.config["issue_bug_text"] + + def g_feature_text(self) -> str: + return self.config["issue_feature_text"] + + def g_disclaimer(self) -> str: + return self.config["issue_disclaimer_text"] def g_c_self_error(self) -> str: return self.config["command_pm_error"] @@ -55,6 +86,12 @@ class DevopsBot(Plugin): def g_c_self_success(self) -> str: return self.config["command_pm_success"] + + def g_c_new_help(self) -> str: + return self.config["command_new_help"] + + def g_c_command_success(self) -> str: + return self.config["command_success"] # Match all the registered pm rooms in pm_rooms with # the username and return the room id @@ -72,7 +109,7 @@ class DevopsBot(Plugin): if state not in ['open', 'closed', 'all']: raise ValueError("State må være enten 'open', 'closed', eller 'all") - params = {'state': state} + params = {'state': state, 'limit': 500} query_string = urlencode(params) url = f"https://{self.g_instance()}/api/v1/repos/{self.g_owner()}/{self.g_repo()}/issues?{query_string}" headers = { @@ -84,9 +121,117 @@ class DevopsBot(Plugin): async with session.get(url, headers=headers) as response: response.raise_for_status() return await response.json() + + # Post an issue to the configured repository + async def post_issue(self, label: int, title: str, body: str) -> list: + url = f"https://{self.g_instance()}/api/v1/repos/{self.g_owner()}/{self.g_repo()}/issues" + headers = { + "Authorization": f"token {self.g_token()}", + "Accept": "application/json", + "Content-Type": "application/json" + } + + issue_data = { + "title": title, + "body": body, + "labels": [ + label + ] + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, json=issue_data) as response: + response.raise_for_status() + return await response.json() + + @command.new(name="ny", aliases=["new"]) + @command.argument("args", pass_raw=True, required=False) + async def ny(self, evt: MessageEvent, args: 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 + + try: + arg_list = shlex.split(args) + except ValueError: + await evt.reply(self.g_c_new_help().format(error="Feil i formatering: Forsikre deg om at du bruker gåseøyne separat for tittel og beskrivelse")) + return + + if len(arg_list) < 3: + await evt.reply(self.g_c_new_help().format(error="Feil i formatering. Ikke nok argumenter. Bruk !ny [type] \"[Tittel]\" \"[Beskrivelse]\"")) + return + if len(arg_list) > 3: + await evt.reply(self.g_c_new_help().format(error="Feil i formatering. For mange argumenter. Bruk !ny [type] \"[Tittel]\" \"[Beskrivelse]\"")) + return + + issue_type, title, description = arg_list + + if not title.strip() or not description.strip(): + await evt.reply(self.g_c_new_help().format(error="Feil i formatering. Tittel og/eller beskrivelse kan ikke være tomme verdier")) + return + + if len(title) > 250: + await evt.reply(self.g_c_new_help().format(error="Feil i formatering. Tittelen kan ikke overskride 250 tegn.")) + return + + if issue_type == 'bug': + label = self.g_bug_label() + elif issue_type == 'forbedring': + label = self.g_feature_label() + else: + await evt.reply(f"Noe gikk feil med parsingen av argumentet 'type' (Mottok '{issue_type}' forventet 'bug' eller 'forbedring')") + return + + # Append information about the sender to the description text before posting it + description_without_signature = description # store description in a new variable for use on matrix + + description += f"\n\n--\n" + description += self.g_disclaimer().format( + nick=self.client.parse_user_id(evt.sender)[0], + userId=evt.sender + ) + + # Create issue on the git repository, and make a thread on this matrix server + + try: + new_issue = await self.post_issue(label=label, title=title, body=description) + if issue_type == 'bug': + await self.client.send_notice(self.g_bug_room(), html=self.g_bug_text().format( + user=self.client.parse_user_id(evt.sender)[0], + userId=evt.sender, + title=title, + description=description_without_signature, + link=new_issue["html_url"], + casenr=new_issue["number"] + )) + else: + await self.client.send_notice(self.g_feature_room(), html=self.g_feature_text().format( + user=self.client.parse_user_id(evt.sender)[0], + userId=evt.sender, + title=title, + description=description_without_signature, + link=new_issue["html_url"], + casenr=new_issue["number"] + )) + + await evt.reply(self.g_c_command_success().format( + result=f"Sak med saksnr {new_issue['number']} ble opprettet. Bruk !sak for å se detaljer" + )) + 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="saker", aliases=["cases"]) - @command.argument("status", pass_raw=True, required=False) + @command.argument("status", required=False) async def saker(self, evt: MessageEvent, status: str) -> None: # We want to see the commands ran in a open room, so # we can get a sense of it's usage @@ -108,16 +253,14 @@ class DevopsBot(Plugin): await evt.reply(f"Ukjent status {status}. Status kan enten være 'åpen', 'lukket', eller 'alle'") return - output = """ -|Saksnr | Tittel | Type(r) | -| :--- | :----: | ---: | -""" + output = "" for issue in issues: - output += f"\n| {issue['number']} | {issue['title']} | " - for label in issue['labels']: - output += f"{label['name']} " - output += '|' + output += f"" + + output += "
SaksnrTittelType(r)
{issue['number']}{issue['title']}" + labels = " ".join(label['name'] for label in issue['labels']) + output += f"{labels}
" # Check if the bot already has a PM session with the user # so we don't stack up invites to new rooms for each @@ -125,11 +268,11 @@ class DevopsBot(Plugin): pm_room = await self.has_pm_room(evt.sender) if pm_room is not 'NONE': - await self.client.send_text(pm_room, output) + await self.client.send_text(pm_room, html=output) 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, output) + await self.client.send_text(pm_room, html=output) await evt.reply(self.g_c_self_success()) diff --git a/maubot.yaml b/maubot.yaml index 93504af..14cf361 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,6 +1,6 @@ maubot: 0.1.0 id: no.arcticsoftware.devopsbot -version: 0.1.23 +version: 0.1.34 license: AGPL-3.0-or-later modules: - devops_bot diff --git a/no.arcticsoftware.devopsbot-v0.1.34.mbp b/no.arcticsoftware.devopsbot-v0.1.34.mbp new file mode 100644 index 0000000000000000000000000000000000000000..16d2a83ee7647007eba4d0e58bd391a323bfc28f GIT binary patch literal 17613 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 P#7Fq=7x?_n6T138sJtq& literal 0 HcmV?d00001