diff --git a/commandListener.py b/commandListener.py index 0fb782d..0c051cc 100644 --- a/commandListener.py +++ b/commandListener.py @@ -14,12 +14,13 @@ import messageFunctions as msgFnc from event import Event from eventDatabase import EventDatabase +from container import Container from operationbot import OperationBot from secret import ADMINS from secret import COMMAND_CHAR as CMD -class EventDateTime(Converter): +class ArgDateTime(Converter): async def convert(self, ctx: Context, arg: str) -> datetime: try: date = datetime.strptime(arg, '%Y-%m-%d') @@ -29,7 +30,7 @@ async def convert(self, ctx: Context, arg: str) -> datetime: return date.replace(hour=18, minute=45) -class EventDate(Converter): +class ArgDate(Converter): async def convert(self, ctx: Context, arg: str) -> date: try: _date = date.fromisoformat(arg) @@ -39,7 +40,7 @@ async def convert(self, ctx: Context, arg: str) -> date: return _date -class EventTime(Converter): +class ArgTime(Converter): async def convert(self, ctx: Context, arg: str) -> datetime: try: time = datetime.strptime(arg, '%H:%M') @@ -49,7 +50,7 @@ async def convert(self, ctx: Context, arg: str) -> datetime: return time -class EventMessage(Converter): +class ArgMessage(Converter): async def convert(self, ctx: Context, arg: str) -> Message: try: eventID = int(arg) @@ -60,7 +61,7 @@ async def convert(self, ctx: Context, arg: str) -> Message: event = EventDatabase.getEventByID(eventID) if event is None: raise BadArgument("No event found with ID {}".format(eventID)) - message = await msgFnc.getEventMessage(event, ctx.bot) + message = await event.container.getMessage(ctx.bot) if message is None: raise BadArgument("No message found with event ID {}" .format(eventID)) @@ -68,7 +69,7 @@ async def convert(self, ctx: Context, arg: str) -> Message: return message -class EventEvent(Converter): +class ArgEvent(Converter): async def convert(self, ctx: Context, arg: str) -> Event: try: eventID = int(arg) @@ -83,7 +84,7 @@ async def convert(self, ctx: Context, arg: str) -> Event: return event -class ArchivedEvent(Converter): +class ArgArchivedEvent(Converter): async def convert(self, ctx: Context, arg: str) -> Event: try: eventID = int(arg) @@ -91,7 +92,7 @@ async def convert(self, ctx: Context, arg: str) -> Event: raise BadArgument("Invalid message ID {}, needs to be an " "integer".format(arg)) - event = EventDatabase.getArchivedEventByID(eventID) + event = EventDatabase.getEventByID(eventID, archived=True) if event is None: raise BadArgument("No event found with ID {}".format(eventID)) @@ -182,16 +183,16 @@ async def _create_event(self, ctx: Context, date: datetime, # TODO: Check for duplicate event dates? # Create event and sort events, export event: Event = EventDatabase.createEvent(date, ctx.guild.emojis) - message = await msgFnc.createEventMessage(event, self.bot.eventchannel) + container: Container = await Container.send(event, self.bot.eventchannel) if not batch: - await msgFnc.updateReactions(event, message=message) + await container.updateReactions() await msgFnc.sortEventMessages(ctx) EventDatabase.toJson() # Update JSON file await ctx.send("Created event {} with id {}".format(event, event.id)) # Create event command @command() - async def create(self, ctx: Context, date: EventDateTime, force=None): + async def create(self, ctx: Context, date: ArgDateTime, force=None): """ Create a new event. @@ -211,8 +212,8 @@ async def create(self, ctx: Context, date: EventDateTime, force=None): await self._create_event(ctx, date) @command() - async def multicreate(self, ctx: Context, start: EventDate, - end: EventDate = None, force=None): + async def multicreate(self, ctx: Context, start: ArgDate, + end: ArgDate = None, force=None): """Create events for all weekends within specified range. If the end date is omitted, events are created for the rest of the @@ -298,14 +299,14 @@ def pred(m): self.bot.awaiting_reply = False @command() - async def addrole(self, ctx: Context, eventMessage: EventMessage, *, + async def addrole(self, ctx: Context, argMessage: ArgMessage, *, rolename: str): """ Add a new additional role to the event. Example: addrole 1 Y1 (Bradley) Driver """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -317,7 +318,7 @@ async def addrole(self, ctx: Context, eventMessage: EventMessage, *, "happen. Nag at {}".format(user.mention)) return try: - await eventMessage.add_reaction(reaction) + await argMessage.add_reaction(reaction) except Forbidden as e: if e.code == 30010: await ctx.send("Too many reactions, not adding role {}" @@ -325,21 +326,21 @@ async def addrole(self, ctx: Context, eventMessage: EventMessage, *, event.removeAdditionalRole(rolename) return - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Role {} added to event {}".format(rolename, event)) # Remove additional role from event command @command() - async def removerole(self, ctx: Context, eventMessage: EventMessage, *, + async def removerole(self, ctx: Context, argMessage: ArgMessage, *, rolename: str): """ Remove an additional role from the event. Example: removerole 1 Y1 (Bradley) Driver """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -351,23 +352,23 @@ async def removerole(self, ctx: Context, eventMessage: EventMessage, *, # Remove reactions, remove role, update event, add reactions, export for reaction in event.getReactionsOfGroup("Additional"): - await eventMessage.remove_reaction(reaction, self.bot.user) + await argMessage.remove_reaction(reaction, self.bot.user) event.removeAdditionalRole(rolename) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() for reaction in event.getReactionsOfGroup("Additional"): - await eventMessage.add_reaction(reaction) + await argMessage.add_reaction(reaction) EventDatabase.toJson() # Update JSON file await ctx.send("Role {} removed from {}".format(rolename, event)) @command() - async def removegroup(self, ctx: Context, eventMessage: EventMessage, *, + async def removegroup(self, ctx: Context, argMessage: ArgMessage, *, groupName: str): """ Remove a role group from the event. Example: removegroup 1 Bravo """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -378,22 +379,22 @@ async def removegroup(self, ctx: Context, eventMessage: EventMessage, *, # Remove reactions, remove role, update event, add reactions, export for reaction in event.getReactionsOfGroup(groupName): - await eventMessage.remove_reaction(reaction, self.bot.user) + await argMessage.remove_reaction(reaction, self.bot.user) event.removeRoleGroup(groupName) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Group {} removed from {}".format(groupName, event)) # Set title of event command @command() - async def settitle(self, ctx: Context, eventMessage: EventMessage, *, + async def settitle(self, ctx: Context, argMessage: ArgMessage, *, title: str): """ Set event title. Example: settitle 1 Operation Striker """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -401,21 +402,21 @@ async def settitle(self, ctx: Context, eventMessage: EventMessage, *, # NOTE: Does not check for too long input. Will result in an API error # and a bot crash event.setTitle(title) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Title {} set for operation ID {} at {}" .format(event.title, event.id, event.date)) # Set date of event command @command() - async def setdate(self, ctx: Context, eventMessage: EventMessage, - date: EventDateTime): + async def setdate(self, ctx: Context, argMessage: ArgMessage, + date: ArgDateTime): """ Set event date. Example: setdate 1 2019-01-01 """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -423,7 +424,7 @@ async def setdate(self, ctx: Context, eventMessage: EventMessage, event.setDate(date) # Update event and sort events, export - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() await msgFnc.sortEventMessages(ctx) EventDatabase.toJson() # Update JSON file await ctx.send("Date {} set for operation {} ID {}" @@ -431,14 +432,14 @@ async def setdate(self, ctx: Context, eventMessage: EventMessage, # Set time of event command @command() - async def settime(self, ctx: Context, eventMessage: EventMessage, - time: EventTime): + async def settime(self, ctx: Context, argMessage: ArgMessage, + time: ArgTime): """ Set event time. Example: settime 1 18:45 """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -446,7 +447,7 @@ async def settime(self, ctx: Context, eventMessage: EventMessage, event.setTime(time) # Update event and sort events, export - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() await msgFnc.sortEventMessages(ctx) EventDatabase.toJson() # Update JSON file await ctx.send("Time set for operation {}" @@ -454,67 +455,67 @@ async def settime(self, ctx: Context, eventMessage: EventMessage, # Set terrain of event command @command() - async def setterrain(self, ctx: Context, eventMessage: EventMessage, *, + async def setterrain(self, ctx: Context, argMessage: ArgMessage, *, terrain: str): """ Set event terrain. Example: settime 1 Takistan """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return # Change terrain, update event, export event.setTerrain(terrain) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Terrain {} set for operation {}" .format(event.terrain, event)) # Set faction of event command @command() - async def setfaction(self, ctx: Context, eventMessage: EventMessage, *, + async def setfaction(self, ctx: Context, argMessage: ArgMessage, *, faction: str): """ Set event faction. Example: setfaction 1 Insurgents """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return # Change faction, update event, export event.setFaction(faction) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Faction {} set for operation {}" .format(event.faction, event)) # Set faction of event command @command() - async def setdescription(self, ctx: Context, eventMessage: EventMessage, *, + async def setdescription(self, ctx: Context, argMessage: ArgMessage, *, description: str): """ Set event description. Example: setdescription 1 Extra mods required """ - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return # Change description, update event, export event.description = description - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file await ctx.send("Description \"{}\" set for operation {}" .format(event.description, event)) # Sign user up to event command @command() - async def signup(self, ctx: Context, eventMessage: EventMessage, + async def signup(self, ctx: Context, argMessage: ArgMessage, user: Member, *, roleName: str): """ Sign user up (manually). @@ -524,7 +525,7 @@ async def signup(self, ctx: Context, eventMessage: EventMessage, Example: signup 1 "S. Gehock" Y1 (Bradley) Gunner """ # NOQA - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return @@ -536,7 +537,7 @@ async def signup(self, ctx: Context, eventMessage: EventMessage, # Sign user up, update event, export event.signup(role, user) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file # TODO: handle users without separate nickname await ctx.send("User {} signed up to event {} as {}" @@ -544,7 +545,7 @@ async def signup(self, ctx: Context, eventMessage: EventMessage, # Remove signup on event of user command @command() - async def removesignup(self, ctx: Context, eventMessage: EventMessage, + async def removesignup(self, ctx: Context, argMessage: ArgMessage, user: Member): """ Undo user signup (manually). @@ -553,13 +554,13 @@ async def removesignup(self, ctx: Context, eventMessage: EventMessage, Example: removesignup 1 "S. Gehock" """ # NOQA - event = await msgFnc.getEvent(eventMessage.id, ctx) + event = await msgFnc.getEvent(argMessage.id, ctx) if event is None: return # Remove signup, update event, export event.undoSignup(user) - await msgFnc.updateMessageEmbed(eventMessage, event) + await event.container.updateEmbed() EventDatabase.toJson() # Update JSON file # TODO: handle users without separate nickname await ctx.send("User {} removed from event {}" @@ -567,7 +568,7 @@ async def removesignup(self, ctx: Context, eventMessage: EventMessage, # Archive event command @command() - async def archive(self, ctx: Context, event: EventEvent): + async def archive(self, ctx: Context, event: ArgEvent): """ Archive event. @@ -576,50 +577,34 @@ async def archive(self, ctx: Context, event: EventEvent): # Archive event and export EventDatabase.archiveEvent(event) - eventMessage = await msgFnc.getEventMessage(event, self.bot) - if eventMessage: - await eventMessage.delete() + container = await event.container.getMessage(self.bot) + if container: + await container.delete() else: await ctx.send("Internal error: event {} without a message found" .format(event)) # Create new message - await msgFnc.createEventMessage(event, self.bot.eventarchivechannel) + await Container.send(event, self.bot.eventarchivechannel) EventDatabase.toJson() # Update JSON file await ctx.send("Event {} archived".format(event)) # Delete event command @command() - async def delete(self, ctx: Context, event: EventEvent): + async def delete(self, ctx: Context, event: ArgEvent): """ Delete event. Example: delete 1 """ - eventMessage = await msgFnc.getEventMessage(event, self.bot) - EventDatabase.removeEvent(event.id) + message = event.container.message + EventDatabase.removeEvent(event) # TODO: handle missing events - await eventMessage.delete() + await message.delete() EventDatabase.toJson() await ctx.send("Event {} removed".format(event)) - @command() - async def deletearchived(self, ctx: Context, event: ArchivedEvent): - """ - Delete archived event. - - Example: deletearchived 1 - """ - eventMessage = await msgFnc.getEventMessage( - event, self.bot, archived=True) - EventDatabase.removeEvent(event.id, archived=True) - # TODO: handle missing events - # TODO: Check if archived message can be deleted - await eventMessage.delete() - EventDatabase.toJson() - await ctx.send("Event {} removed from archive".format(event)) - # sort events command @command() async def sort(self, ctx: Context): @@ -642,14 +627,6 @@ async def importJson(self, ctx: Context): await self.bot.import_database() await ctx.send("{} events imported".format(len(EventDatabase.events))) - # @command() - # async def createmessages(self, ctx: Context): - # """Import database and (re)create event messages.""" - # await self.bot.import_database() - # await msgFnc.createMessages(EventDatabase.events, self.bot) - # EventDatabase.toJson() - # await ctx.send("Event messages created") - @command() async def syncmessages(self, ctx: Context): """Import database, sync messages with events and create missing messages.""" @@ -686,7 +663,6 @@ async def shutdown(self, ctx: Context): @removesignup.error @archive.error @delete.error - @deletearchived.error @sort.error @export.error @importJson.error diff --git a/config.py b/config.py index 3528b57..fd7069b 100644 --- a/config.py +++ b/config.py @@ -42,34 +42,39 @@ "\N{REGIONAL INDICATOR SYMBOL LETTER J}", ] -DEFAULT_ROLES = { # NOTE: role name equals emote name - "ZEUS": "Battalion", - "MOD": "Battalion", - "CO": "Company", - "FAC": "Company", - "RTO": "Company", - "1PLT": "1st Platoon", - "ASL": "Alpha", - "A1": "Alpha", - "BSL": "Bravo", - "B1": "Bravo", - "CSL": "Charlie", - "C1": "Charlie", - "DSL": "Delta", - "D1": "Delta", - "2PLT": "2nd Platoon", - "ESL": "Echo", - "E1": "Echo", - "FSL": "Foxtrot", - "F1": "Foxtrot", - "GSL": "Golf", - "G1": "Golf", - "HSL": "Hotel", - "H1": "Hotel", +# NOTE: role name equals emote name +DEFAULT_ROLES = { + "Battalion": ["ZEUS", "MOD"], + "Company": ["CO", "FAC", "RTO"], + "Dummy": [], + "1st Platoon": ["1PLT", "FAC", "RTO"], + "Alpha": ["ASL", "A1"], + "Bravo": ["BSL", "B1"], + + "Charlie": ["CSL", "C1"], + "Delta": ["DSL", "D1"], + + "2nd Platoon": ["2PLT", "FAC", "RTO"], + "Echo": ["ESL", "E1"], + "Foxtrot": ["FSL", "F1"], + + "Golf": ["GSL", "G1"], + "Hotel": ["HSL", "H1"], } +# A list of groups to add to an event by default +DEFAULT_GROUPS = [ + # Dummy: an empty spacer. An embed can only have either one or three items + # on a line + "Battalion", "Company", "Dummy", + "1st Platoon", "Alpha", "Bravo", + "2nd Platoon", "Echo", "Foxtrot", +] + EMOJI_ZEUS = "ZEUS" +EMOJI_SIGNOFF = "\N{CROSS MARK}" + # If a user signs off from a role listed in SIGNOFF_NOTIFY_ROLES when # there is less than SIGNOFF_NOTIFY_TIME left until the operation start, # a user defined in secrets.py gets notified about that. diff --git a/container.py b/container.py new file mode 100644 index 0000000..527798f --- /dev/null +++ b/container.py @@ -0,0 +1,135 @@ +from typing import List, Optional + +from discord import Embed, Emoji, Message, NotFound, TextChannel + +import config as cfg +from eventDatabase import EventDatabase +from event import Event +import messageFunctions as msgFnc + + +class Container(): + def __init__(self, event: Event, channel: TextChannel, message: Message): + self.event = event + self.channel = channel + self.message = message + self.id = message.id + + def __lt__(self, other): + return self.id < other.id + + def __repr__(self): + return "".format(self) + + @classmethod + async def send(cls, event: Event, channel: TextChannel): + # TODO: Check if actually necessary to have content + message = await channel.send("\N{ZERO WIDTH SPACE}") + self = Container(event, channel, message) + + event.container = self + event.messageID = self.message.id + await self.updateEmbed() + return self + + # Return an embed for the event + def _createEmbed(self) -> Embed: + title = "{} ({})".format( + self.event.title, + self.event.date.strftime("%a %Y-%m-%d - %H:%M CEST")) + description = "Terrain: {} - Faction: {}\n\n{}".format( + self.event.terrain, self.event.faction, self.event.description) + eventEmbed = Embed(title=title, description=description, + colour=self.event.color) + + # Add field to embed for every rolegroup + for group in self.event.roleGroups.values(): + if len(group.roles) > 0: + eventEmbed.add_field(name=group.name, value=str(group), + inline=group.isInline) + elif group.name == "Dummy": + eventEmbed.add_field(name="\N{ZERO WIDTH SPACE}", + value="\N{ZERO WIDTH SPACE}", + inline=group.isInline) + + return eventEmbed + + # was: EventDatabase.updateEvent + async def updateEmbed(self) -> None: + """Update the embed and footer of a message.""" + embed = self._createEmbed() + embed.set_footer(text="Event ID: " + str(self.event.id)) + await self.message.edit(embed=embed) + + async def getMessage(self, bot) -> Message: + """Get a message related to an event.""" + if self.message is not None: + return self.message + else: + return msgFnc.getEventMessage(self.event, bot) + + # from EventDatabase + async def updateReactions(self, message: Message = None, bot=None): + """ + Update reactions of an event message. + + Requires either container.message to be set or the `bot` argument to + be provided. + """ + if message is None: + message = await self.getMessage(bot) + if message is None and bot is None: + raise ValueError("container.message not set. Requires the " + "`bot` argument to be provided") + + reactions: List[Emoji] = self.event.getReactions() + reactionEmojisIntended = [cfg.EMOJI_SIGNOFF] + reactions + reactionsCurrent = message.reactions + reactionEmojisCurrent = {} + reactionsToRemove = [] + reactionEmojisToAdd = [] + + # Find reaction emojis current + for reaction in reactionsCurrent: + reactionEmojisCurrent[reaction.emoji] = reaction + + # Find emojis to remove + for emoji, reaction in reactionEmojisCurrent.items(): + if emoji not in reactionEmojisIntended: + reactionsToRemove.append(reaction) + + # Find emojis to add + for emoji in reactionEmojisIntended: + if emoji not in reactionEmojisCurrent.keys(): + reactionEmojisToAdd.append(emoji) + + # Remove existing unintended reactions + for reaction in reactionsToRemove: + await message.clear_reaction(reaction) + + # Add not existing intended emojis + for emoji in reactionEmojisToAdd: + await message.add_reaction(emoji) + + def setEvent(self, event: Event): + self.event = event + self.event.container = self + self.event.messageID = self.event.container.id + + @staticmethod + def sortEvents(): + sortedEvents = [] + containers = [] + + # Store existing events + for event in EventDatabase.events.values(): + sortedEvents.append(event) + containers.append(event.container) + + # Sort events based on date and time + sortedEvents.sort(reverse=True) + containers.sort(reverse=True) + + # Fill events again + for event in sortedEvents: + containers.pop().setEvent(event) diff --git a/event.py b/event.py index e47b562..72c5f5d 100644 --- a/event.py +++ b/event.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from discord import Embed, Emoji @@ -27,57 +27,30 @@ def __init__(self, date: datetime, guildEmojis: Tuple[Emoji], self.roleGroups: Dict[str, RoleGroup] = {} self.additionalRoleCount = 0 self.messageID = 0 + self.container: Any = None self.id = eventID + self.archived = False self.normalEmojis = self._getNormalEmojis(guildEmojis) if not importing: self.addDefaultRoleGroups() self.addDefaultRoles() - # Return an embed for the event - def createEmbed(self) -> Embed: - title = "{} ({})".format( - self.title, self.date.strftime("%a %Y-%m-%d - %H:%M CEST")) - description = "Terrain: {} - Faction: {}\n\n{}".format( - self.terrain, self.faction, self.description) - eventEmbed = Embed(title=title, description=description, - colour=self.color) - - # Add field to embed for every rolegroup - for group in self.roleGroups.values(): - if len(group.roles) > 0: - eventEmbed.add_field(name=group.name, value=str(group), - inline=group.isInline) - elif group.name == "Dummy": - eventEmbed.add_field(name="\N{ZERO WIDTH SPACE}", - value="\N{ZERO WIDTH SPACE}", - inline=group.isInline) - - return eventEmbed - # Add default role groups def addDefaultRoleGroups(self): - self.roleGroups["Battalion"] = RoleGroup("Battalion") - self.roleGroups["Company"] = RoleGroup("Company") - # An empty spacer. An embed can only have either one or three items on - # a line - self.roleGroups["Dummy"] = RoleGroup("Dummy") - self.roleGroups["1st Platoon"] = RoleGroup("1st Platoon") - self.roleGroups["Alpha"] = RoleGroup("Alpha") - self.roleGroups["Bravo"] = RoleGroup("Bravo") - self.roleGroups["2nd Platoon"] = RoleGroup("2nd Platoon") - self.roleGroups["Echo"] = RoleGroup("Echo") - self.roleGroups["Foxtrot"] = RoleGroup("Foxtrot") + for group in cfg.DEFAULT_GROUPS: + self.roleGroups[group] = RoleGroup(group) self.roleGroups["Additional"] = RoleGroup("Additional", isInline=False) # Add default roles def addDefaultRoles(self): - for name, groupName in cfg.DEFAULT_ROLES.items(): + for groupName, roles in cfg.DEFAULT_ROLES.items(): # Only add role if the group exists if groupName in self.roleGroups.keys(): - emoji = self.normalEmojis[name] - newRole = Role(name, emoji, False) - self.roleGroups[groupName].addRole(newRole) + for role in roles: + emoji = self.normalEmojis[role] + newRole = Role(role, emoji, displayName=False) + self.roleGroups[groupName].addRole(newRole) # Add an additional role to the event def addAdditionalRole(self, name: str) -> str: @@ -86,7 +59,7 @@ def addAdditionalRole(self, name: str) -> str: emoji = cfg.ADDITIONAL_ROLE_EMOJIS[self.additionalRoleCount] # Create role - newRole = Role(name, emoji, True) + newRole = Role(name, emoji, displayName=True) # Add role to additional roles self.roleGroups["Additional"].addRole(newRole) @@ -142,8 +115,14 @@ def setFaction(self, newFaction): def _getNormalEmojis(self, guildEmojis) -> Dict[str, Emoji]: normalEmojis = {} + allRoles = [ + role + for roles in cfg.DEFAULT_ROLES.values() + for role in roles + ] + for emoji in guildEmojis: - if emoji.name in cfg.DEFAULT_ROLES: + if emoji.name in allRoles: normalEmojis[emoji.name] = emoji return normalEmojis @@ -228,6 +207,9 @@ def __repr__(self): return "".format( self.title, self.id, self.date) + def __lt__(self, other): + return self.date < other.date + def toJson(self): roleGroupsData = {} for groupName, roleGroup in self.roleGroups.items(): diff --git a/eventDatabase.py b/eventDatabase.py index e2f66e2..098b3b3 100644 --- a/eventDatabase.py +++ b/eventDatabase.py @@ -48,76 +48,52 @@ def archiveEvent(event: Event): Does not remove or create messages. """ # Remove event from events - EventDatabase.removeEvent(event.id) + EventDatabase.removeEvent(event) + + event.archived = True # Add event to eventsArchive EventDatabase.eventsArchive[event.id] = event @staticmethod - def removeEvent(eventID: int, archived=False) -> bool: + def removeEvent(event: Event) -> bool: """ Remove event. Does not remove the message associated with the event. """ - if archived: - if eventID in EventDatabase.eventsArchive.keys(): - del EventDatabase.eventsArchive[eventID] + if event.archived: + if event.id in EventDatabase.eventsArchive.keys(): + del EventDatabase.eventsArchive[event.id] return True else: - if eventID in EventDatabase.events.keys(): - del EventDatabase.events[eventID] + if event.id in EventDatabase.events.keys(): + del EventDatabase.events[event.id] return True return False # was: findEvent @staticmethod - def getEventByMessage(messageID: int) -> Optional[Event]: + def getEventByMessage(messageID: int, archived=False) -> Optional[Event]: """Find an event with it's message ID.""" event: Event - for event in EventDatabase.events.values(): - if event.messageID == messageID: - return event - return None - - @staticmethod - def getEventByID(eventID: int) -> Optional[Event]: - """Find an event with it's ID.""" - return EventDatabase.events.get(eventID) + if archived: + events = EventDatabase.eventsArchive + else: + events = EventDatabase.events - @staticmethod - def getArchivedEventByMessage(messageID: int) -> Optional[Event]: - # return EventDatabase.eventsArchive.get(messageID) - for event in EventDatabase.eventsArchive.values(): + for event in events.values(): if event.messageID == messageID: return event return None - # was: findEventInArchiveeventid - @staticmethod - def getArchivedEventByID(eventID: int): - return EventDatabase.eventsArchive.get(eventID) - @staticmethod - def sortEvents(): - sortedEvents = [] - messageIDs = [] - - # Store existing events - for event in EventDatabase.events.values(): - sortedEvents.append(event) - messageIDs.append(event.messageID) - - # Sort events based on date and time - sortedEvents.sort(key=lambda event: event.date, reverse=True) - messageIDs.sort(reverse=True) - - # Fill events again - EventDatabase.events: Dict[int, Event] = {} - for event in sortedEvents: - # event = sortedEvents[index] - event.messageID = messageIDs.pop() - EventDatabase.events[event.id] = event + def getEventByID(eventID: int, archived=False) -> Optional[Event]: + """Find an event with it's ID.""" + if archived: + return EventDatabase.eventsArchive.get(eventID) + else: + return EventDatabase.events.get(eventID) @staticmethod def toJson(): diff --git a/eventListener.py b/eventListener.py index 72a8b6c..97e8f2c 100644 --- a/eventListener.py +++ b/eventListener.py @@ -1,5 +1,6 @@ import importlib from datetime import datetime, timedelta +from typing import Optional from discord import Game, Member, Message, RawReactionActionEvent, Reaction from discord.ext.commands import Cog @@ -22,15 +23,14 @@ async def on_ready(self): print("Waiting until ready") await self.bot.wait_until_ready() self.bot.fetch_data() - commandchannel = self.bot.commandchannel - await commandchannel.send("Connected") + await self.bot.commandchannel.send("Connected") print("Ready, importing") - await commandchannel.send("Importing events") + await self.bot.commandchannel.send("Importing events") # await EventDatabase.fromJson(self.bot) await self.bot.import_database() - await commandchannel.send("syncing") + await self.bot.commandchannel.send("syncing") await msgFnc.syncMessages(EventDatabase.events, self.bot) - await commandchannel.send("synced") + await self.bot.commandchannel.send("synced") EventDatabase.toJson() # TODO: add conditional message creation # if debug: @@ -39,7 +39,7 @@ async def on_ready(self): # detect existing messages msg = "{} events imported".format(len(EventDatabase.events)) print(msg) - await commandchannel.send(msg) + await self.bot.commandchannel.send(msg) await self.bot.change_presence(activity=Game(name=cfg.GAME, type=2)) print('Logged in as', self.bot.user.name, self.bot.user.id) @@ -56,7 +56,7 @@ async def on_raw_reaction_add(self, payload: RawReactionActionEvent): await message.remove_reaction(payload.emoji, user) # Get event from database with message ID - event: Event = EventDatabase.getEventByMessage(message.id) + event: Optional[Event] = EventDatabase.getEventByMessage(message.id) if event is None: print("No event found with that id", message.id) await self.bot.logchannel.send( @@ -73,6 +73,8 @@ async def on_raw_reaction_add(self, payload: RawReactionActionEvent): else: emoji = payload.emoji.name + print("emoji", emoji) + print(emoji == cfg.EMOJI_SIGNOFF) # Find signup of user signup: Role = event.findSignupRole(user.id) @@ -101,7 +103,7 @@ async def on_raw_reaction_add(self, payload: RawReactionActionEvent): event.signup(role, user) # Update event - await msgFnc.updateMessageEmbed(message, event) + await event.container.updateEmbed() EventDatabase.toJson() else: # Role is already taken, ignoring sign up attempt @@ -113,7 +115,7 @@ async def on_raw_reaction_add(self, payload: RawReactionActionEvent): event.undoSignup(user) # Update event - await msgFnc.updateMessageEmbed(message, event) + await event.container.updateEmbed() EventDatabase.toJson() message_action = "Signoff" diff --git a/messageFunctions.py b/messageFunctions.py index 9371b0e..00d351d 100644 --- a/messageFunctions.py +++ b/messageFunctions.py @@ -6,22 +6,25 @@ import config as cfg from event import Event +from container import Container # from operationbot import OperationBot - -async def getEventMessage(event: Event, bot, archived=False) \ +async def getEventMessage(event: Event, bot) \ -> Optional[Message]: """Get a message related to an event.""" - if archived: - channel = bot.eventarchivechannel + if event.container is not None: + return event.container.getMessage(bot) else: - channel = bot.eventchannel + if event.archived: + channel = bot.eventarchivechannel + else: + channel = bot.eventchannel - try: - return await channel.fetch_message(event.messageID) - except NotFound: - return None + try: + return await channel.fetch_message(event.messageID) + except NotFound: + return None async def getEvent(messageID, ctx: Context) -> Optional[Event]: @@ -34,7 +37,6 @@ async def getEvent(messageID, ctx: Context) -> Optional[Event]: return None return eventToUpdate - async def sortEventMessages(target: Messageable, bot=None): """Sort events in event database.""" if bot is None: @@ -43,87 +45,20 @@ async def sortEventMessages(target: Messageable, bot=None): else: raise ValueError("Requires either the bot argument or context.") + Container.sortEvents() from eventDatabase import EventDatabase - EventDatabase.sortEvents() print(EventDatabase.events) - event: Event - for event in EventDatabase.events.values(): - message = await getEventMessage(event, bot) + for container in [event.container for event in EventDatabase.events.values()]: + message = await container.getMessage(bot) if message is None: await target.send( "sortEventMessages: No message found with that message ID: {}" - .format(event.messageID)) + .format(container.id)) return - await updateReactions(event, message=message) - await updateMessageEmbed(message, event) - - -# from EventDatabase -async def createEventMessage(event: Event, channel: TextChannel) \ - -> Message: - """Create a new event message.""" - # Create embed and message - embed = event.createEmbed() - embed.set_footer(text="Event ID: " + str(event.id)) - message = await channel.send(embed=embed) - event.messageID = message.id - - return message - - -# was: EventDatabase.updateEvent -async def updateMessageEmbed(eventMessage: Message, updatedEvent: Event) \ - -> None: - """Update the embed and footer of a message.""" - newEventEmbed = updatedEvent.createEmbed() - newEventEmbed.set_footer(text="Event ID: " + str(updatedEvent.id)) - await eventMessage.edit(embed=newEventEmbed) - - -# from EventDatabase -async def updateReactions(event: Event, message: Message = None, bot=None): - """ - Update reactions of an event message. - - Requires either the `message` or `bot` argument to be provided. - """ - if message is None: - if bot is None: - raise ValueError("Requires either the `message` or `bot` argument" - " to be provided") - message = await getEventMessage(event, bot) - - reactions: List[Emoji] = event.getReactions() - reactionEmojisIntended = reactions - reactionsCurrent = message.reactions - reactionEmojisCurrent = {} - reactionsToRemove = [] - reactionEmojisToAdd = [] - - # Find reaction emojis current - for reaction in reactionsCurrent: - reactionEmojisCurrent[reaction.emoji] = reaction - - # Find emojis to remove - for emoji, reaction in reactionEmojisCurrent.items(): - if emoji not in reactionEmojisIntended: - reactionsToRemove.append(reaction) - - # Find emojis to add - for emoji in reactionEmojisIntended: - if emoji not in reactionEmojisCurrent.keys(): - reactionEmojisToAdd.append(emoji) - - # Remove existing unintended reactions - for reaction in reactionsToRemove: - await message.clear_reaction(reaction) - - # Add not existing intended emojis - for emoji in reactionEmojisToAdd: - await message.add_reaction(emoji) - + await container.updateReactions(message=message) + await container.updateEmbed() # async def createMessages(events: Dict[int, Event], bot): # # Update event message contents and add reactions @@ -150,13 +85,16 @@ async def syncMessages(events: Dict[int, Event], bot): message = await getEventMessage(event, bot) if message is not None and messageEventId(message) == event.id: print("found message {} for event {}".format(message.id, event)) + if event.container is None: + print("event {} does not have a container, creating" + .format(event)) + event.container = Container(event, bot.eventchannel, message) else: print("missing a message for event {}, creating".format(event)) - message = await createEventMessage(event, bot.eventchannel) + message = await Container.send(event, bot.eventchannel) await sortEventMessages(bot.commandchannel, bot) - # async def importMessages(events: Dict[int, Event], bot): # found = 0 # async for message in bot.eventchannel.history():