From 519cc9815b112d6598410f913971bd6f718a6dff Mon Sep 17 00:00:00 2001 From: Ginger <75683114+gingerindustries@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:07:19 +0000 Subject: [PATCH] Blacken everything --- vyxalbot2/__init__.py | 26 +++++-- vyxalbot2/__main__.py | 2 +- vyxalbot2/commands/__init__.py | 20 +++-- vyxalbot2/commands/common.py | 33 ++++++-- vyxalbot2/commands/discord.py | 2 +- vyxalbot2/commands/se.py | 124 +++++++++++++++++++++--------- vyxalbot2/github/__init__.py | 48 +++++++++--- vyxalbot2/github/formatters.py | 4 +- vyxalbot2/reactions.py | 24 ++++-- vyxalbot2/services/__init__.py | 5 +- vyxalbot2/services/discord.py | 64 +++++++++------ vyxalbot2/services/se/__init__.py | 52 ++++++++++--- vyxalbot2/services/se/parser.py | 25 ++++-- vyxalbot2/types.py | 11 ++- vyxalbot2/userdb.py | 23 ++++-- vyxalbot2/util.py | 25 +++--- 16 files changed, 360 insertions(+), 128 deletions(-) diff --git a/vyxalbot2/__init__.py b/vyxalbot2/__init__.py index ab6b35f..9dbe5a3 100644 --- a/vyxalbot2/__init__.py +++ b/vyxalbot2/__init__.py @@ -17,7 +17,12 @@ from vyxalbot2.services.discord import DiscordService from vyxalbot2.services.se import SEService from vyxalbot2.userdb import UserDB -from vyxalbot2.types import CommonData, PublicConfigType, PrivateConfigType, MessagesType +from vyxalbot2.types import ( + CommonData, + PublicConfigType, + PrivateConfigType, + MessagesType, +) __version__ = "2.0.0" @@ -42,9 +47,18 @@ def __init__( self.privkey = f.read() async def run(self): - userDB = UserDB(AsyncIOMotorClient(self.privateConfig["mongoUrl"]), self.privateConfig["database"]) + userDB = UserDB( + AsyncIOMotorClient(self.privateConfig["mongoUrl"]), + self.privateConfig["database"], + ) - ghApp = GitHubApplication(self.publicConfig, self.privkey, self.privateConfig["appID"], self.privateConfig["account"], self.privateConfig["webhookSecret"]) + ghApp = GitHubApplication( + self.publicConfig, + self.privkey, + self.privateConfig["appID"], + self.privateConfig["account"], + self.privateConfig["webhookSecret"], + ) reactions = Reactions(self.messages, self.privateConfig["chat"]["ignore"]) common = CommonData( @@ -55,7 +69,7 @@ async def run(self): 0, datetime.now(), userDB, - ghApp + ghApp, ) self.se = await SEService.create(reactions, common) self.discord = await DiscordService.create(reactions, common) @@ -68,7 +82,7 @@ async def run(self): async def shutdown(self, _): await self.se.shutdown() await self.discord.shutdown() - + def run(): PUBLIC_CONFIG_PATH = os.environ.get("VYXALBOT_CONFIG_PUBLIC", "config.json") @@ -96,4 +110,4 @@ def run(): STORAGE_PATH, statuses, ) - run_app(app.run(), port=privateConfig["port"]) \ No newline at end of file + run_app(app.run(), port=privateConfig["port"]) diff --git a/vyxalbot2/__main__.py b/vyxalbot2/__main__.py index ae3de68..8a3cb09 100644 --- a/vyxalbot2/__main__.py +++ b/vyxalbot2/__main__.py @@ -1,3 +1,3 @@ from vyxalbot2 import run -run() \ No newline at end of file +run() diff --git a/vyxalbot2/commands/__init__.py b/vyxalbot2/commands/__init__.py index ee73ea2..c6531de 100644 --- a/vyxalbot2/commands/__init__.py +++ b/vyxalbot2/commands/__init__.py @@ -8,8 +8,11 @@ from vyxalbot2.types import EventInfo + class Command(dict[str, Self]): - def __init__(self, name: str, doc: str, impl: Callable[..., AsyncGenerator[Any, None]]): + def __init__( + self, name: str, doc: str, impl: Callable[..., AsyncGenerator[Any, None]] + ): super().__init__() self.name = name self.helpStr = doc @@ -35,7 +38,10 @@ def fullHelp(self): parameters.append(f"[{parameter.name}: {typeString}]") else: parameters.append(f"<{parameter.name}: {typeString}>") - return (f"`!!/{self.name} " + " ".join(parameters)).strip() + "`: " + self.helpStr + return ( + (f"`!!/{self.name} " + " ".join(parameters)).strip() + "`: " + self.helpStr + ) + class CommandSupplier: def __init__(self): @@ -56,7 +62,11 @@ def genCommands(self): doc = "…" else: doc = attr.__doc__ - name = re.sub(r"([A-Z])", lambda match: " " + match.group(0).lower(), attr.__name__.removesuffix("Command")) + name = re.sub( + r"([A-Z])", + lambda match: " " + match.group(0).lower(), + attr.__name__.removesuffix("Command"), + ) commands[name] = Command(name, doc, attr) - - return commands \ No newline at end of file + + return commands diff --git a/vyxalbot2/commands/common.py b/vyxalbot2/commands/common.py index 63c7c75..47a762a 100644 --- a/vyxalbot2/commands/common.py +++ b/vyxalbot2/commands/common.py @@ -13,6 +13,7 @@ from vyxalbot2.types import CommonData, EventInfo from vyxalbot2.util import RAPTOR + class StatusMood(Enum): MESSAGE = "message" BORING = "boring" @@ -22,6 +23,7 @@ class StatusMood(Enum): CRYPTIC = "cryptic" GOOFY = "goofy" + class CommonCommands(CommandSupplier): def __init__(self, common: CommonData): super().__init__() @@ -41,7 +43,9 @@ def status(self): f"Errors since startup: {self.common.errorsSinceStartup}" ) - async def statusCommand(self, event: EventInfo, mood: StatusMood = StatusMood.MESSAGE): + async def statusCommand( + self, event: EventInfo, mood: StatusMood = StatusMood.MESSAGE + ): """I will tell you what I'm doing (maybe).""" match mood: case StatusMood.MESSAGE: @@ -54,21 +58,33 @@ async def statusCommand(self, event: EventInfo, mood: StatusMood = StatusMood.ME case StatusMood.BORING: yield self.status() case StatusMood.EXCITING: - yield "\n".join(map(lambda line: line + ("!" * random.randint(2, 5)), self.status().upper().splitlines())) + yield "\n".join( + map( + lambda line: line + ("!" * random.randint(2, 5)), + self.status().upper().splitlines(), + ) + ) case StatusMood.TINGLY: uwu = uwuipy(None, 0.3, 0.2, 0.2, 1) # type: ignore Me when the developers of uwuipy don't annotate their types correctly yield uwu.uwuify(self.status()) case StatusMood.SLEEPY: status = self.status() yield ( - "\n".join(status.splitlines())[:random.randint(1, len(status.splitlines()))] + "\n".join(status.splitlines())[ + : random.randint(1, len(status.splitlines())) + ] + " *yawn*\n" + "z" * random.randint(5, 10) ) case StatusMood.CRYPTIC: yield codecs.encode(self.status(), "rot13") case StatusMood.GOOFY: - yield "\n".join(map(lambda line: line + "🤓" * random.randint(1, 3), self.status().splitlines())) + yield "\n".join( + map( + lambda line: line + "🤓" * random.randint(1, 3), + self.status().splitlines(), + ) + ) async def coffeeCommand(self, event: EventInfo, target: str = "me"): """Brew some coffee.""" @@ -112,13 +128,16 @@ async def cookieCommand(self, event: EventInfo): async def deliterateifyCommand(self, event: EventInfo, code: str): """Convert literate code to sbcs""" async with ClientSession() as session: - async with session.post(self.common.privateConfig["tyxalInstance"] + "/deliterateify", data=code) as response: + async with session.post( + self.common.privateConfig["tyxalInstance"] + "/deliterateify", data=code + ) as response: if response.status == 400: yield f"Failed to deliterateify: {await response.text()}" elif response.status == 200: yield f"`{await response.text()}`" else: yield f"Tyxal sent back an error response! ({response.status})" + # Add an alias async def delitCommand(self, event: EventInfo, code: str): async for line in self.deliterateifyCommand(event, code): @@ -134,7 +153,9 @@ async def pullCommand(self, event: EventInfo): async def commitCommand(self, event: EventInfo): """Check the commit the bot is running off of""" - result = subprocess.run(["git", "show", "--oneline", "-s", "--no-color"], capture_output=True) + result = subprocess.run( + ["git", "show", "--oneline", "-s", "--no-color"], capture_output=True + ) if result.returncode != 0: yield "Failed to get commit info!" else: diff --git a/vyxalbot2/commands/discord.py b/vyxalbot2/commands/discord.py index 2180704..5180652 100644 --- a/vyxalbot2/commands/discord.py +++ b/vyxalbot2/commands/discord.py @@ -4,4 +4,4 @@ class DiscordCommands(CommonCommands): def __init__(self, common: CommonData): - super().__init__(common) \ No newline at end of file + super().__init__(common) diff --git a/vyxalbot2/commands/se.py b/vyxalbot2/commands/se.py index 4ee908e..77a05a4 100644 --- a/vyxalbot2/commands/se.py +++ b/vyxalbot2/commands/se.py @@ -14,7 +14,14 @@ from vyxalbot2.commands.common import CommonCommands from vyxalbot2.types import CommonData, EventInfo from vyxalbot2.userdb import User -from vyxalbot2.util import TRASH, extractMessageIdent, getMessageRange, getRoomOfMessage, resolveChatPFP +from vyxalbot2.util import ( + TRASH, + extractMessageIdent, + getMessageRange, + getRoomOfMessage, + resolveChatPFP, +) + class SECommands(CommonCommands): def __init__(self, room: Room, common: CommonData, service: "SEService"): @@ -49,9 +56,20 @@ async def helpCommand(self, event: EventInfo, command: str = ""): else: yield "No help is available for that command." else: - yield self.common.messages["help"] + ", ".join(sorted(set(map(lambda i: i.split(" ")[0], event.service.commands.commands.keys())))) + yield self.common.messages["help"] + ", ".join( + sorted( + set( + map( + lambda i: i.split(" ")[0], + event.service.commands.commands.keys(), + ) + ) + ) + ) - async def getPermissionsTarget(self, event: EventInfo, name: str) -> Union[User, str]: + async def getPermissionsTarget( + self, event: EventInfo, name: str + ) -> Union[User, str]: if name == "me": target = await self.userDB.getUser(self.service, event.userIdent) if target is None: @@ -69,7 +87,9 @@ async def permissionsListCommand(self, event: EventInfo, name: str): return yield f"User {target.name} is a member of groups {', '.join(target.groups)}." - async def permissionsModify(self, event: EventInfo, name: str, group: str, grant: bool): + async def permissionsModify( + self, event: EventInfo, name: str, group: str, grant: bool + ): if isinstance(target := (await self.getPermissionsTarget(event, name)), str): yield target return @@ -83,7 +103,9 @@ async def permissionsModify(self, event: EventInfo, name: str, group: str, grant except KeyError: yield "That group does not exist." return - if (not any([i in promotionRequires for i in sender.groups])) and len(promotionRequires): + if (not any([i in promotionRequires for i in sender.groups])) and len( + promotionRequires + ): yield "Insufficient permissions." return if grant: @@ -92,7 +114,9 @@ async def permissionsModify(self, event: EventInfo, name: str, group: str, grant else: target.groups.append(group) else: - if target.serviceIdent in self.groups[group].get("protected", {}).get(self.service.name, []): + if target.serviceIdent in self.groups[group].get("protected", {}).get( + self.service.name, [] + ): yield "That user may not be removed." elif group not in target.groups: yield f"That user is not in {group}." @@ -105,6 +129,7 @@ async def permissionsGrantCommand(self, event: EventInfo, name: str, group: str) """Add a user to a group.""" async for line in self.permissionsModify(event, name, group, True): yield line + async def permissionsRevokeCommand(self, event: EventInfo, name: str, group: str): """Remove a user from a group.""" async for line in self.permissionsModify(event, name, group, False): @@ -124,7 +149,7 @@ async def registerCommand(self, event: EventInfo): self.service, thumb["id"], thumb["name"], - resolveChatPFP(thumb["email_hash"]) + resolveChatPFP(thumb["email_hash"]), ) yield "You have been registered! You don't have any permisssions yet." @@ -147,21 +172,32 @@ async def refreshCommand(self, event: EventInfo): async def groupsListCommand(self, event: EventInfo): """List all groups known to the bot.""" yield "All groups: " + ", ".join(self.groups.keys()) + async def groupsMembersCommand(self, event: EventInfo, group: str): """List all members of a group.""" group = group.removesuffix("s") - yield f"Members of {group}: " + ', '.join(map(lambda i: i.name, await self.userDB.membersOfGroup(self.service, group))) + yield f"Members of {group}: " + ", ".join( + map(lambda i: i.name, await self.userDB.membersOfGroup(self.service, group)) + ) async def pingCommand(self, event: EventInfo, group: str, message: str): """Ping all members of a group. Use with care!""" group = group.removesuffix("s") - pings = " ".join(["@" + target.name for target in await self.userDB.membersOfGroup(self.service, group) if target.serviceIdent != event.userIdent]) + pings = " ".join( + [ + "@" + target.name + for target in await self.userDB.membersOfGroup(self.service, group) + if target.serviceIdent != event.userIdent + ] + ) if not len(pings): yield "Nobody to ping." else: yield pings + " ^" - async def issueOpenCommand(self, event: EventInfo, repo: str, title: str, body: str, tags: list[str] = []): + async def issueOpenCommand( + self, event: EventInfo, repo: str, title: str, body: str, tags: list[str] = [] + ): """Open an issue in a repository.""" tagSet = set(tags) if repo in self.common.publicConfig["requiredLabels"]: @@ -170,43 +206,45 @@ async def issueOpenCommand(self, event: EventInfo, repo: str, title: str, body: labelSet = set(rule["tags"]) if rule["exclusive"]: if len(labelSet.intersection(tagSet)) != 1: - yield f"Must be tagged with exactly one of " + ", ".join(f"`{i}`" for i in labelSet) + yield f"Must be tagged with exactly one of " + ", ".join( + f"`{i}`" for i in labelSet + ) return else: if len(labelSet.intersection(tagSet)) < 1: - yield f"Must be tagged with one or more of " + ", ".join(f"`{i}`" for i in labelSet) + yield f"Must be tagged with one or more of " + ", ".join( + f"`{i}`" for i in labelSet + ) return body = body + ( f"\n\n_Issue created by {event.userName} [here]" - f'(https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent})' + f"(https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent})" "_" ) try: await self.common.ghClient.gh.post( f"/repos/{self.common.privateConfig['account']}/{repo}/issues", - data={ - "title": title, - "body": body, - "labels": tags - }, - oauth_token = await self.common.ghClient.appToken() + data={"title": title, "body": body, "labels": tags}, + oauth_token=await self.common.ghClient.appToken(), ) except BadRequest as e: yield f"Failed to open issue: {e.args}" - async def issueCloseCommand(self, event: EventInfo, repo: str, num: int, body: str=""): + async def issueCloseCommand( + self, event: EventInfo, repo: str, num: int, body: str = "" + ): """Close an issue in a repository.""" if body: body = body + ( f"\n\n_Issue closed by {event.userName} [here]" - f'(https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent})' + f"(https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent})" "_" ) try: await self.common.ghClient.gh.post( f"/repos/{self.common.privateConfig['account']}/{repo}/issues/{num}/comments", data={"body": body}, - oauth_token = await self.common.ghClient.appToken() + oauth_token=await self.common.ghClient.appToken(), ) except BadRequest as e: yield f"Failed to send comment: {e.args}" @@ -214,7 +252,7 @@ async def issueCloseCommand(self, event: EventInfo, repo: str, num: int, body: s await self.common.ghClient.gh.patch( f"/repos/{self.common.privateConfig['account']}/{repo}/issues/{num}", data={"state": "closed"}, - oauth_token = await self.common.ghClient.appToken() + oauth_token=await self.common.ghClient.appToken(), ) except BadRequest as e: yield f"Failed to close issue: {e.args}" @@ -235,14 +273,21 @@ async def prodCommand(self, event: EventInfo, repo: str = ""): "base": self.common.publicConfig["production"][repo]["base"], "body": f"Requested by {event.userName} [here]({f'https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent})'}.", }, - oauth_token=await self.common.ghClient.appToken() + oauth_token=await self.common.ghClient.appToken(), ) except ValidationError as e: yield f"Unable to open PR: {e}" except GitHubHTTPException as e: yield f"Failed to create issue: {e.status_code.value} {e.status_code.description}", - async def idiomAddCommand(self, event: EventInfo, title: str, code: str, description: str, keywords: list[str] = []): + async def idiomAddCommand( + self, + event: EventInfo, + title: str, + code: str, + description: str, + keywords: list[str] = [], + ): """Add an idiom to the idiom list.""" file = await self.common.ghClient.gh.getitem( f"/repos/{self.common.privateConfig['account']}/vyxal.github.io/contents/src/data/idioms.yaml", @@ -258,9 +303,7 @@ async def idiomAddCommand(self, event: EventInfo, title: str, code: str, descrip "description": description, "link": "#" + base64.b64encode( - json.dumps(["", "", "", code, ""]).encode( - "utf-8" - ) + json.dumps(["", "", "", code, ""]).encode("utf-8") ).decode("utf-8"), "keywords": keywords, } @@ -270,16 +313,16 @@ async def idiomAddCommand(self, event: EventInfo, title: str, code: str, descrip data={ "message": f"Added \"{title}\" to the idiom list.\nRequested by {event.userName} here: {f'https://chat.stackexchange.com/transcript/{self.room.roomID}?m={event.messageIdent}#{event.messageIdent}'}", "content": base64.b64encode( - yaml.dump( - idioms, encoding="utf-8", allow_unicode=True - ) + yaml.dump(idioms, encoding="utf-8", allow_unicode=True) ).decode("utf-8"), "sha": file["sha"], }, oauth_token=await self.common.ghClient.appToken(), ) - async def trashCommand(self, event: EventInfo, startRaw: str, endRaw: str, target: int = TRASH): + async def trashCommand( + self, event: EventInfo, startRaw: str, endRaw: str, target: int = TRASH + ): """Move messages to a room (defaults to Trash).""" async with ClientSession() as session: start = extractMessageIdent(startRaw) @@ -291,13 +334,22 @@ async def trashCommand(self, event: EventInfo, startRaw: str, endRaw: str, targe yield "Malformed end id" return # Sanity check: make sure the messages are actually in our room - if (await getRoomOfMessage(session, start)) != self.common.privateConfig["chat"]["room"]: + if (await getRoomOfMessage(session, start)) != self.common.privateConfig[ + "chat" + ]["room"]: yield "Start message does not exist or is not in this room" return - if (await getRoomOfMessage(session, start)) != self.common.privateConfig["chat"]["room"]: + if (await getRoomOfMessage(session, start)) != self.common.privateConfig[ + "chat" + ]["room"]: yield "End message does not exist or is not in this room" return # Dubious code to figure out the range of messages we're dealing with - identRange = [i async for i in getMessageRange(session, self.common.privateConfig["chat"]["room"], start, end)] + identRange = [ + i + async for i in getMessageRange( + session, self.common.privateConfig["chat"]["room"], start, end + ) + ] await self.room.moveMessages(identRange, target) - yield f"Moved {len(identRange)} messages successfully." \ No newline at end of file + yield f"Moved {len(identRange)} messages successfully." diff --git a/vyxalbot2/github/__init__.py b/vyxalbot2/github/__init__.py index 12c3d4e..82510e1 100644 --- a/vyxalbot2/github/__init__.py +++ b/vyxalbot2/github/__init__.py @@ -16,11 +16,23 @@ from vyxalbot2.services import PinThat, Service from vyxalbot2.types import AppToken, PublicConfigType -from vyxalbot2.github.formatters import formatIssue, formatRef, formatRepo, formatUser, msgify +from vyxalbot2.github.formatters import ( + formatIssue, + formatRef, + formatRepo, + formatUser, + msgify, +) from vyxalbot2.util import GITHUB_MERGE_QUEUE + def wrap(fun): - async def wrapper(self: "GitHubApplication", event: GitHubEvent, services: list[Service], gh: AsyncioGitHubAPI): + async def wrapper( + self: "GitHubApplication", + event: GitHubEvent, + services: list[Service], + gh: AsyncioGitHubAPI, + ): lines = [i async for i in fun(self, event)] for service in services: ids = [] @@ -29,10 +41,19 @@ async def wrapper(self: "GitHubApplication", event: GitHubEvent, services: list[ await service.pin(ids[-1]) continue ids.append(await service.send(line, discordSuppressEmbeds=True)) + return wrapper + class GitHubApplication(Application): - def __init__(self, publicConfig: PublicConfigType, privkey: str, appId: str, account: str, webhookSecret: str): + def __init__( + self, + publicConfig: PublicConfigType, + privkey: str, + appId: str, + account: str, + webhookSecret: str, + ): super().__init__() self.services = [] self.privkey = privkey @@ -87,7 +108,8 @@ async def appToken(self) -> str: private_key=self.privkey, ) self._appToken = AppToken( - tokenData["token"], parseDatetime(tokenData["expires_at"], ignoretz=True) + tokenData["token"], + parseDatetime(tokenData["expires_at"], ignoretz=True), ) return self._appToken.token raise ValueError("Unable to locate installation") @@ -131,10 +153,14 @@ async def autoTagPR(self, event: GitHubEvent): return if len(pullRequest["labels"]): return - - autotagConfig = self.publicConfig["autotag"].get(event.data["repository"]["name"]) + + autotagConfig = self.publicConfig["autotag"].get( + event.data["repository"]["name"] + ) if autotagConfig is None: - autotagConfig = self.publicConfig["autotag"].get("*", {"prregex": {}, "issue2pr": {}}) + autotagConfig = self.publicConfig["autotag"].get( + "*", {"prregex": {}, "issue2pr": {}} + ) tags = set() for regex, tag in autotagConfig["prregex"].items(): if re.fullmatch(regex, pullRequest["head"]["ref"]) is not None: @@ -177,7 +203,7 @@ async def onPushAction(self, event: GitHubEvent): if len(commit["message"]) < 1: message = "(no title)" else: - message = commit['message'].splitlines()[0] + message = commit["message"].splitlines()[0] yield f"{user} {verb}ed a [commit]({commit['url']}) to {formatRef(branch, event.data['repository'])} in {formatRepo(event.data['repository'])}: {message}" else: counter = Counter() @@ -193,7 +219,7 @@ async def onPushAction(self, event: GitHubEvent): if len(userCommits[-1]["message"]) < 1: message = "(no title)" else: - message = userCommits[-1]['message'].splitlines()[0] + message = userCommits[-1]["message"].splitlines()[0] yield f"{user} {verb}ed {count} commits ([s]({userCommits[0]['url']}) [e]({userCommits[-1]['url']})) to {formatRef(branch, event.data['repository'])} in {formatRepo(event.data['repository'])}: {message}" @wrap @@ -261,7 +287,7 @@ async def onReleaseCreated(self, event: GitHubEvent): # attempt to match version number, otherwise default to the whole name if match := re.search(r"\d.*", releaseName): releaseName = match[0] - + yield f'__[{event.data["repository"]["name"]} {releaseName}]({release["html_url"]})__' if ( event.data["repository"]["name"] @@ -287,7 +313,7 @@ async def onReviewSubmitted(self, event: GitHubEvent): action = "requested changes on" case _: action = "did something to" - + yield ( f'{formatUser(event.data["sender"])} [{action}]({review["html_url"]}) {formatIssue(event.data["pull_request"])} in {formatRepo(event.data["repository"])}' + (': "' + msgify(review["body"]) + '"' if review["body"] else "") diff --git a/vyxalbot2/github/formatters.py b/vyxalbot2/github/formatters.py index 52797cb..27305de 100644 --- a/vyxalbot2/github/formatters.py +++ b/vyxalbot2/github/formatters.py @@ -8,9 +8,11 @@ def msgify(text): .replace("`", r"\`") ) + def linkify(text): return msgify(str(text)).replace("[", "\\[").replace("]", "\\]") + def formatUser(user: dict) -> str: return f'[{linkify(user["login"])}]({user["html_url"]})' @@ -24,4 +26,4 @@ def formatIssue(issue: dict) -> str: def formatRef(ref: str, repo: dict) -> str: - return f'[{repo["name"]}/{ref}]({repo["html_url"]}/tree/{ref})' \ No newline at end of file + return f'[{repo["name"]}/{ref}]({repo["html_url"]}/tree/{ref})' diff --git a/vyxalbot2/reactions.py b/vyxalbot2/reactions.py index 2658455..256d6ea 100644 --- a/vyxalbot2/reactions.py +++ b/vyxalbot2/reactions.py @@ -13,7 +13,10 @@ (r"(wh?[au]t( i[sz]|'s)? vyxal\??)", r"what vyxal i[sz]\??"): "info", (r"(!!/)?(pl(s|z|ease) )?make? meh? (a )?coo?kie?", r"cookie"): "cookie", (r"((please|pls|plz) )?(make|let|have) velociraptors maul (?P.+)",): "maul", - (r"(make?|brew)( a cup of|some)? coffee for (?P.+)", r"(make?|brew) (?Pme)h?( a)? coffee",): "coffee", + ( + r"(make?|brew)( a cup of|some)? coffee for (?P.+)", + r"(make?|brew) (?Pme)h?( a)? coffee", + ): "coffee", (r"(.* |^)(su+s(sy)?|amon?g ?us|suspicious)( .*|$)",): "sus", ( r"(.* |^)([Ww]ho(mst)?|[Ww]hat) (did|done) (that|this|it).*", @@ -23,13 +26,19 @@ r"(much |very |super |ultra |extremely )*(good|great|excellent|gaming) bot!*", ): "goodBot", (r"(hello|hey|hi|howdy|(good )?mornin['g]|(good )?evenin['g])( y'?all)?",): "hello", - (r"((good)?bye|adios|(c|see) ?ya\!?|'night|(good|night )night|\\o)( y'?all)?",): "goodbye", - (r".*mojo.*", ".*🔥+.*",): "mojo" + ( + r"((good)?bye|adios|(c|see) ?ya\!?|'night|(good|night )night|\\o)( y'?all)?", + ): "goodbye", + ( + r".*mojo.*", + ".*🔥+.*", + ): "mojo", } MESSAGE_REGEXES: dict[str, str] = dict( chain.from_iterable(zip(k, repeat(v)) for k, v in MESSAGE_REGEXES_IN.items()) ) + class Reactions: def __init__(self, messages: MessagesType, ignore: list[int]): self.messages = messages @@ -46,7 +55,10 @@ async def onMessage(self, service: Service, event: EventInfo): else: reMatch = re.fullmatch(regex, event.content.lower()) if reMatch is not None: - if event.userIdent == event.service.clientIdent and function not in OK_TO_SELF_REPLY: + if ( + event.userIdent == event.service.clientIdent + and function not in OK_TO_SELF_REPLY + ): continue if event.userIdent in self.ignore: continue @@ -88,7 +100,9 @@ async def goodbye(self, service: Service, event: EventInfo, reMatch: re.Match): async def mojo(self, service: Service, event: EventInfo, reMatch: re.Match): emojis = [ - "".join(random.choices(("🤣", "😂"), weights=[12, 8], k=random.randint(3, 7))), + "".join( + random.choices(("🤣", "😂"), weights=[12, 8], k=random.randint(3, 7)) + ), "💯" * random.choice((1, 3, 5)), "🔥" * random.randint(1, 10), ] diff --git a/vyxalbot2/services/__init__.py b/vyxalbot2/services/__init__.py index a8c4dd5..49c0471 100644 --- a/vyxalbot2/services/__init__.py +++ b/vyxalbot2/services/__init__.py @@ -9,11 +9,13 @@ PinThat = object() + class Service: messageSignal = Signal() editSignal = Signal() commandRequestSignal = Signal() commandResponseSignal = Signal() + @classmethod async def create(cls, reactions: "Reactions", common: "CommonData") -> Self: raise NotImplementedError @@ -25,6 +27,7 @@ def __init__(self, name: str, clientIdent: int, commands: "CommandSupplier"): async def startup(self): pass + async def shutdown(self): pass @@ -35,4 +38,4 @@ async def send(self, message: str, **kwargs) -> int: raise NotImplementedError async def pin(self, message: int): - raise NotImplementedError \ No newline at end of file + raise NotImplementedError diff --git a/vyxalbot2/services/discord.py b/vyxalbot2/services/discord.py index 120f901..2decaff 100644 --- a/vyxalbot2/services/discord.py +++ b/vyxalbot2/services/discord.py @@ -15,6 +15,7 @@ from vyxalbot2.reactions import Reactions from vyxalbot2.types import CommandImpl, CommonData, EventInfo + class VBClient(Client): def __init__(self, guild: int, statuses: list[str]): super().__init__(intents=Intents.all()) @@ -33,22 +34,26 @@ async def wrapper(interaction: Interaction, *args, **kwargs): assert interaction.channel_id is not None async for line in impl( EventInfo( - "", # :( + "", # :( interaction.user.display_name, interaction.user.display_avatar.url, interaction.user.id, interaction.channel_id, interaction.id, - service + service, ), - *args, **kwargs + *args, + **kwargs, ): await interaction.response.send_message(line) - + # 😰 wrapSig = inspect.signature(wrapper) wrapper.__signature__ = wrapSig.replace( - parameters=[wrapSig.parameters["interaction"], *tuple(inspect.signature(impl).parameters.values())[1:]] + parameters=[ + wrapSig.parameters["interaction"], + *tuple(inspect.signature(impl).parameters.values())[1:], + ] ) return wrapper @@ -60,8 +65,13 @@ def addCommand(self, service: "DiscordService", command: Command): part = parts.pop(0) parent = self.tree.get_command(part) if parent is None: - parent = Group(name=part, description="This seems to be a toplevel group of some kind.") - assert not isinstance(parent, DiscordCommand), "Cannot nest commands under commands" + parent = Group( + name=part, + description="This seems to be a toplevel group of some kind.", + ) + assert not isinstance( + parent, DiscordCommand + ), "Cannot nest commands under commands" while len(parts) > 1: part = parts.pop(0) newParent = parent.get_command(part) @@ -69,16 +79,21 @@ def addCommand(self, service: "DiscordService", command: Command): newParent = Group( name=part, parent=parent, - description="This seems to be a group of some kind." + description="This seems to be a group of some kind.", ) parent = newParent - assert not isinstance(parent, DiscordCommand), "Cannot nest commands under commands" - self.tree.add_command(DiscordCommand( - name=parts[0], - description=command.helpStr, - callback=self.wrap(service, command.impl), - parent=parent - )) + assert not isinstance( + parent, DiscordCommand + ), "Cannot nest commands under commands" + self.tree.add_command( + DiscordCommand( + name=parts[0], + description=command.helpStr, + callback=self.wrap(service, command.impl), + parent=parent, + ) + ) + async def setup_hook(self): self.tree.copy_global_to(guild=self.guild) self.updateStatus.start() @@ -101,7 +116,7 @@ async def create(cls, reactions: Reactions, common: CommonData): def __init__(self, client: VBClient, reactions: Reactions, common: CommonData): assert client.user is not None, "Need to be logged in to Discord!" super().__init__("discord", client.user.id, DiscordCommands(common)) - + self.logger = logging.getLogger("DiscordService") self.client = client self.client.event(self.on_message) @@ -110,12 +125,14 @@ def __init__(self, client: VBClient, reactions: Reactions, common: CommonData): for command in self.commands.commands.values(): self.client.addCommand(self, command) - + async def startup(self): self.clientTask = get_event_loop().create_task(self.client.connect()) await self.client.wait_until_ready() await self.client.tree.sync() - eventChannel = self.client.get_channel(self.common.privateConfig["discord"]["eventChannel"]) + eventChannel = self.client.get_channel( + self.common.privateConfig["discord"]["eventChannel"] + ) assert isinstance(eventChannel, TextChannel), str(eventChannel) self.eventChannel = eventChannel self.logger.info(f"Discord connection established! We are {self.client.user}.") @@ -130,7 +147,7 @@ async def on_message(self, message: Message): roomIdent=message.channel.id, userIdent=message.author.id, messageIdent=message.id, - service=self + service=self, ) event.content = re.sub(r"<:(\w+):(\d+)>", lambda m: m.group(1), event.content) for embed in message.embeds: @@ -149,13 +166,16 @@ async def on_message(self, message: Message): return await self.messageSignal.send_async(self, event=event) - async def shutdown(self): self.clientTask.cancel() await self.clientTask async def send(self, message: str, **kwargs): - return (await self.eventChannel.send(message, suppress_embeds=kwargs.get("discordSuppressEmbeds", False))).id + return ( + await self.eventChannel.send( + message, suppress_embeds=kwargs.get("discordSuppressEmbeds", False) + ) + ).id async def pin(self, message: int): - await self.eventChannel.get_partial_message(message).pin() \ No newline at end of file + await self.eventChannel.get_partial_message(message).pin() diff --git a/vyxalbot2/services/se/__init__.py b/vyxalbot2/services/se/__init__.py index 67c14e9..236c486 100644 --- a/vyxalbot2/services/se/__init__.py +++ b/vyxalbot2/services/se/__init__.py @@ -19,6 +19,7 @@ from vyxalbot2.types import CommonData, EventInfo from vyxalbot2.util import resolveChatPFP + class SEService(Service): @classmethod async def create(cls, reactions: Reactions, common: CommonData): @@ -74,7 +75,9 @@ async def getPFP(self, user: int): async with session.get( f"https://chat.stackexchange.com/users/thumbs/{user}" ) as response: - self.pfpCache[user] = resolveChatPFP((await response.json())["email_hash"]) + self.pfpCache[user] = resolveChatPFP( + (await response.json())["email_hash"] + ) return self.pfpCache[user] def preprocessMessage(self, message: str): @@ -84,9 +87,20 @@ def preprocessMessage(self, message: str): continue url = urlparse(tag.attrs["href"]) if not url.netloc: - tag.attrs["href"] = urlunparse(("https", "chat.stackexchange.com", url.path, url.params, url.query, url.fragment)) + tag.attrs["href"] = urlunparse( + ( + "https", + "chat.stackexchange.com", + url.path, + url.params, + url.query, + url.fragment, + ) + ) elif not url.scheme: - tag.attrs["href"] = urlunparse(("https", url.netloc, url.path, url.params, url.query, url.fragment)) + tag.attrs["href"] = urlunparse( + ("https", url.netloc, url.path, url.params, url.query, url.fragment) + ) for tag in soup.find_all("img"): if not isinstance(tag, Tag): continue @@ -101,7 +115,7 @@ async def onMessage(self, room: Room, message: MessageEvent): userIdent=message.user_id, roomIdent=message.room_id, messageIdent=message.message_id, - service=self + service=self, ) if message.user_id == self.room.userID: return @@ -114,12 +128,19 @@ async def onMessage(self, room: Room, message: MessageEvent): await self.send(line) await self.commandResponseSignal.send_async(self, line=line) return - await self.messageSignal.send_async(self, event=event, directedAtUs=message.content.startswith("!!/")) + await self.messageSignal.send_async( + self, event=event, directedAtUs=message.content.startswith("!!/") + ) if not message.content.startswith("!!/"): return await self.commandRequestSignal.send_async(self, event=event) sentAt = datetime.now() - response = [i async for i in self.processMessage(message.content.removeprefix("!!/"), event)] + response = [ + i + async for i in self.processMessage( + message.content.removeprefix("!!/"), event + ) + ] if not len(response): return responseIDs = [await self.room.reply(message.message_id, response[0])] @@ -140,11 +161,13 @@ async def onEdit(self, room: Room, edit: EditEvent): userIdent=edit.user_id, roomIdent=edit.room_id, messageIdent=edit.message_id, - service=self + service=self, ) if edit.user_id == self.room.userID: return - await self.editSignal.send_async(self, event=event, directedAtUs=edit.content.startswith("!!/")) + await self.editSignal.send_async( + self, event=event, directedAtUs=edit.content.startswith("!!/") + ) if not edit.content.startswith("!!/"): return await self.commandRequestSignal.send_async(self, event=event) @@ -153,11 +176,16 @@ async def onEdit(self, room: Room, edit: EditEvent): await self.onMessage(room, edit) else: sentAt, idents = self.editDB[edit.message_id] - if (datetime.now() - sentAt).seconds > (60 * 2): # margin of error + if (datetime.now() - sentAt).seconds > (60 * 2): # margin of error with self.messageSignal.muted(), self.commandRequestSignal.muted(): await self.onMessage(room, edit) else: - response = [i async for i in self.processMessage(self.preprocessMessage(edit.content.removeprefix("!!/")), event)] + response = [ + i + async for i in self.processMessage( + self.preprocessMessage(edit.content.removeprefix("!!/")), event + ) + ] for line in response: await self.commandResponseSignal.send_async(line=line) if len(response): @@ -195,4 +223,6 @@ async def processMessage(self, message: str, event: EventInfo): yield l except Exception as e: yield f"@Ginger An exception occured whilst processing this message!" - self.logger.exception(f"An exception occured whilst processing message {event.messageIdent}:") \ No newline at end of file + self.logger.exception( + f"An exception occured whilst processing message {event.messageIdent}:" + ) diff --git a/vyxalbot2/services/se/parser.py b/vyxalbot2/services/se/parser.py index 64b2061..884c857 100644 --- a/vyxalbot2/services/se/parser.py +++ b/vyxalbot2/services/se/parser.py @@ -4,6 +4,7 @@ from vyxalbot2.commands import Command + class ParseState(Enum): TOPLEVEL = auto() FLAG = auto() @@ -11,6 +12,7 @@ class ParseState(Enum): NUMBER = auto() STRARRAY = auto() + class TokenType(Enum): FLAG = auto() STRING = auto() @@ -19,18 +21,21 @@ class TokenType(Enum): STRARRAY = auto() ERROR = auto() + TYPES_TO_TOKENS = { int: TokenType.INT, float: TokenType.FLOAT, str: TokenType.STRING, - list[str]: TokenType.STRARRAY + list[str]: TokenType.STRARRAY, } + class ParseError(Exception): def __init__(self, message: str): super().__init__() self.message = message + class CommandParser: def __init__(self, commands: dict[str, Command]): self.commands = commands @@ -135,7 +140,9 @@ def parseArgs(self, args: str): stack.append([]) break elif char == "]": - yield TokenType.STRARRAY, ["".join(i) for i in stack if len(i)] + yield TokenType.STRARRAY, [ + "".join(i) for i in stack if len(i) + ] stack.clear() state = ParseState.TOPLEVEL break @@ -165,7 +172,10 @@ def parseCommand(self, command: str): if command.startswith(commandName.split(" ")[0]): maybeYouMeant.append(command) if len(maybeYouMeant): - raise ParseError(f"Unknown command. Perhaps you forgot some quotes? Valid subcommands of {commandName.split(' ')[0]} are: " + ", ".join(maybeYouMeant)) + raise ParseError( + f"Unknown command. Perhaps you forgot some quotes? Valid subcommands of {commandName.split(' ')[0]} are: " + + ", ".join(maybeYouMeant) + ) raise ParseError("Unknown command.") from None argValues = [] for paramName, param in signature(impl).parameters.items(): @@ -186,14 +196,17 @@ def parseCommand(self, command: str): if argType == TokenType.ERROR: raise ParseError(str(argValue)) if argType != paramType: - raise ParseError(f"Expected {paramType.name} for {paramName} but got {argType.name}") + raise ParseError( + f"Expected {paramType.name} for {paramName} but got {argType.name}" + ) if argType == TokenType.FLAG: assert issubclass(param.annotation, Enum) try: argValues.append(param.annotation(argValue)) except ValueError: - raise ParseError(f"Invalid value for {paramName}! Expected one of: {', '.join(member.value for member in param.annotation)}") + raise ParseError( + f"Invalid value for {paramName}! Expected one of: {', '.join(member.value for member in param.annotation)}" + ) else: argValues.append(argValue) return commandName, impl, argValues - diff --git a/vyxalbot2/types.py b/vyxalbot2/types.py index 2da2232..3e56246 100644 --- a/vyxalbot2/types.py +++ b/vyxalbot2/types.py @@ -9,6 +9,7 @@ CommandImpl = Callable[..., AsyncGenerator[Any, None]] + class GroupType(TypedDict, total=False): promotionRequires: list[str] canRun: list[str] @@ -28,12 +29,14 @@ class ChatConfigType(TypedDict): password: str ignore: list[int] + class DiscordConfigType(TypedDict): token: str guild: int eventChannel: int bridgeChannel: int + class PrivateConfigType(TypedDict): port: int @@ -50,18 +53,22 @@ class PrivateConfigType(TypedDict): chat: ChatConfigType discord: DiscordConfigType + class AutotagType(TypedDict): issue2pr: dict[str, str] prregex: dict[str, str] + class RequiredLabelType(TypedDict): tags: list[str] exclusive: bool + class RequiredLabelsType(TypedDict): issues: list[RequiredLabelType] prs: list[RequiredLabelType] + class PublicConfigType(TypedDict): importantRepositories: list[str] ignoredRepositories: list[str] @@ -86,6 +93,7 @@ class AppToken: token: str expires: datetime + @dataclass class CommonData: statuses: list[str] @@ -97,6 +105,7 @@ class CommonData: userDB: "UserDB" ghClient: "GitHubApplication" + @dataclass class EventInfo: content: str @@ -105,4 +114,4 @@ class EventInfo: roomIdent: int userIdent: int messageIdent: int - service: "Service" \ No newline at end of file + service: "Service" diff --git a/vyxalbot2/userdb.py b/vyxalbot2/userdb.py index 98d95bf..713b360 100644 --- a/vyxalbot2/userdb.py +++ b/vyxalbot2/userdb.py @@ -5,6 +5,7 @@ from vyxalbot2.services import Service + class User(Model): service: str serviceIdent: int @@ -14,24 +15,32 @@ class User(Model): linked: dict[str, ObjectId] = {} bonusData: dict[str, str] = {} + class UserDB: userModify = Signal() + def __init__(self, client, database: str): self.engine = AIOEngine(client=client, database=database) async def getUser(self, service: Service, ident: int) -> Optional[User]: - return await self.engine.find_one(User, User.service == service.name, User.serviceIdent == ident) - + return await self.engine.find_one( + User, User.service == service.name, User.serviceIdent == ident + ) + async def getUsers(self, service: Service): return await self.engine.find(User, User.service == service.name) async def getUserByName(self, service: Service, name: str) -> Optional[User]: - return await self.engine.find_one(User, User.service == service.name, User.name == name) + return await self.engine.find_one( + User, User.service == service.name, User.name == name + ) async def createUser(self, service: Service, ident: int, name: str, pfp: str): if (await self.getUser(service, ident)) is not None: raise ValueError("User exists") - await self.save(User(service=service.name, serviceIdent=ident, name=name, pfp=pfp)) + await self.save( + User(service=service.name, serviceIdent=ident, name=name, pfp=pfp) + ) async def linkUser(self, one: User, other: User): one.linked[other.service] = other.id @@ -40,8 +49,10 @@ async def linkUser(self, one: User, other: User): await self.save(other) async def membersOfGroup(self, service: Service, group: str): - return await self.engine.find(User, User.service == service.name, {"groups": group}) + return await self.engine.find( + User, User.service == service.name, {"groups": group} + ) async def save(self, user: User): await self.engine.save(user) - await self.userModify.send_async(self) \ No newline at end of file + await self.userModify.send_async(self) diff --git a/vyxalbot2/util.py b/vyxalbot2/util.py index 9f824b3..cbd1d13 100644 --- a/vyxalbot2/util.py +++ b/vyxalbot2/util.py @@ -10,11 +10,14 @@ GITHUB_MERGE_QUEUE = "github-merge-queue[bot]" TRASH = 82806 -LINK_REGEX = r"https?://chat.stackexchange.com/transcript(/message)?/(?P\d+)(#.*)?" +LINK_REGEX = ( + r"https?://chat.stackexchange.com/transcript(/message)?/(?P\d+)(#.*)?" +) STACK_IMGUR = "i.stack.imgur.com" DEFAULT_PFP = "https://cdn-chat.sstatic.net/chat/img/anon.png" + def resolveChatPFP(pfp: str): if pfp.startswith("!"): pfp = pfp.removeprefix("!") @@ -24,6 +27,7 @@ def resolveChatPFP(pfp: str): return pfp return f"https://www.gravatar.com/avatar/{pfp}?s=256&d=identicon&r=PG" + def extractMessageIdent(ident: str): if ident.isdigit(): return int(ident) @@ -32,8 +36,11 @@ def extractMessageIdent(ident: str): else: return None + async def getRoomOfMessage(session: ClientSession, ident: int): - async with session.get(f"https://chat.stackexchange.com/transcript/message/{ident}") as response: + async with session.get( + f"https://chat.stackexchange.com/transcript/message/{ident}" + ) as response: if response.status != 200: return None # may the lord have mercy on my soul @@ -43,20 +50,20 @@ async def getRoomOfMessage(session: ClientSession, ident: int): assert isinstance(href := link.get("href"), str) return int(href.removeprefix("/").removesuffix("/").split("/")[1]) + async def getMessageRange(session: ClientSession, room: int, start: int, end: int): before = end yield end while True: - async with session.post(f"https://chat.stackexchange.com/chats/{room}/events", data={ - "before": str(before), - "mode": "Messages", - "msgCount": 500 - }) as response: - data = (await response.json()) + async with session.post( + f"https://chat.stackexchange.com/chats/{room}/events", + data={"before": str(before), "mode": "Messages", "msgCount": 500}, + ) as response: + data = await response.json() events = data["events"] idents: list[int] = [event["message_id"] for event in events] if start in idents: - for ident in reversed(idents[idents.index(start):]): + for ident in reversed(idents[idents.index(start) :]): yield ident break for ident in reversed(idents):