class RoleByReaction(Cog): ######################################### CONSTRUCTOR ######################################### def __init__(self, bot: Bot): self.bot = bot self.config = Cfg(self) self.defaults = GuildData( channel=0, combinations=list(), message=0, title="React with corresponding emoji to get role" ) self.config.defaults_guild(self.defaults) self.bot.loop.create_task(self.startup_check()) ########################################### UNLOADER ########################################## def cog_unload(self): del self ########################################## SCHEDULER ########################################## async def startup_check(self): await self.bot.wait_until_ready() guilds_configs = self.config.all_guilds() for guild_id, guild_config in guilds_configs.items(): guild = self.bot.get_guild(guild_id) if guild: guild_data = guild_config.get() await self._treat_guild(guild, guild_data) ########################################### EVENTS ############################################ async def _treat_payload(self, payload: RawReactionActionEvent): guild = self.bot.get_guild(payload.guild_id) if guild: guild_config = self.config.guild(guild) guild_data = guild_config.get() await self._treat_reaction(guild, guild_data, payload) @Cog.listener() async def on_raw_reaction_add(self, payload: RawReactionActionEvent): await self._treat_payload(payload) @Cog.listener() async def on_raw_reaction_remove(self, payload: RawReactionActionEvent): await self._treat_payload(payload) ################################## ROLE BY REACTION COMMANDS ################################## @admin_or_permissions() @group() async def rbr(self, ctx: Context): pass @admin() @rbr.command() async def title(self, ctx: Context, *, title: str): guild_config = self.config.guild(ctx.guild) guild_data = guild_config.get() guild_data.title = title try: await self._edit_rbr_message(ctx, guild_data) except InvalidArguments: pass guild_config.set(guild_data) embed = Embed( title='Title Changed', description=f'Successfully updated title to {title}' ) await ctx.send(embed=embed) @admin_or_permissions(manage_roles=True) @rbr.command() async def add(self, ctx: Context, emoji: EmojiType, *, role: RoleType): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() combinations = guild_data.combinations try: emoji = await self.import_emoji(ctx, emoji) if emoji in [c.emoji for c in combinations]: raise InvalidArguments( ctx=ctx, title="Role Error", message=f"Role {role} already used" ) role = await self.import_role(ctx, role) if role.id in [c.role for c in combinations]: raise InvalidArguments( ctx=ctx, title="Role Error", message=f"Role {role} already used" ) if not can_give_role(role, ctx.me): raise InvalidArguments( ctx=ctx, title="Role Error", message=f"Bot doesn't have enough rights to give role" ) except InvalidArguments as error: await error.execute() else: new = Combination( emoji=emoji.id if hasattr(emoji, "id") else emoji, role=role.id ) combinations.append(new) guild_data.combinations = combinations try: await self._edit_rbr_message(ctx, guild_data) except InvalidArguments: pass guild_config.set(guild_data) embed = Embed( title="Combination Added", description=f"{emoji} successfully linked with {role}" ) await ctx.send(embed=embed) @admin_or_permissions(manage_roles=True) @rbr.command() async def remove(self, ctx: Context, *, element: Union[EmojiType, RoleType]): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() combinations = guild_data.combinations try: if not combinations: raise InvalidArguments( ctx=ctx, title="Data Error", message="No data registered yet" ) try: element = await self.import_emoji(ctx, element) element = element.id if hasattr(element, "id") else element var = "emoji" except InvalidArguments: try: role = await self.import_role(ctx, element) element = role.id var = "role" except InvalidArguments: raise InvalidArguments( ctx=ctx, message="Reference not found" ) if element not in [c[var] for c in combinations]: raise InvalidArguments( ctx=ctx, message="Argument wasn't found in data" ) except InvalidArguments as error: await error.execute() else: for combination in combinations: if combination[var] == element: combinations.remove(combination) break guild_data.combinations = combinations try: await self._edit_rbr_message(ctx, guild_data) except InvalidArguments: pass guild_config.set(guild_data) emoji = await self.import_emoji(ctx, combination.emoji) role = guild.get_role(combination.role) embed = Embed( title="Combination Removed", description=f"{emoji} and {role} successfully unlinked" ) await ctx.send(embed=embed) @admin() @rbr.command() async def create(self, ctx: Context): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() embed = self._rbr_message_content(guild, guild_data) message = await ctx.send(embed=embed) await self._add_reactions( ctx=ctx, message=message, emojis=[c.emoji for c in guild_data.combinations] ) guild_data.channel = message.channel.id guild_data.message = message.id guild_config.set(guild_data) @admin() @rbr.command() async def message(self, ctx: Context, channel_id: int, message_id: int): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() guild_data.channel = channel_id guild_data.message = message_id try: message = await self._edit_rbr_message(ctx, guild_data) except InvalidArguments as error: await error.execute() else: guild_config.set(guild_data) embed = Embed( title="Message Set", description=f"[jump to]({message.jump_url})" ) await ctx.send(embed=embed) @admin_or_permissions(manage_roles=True) @rbr.command() async def show(self, ctx: Context): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() guild_data.title = "Role By Reaction Template" embed = self._rbr_message_content(guild, guild_data) await ctx.send(embed=embed) @admin() @rbr.command() async def reset(self, ctx: Context): answer = await ask_confirmation(ctx) if answer: guild = ctx.guild self.config.guild(guild).set(self.defaults) embed = Embed( title="Data Reset" ) await ctx.send(embed=embed) else: embed = Embed( title="Cancelled" ) await ctx.send(embed=embed) @admin() @rbr.command() async def update(self, ctx: Context): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() try: await self._edit_rbr_message(ctx, guild_data) except InvalidArguments as error: await error.execute() ######################################## CLASS METHODS ######################################## @classmethod async def _add_reactions(cls, ctx: Context, message: Message, emojis: List[Union[int, str]]): for emoji in emojis: try: emoji = ImprovedList(ctx.guild.emojis).get_item( emoji, key=lambda e: e.id ) await message.add_reaction(emoji) except ValueError: try: await message.add_reaction(str(emoji)) except: print(f"ROLEBYREACTION_COG: couldn't react with {emoji} on {message.jump_url}") @classmethod async def _edit_rbr_message(cls, ctx: Context, guild_data: GuildData) -> Message: guild = ctx.guild channel_id = guild_data.channel message_id = guild_data.message combinations = guild_data.combinations message = await cls._find_rbr_message(guild, guild_data, ctx) if message.author.id == ctx.me.id: embed = cls._rbr_message_content(guild, guild_data) await message.edit(embed=embed) await cls._add_reactions( ctx=ctx, message=message, emojis=[c.emoji for c in combinations] ) return message else: raise InvalidArguments( ctx=ctx, title="Message Error", description="Linked message isn't from bot" ) @classmethod async def _treat_guild(cls, guild: Guild, guild_data: GuildData): try: message = await cls._find_rbr_message(guild, guild_data) except InvalidArguments: # message not found pass else: reactions_state = await cls._make_reactions_state(guild, guild_data, message.reactions) members_state = cls._make_members_state(guild, guild_data) to_add, to_remove = cls._compare_reaction_members(reactions_state, members_state) await cls._edit_members_roles(to_add, to_remove) @classmethod async def _treat_reaction( cls, guild: Guild, guild_data: GuildData, payload: RawReactionActionEvent ): message_id = guild_data.message if payload.message_id == message_id: combinations = guild_data.combinations emoji = payload.emoji emoji = emoji.id if emoji.id else str(emoji) role = None for c in combinations: if c.emoji == emoji: role = guild.get_role(c.role) break if role: member = guild.get_member(payload.user_id) if member: method = cls._get_role_method( member=member, event_type=payload.event_type ) await method(role) ####################################### STATIC METHODS ######################################## @staticmethod def _compare_reaction_members( react_state: Dict[Member, Set[Role]], memb_state: Dict[Member, Set[Role]] ) -> Dict[Member, Set[Role]] and Dict[Member, Set[Role]]: # Dict[Member, Set[Role]] add = dict() for member in react_state.keys(): member_add = react_state[member] - memb_state.get(member, set()) if member_add: add[member] = member_add # Dict[Member, Set[Role]] remove = dict() for member in memb_state.keys(): member_remove = memb_state[member] - react_state.get(member, set()) if member_remove: remove[member] = member_remove return add, remove @staticmethod async def _edit_members_roles(add: Dict[Member, Set[Role]], remove: Dict[Member, Set[Role]]): for member, roles in add.items(): await member.add_roles(*roles) for member, roles in remove.items(): await member.remove_roles(*roles) @staticmethod async def _find_rbr_message(guild: Guild, guild_data: GuildData, ctx: Context=None) -> Message: channel_id = guild_data.channel message_id = guild_data.message channel = guild.get_channel(channel_id) if channel: try: return await channel.fetch_message(message_id) except NotFound: raise InvalidArguments( ctx=ctx, title="Message Not Found", message="Message doesn't exist or not provided" ) else: raise InvalidArguments( ctx=ctx, title="Channel Not Found", message="Channel doesn't exist or not provided" ) @staticmethod def _get_role_method(member: Member, event_type: str) -> Awaitable: if event_type == "REACTION_ADD": async def method(role): await member.add_roles(role) elif event_type == "REACTION_REMOVE": async def method(role): await member.remove_roles(role) else: async def method(role): return return method @staticmethod def _make_members_state(guild: Guild, guild_data: GuildData) -> Dict[Member, Set[Role]]: members = guild.members roles = [c.role for c in guild_data.combinations] return {member: {role for role in member.roles if role.id in roles} for member in members} @staticmethod async def _make_reactions_state( guild: Guild, guild_data: GuildData, reactions: List[Reaction] ) -> Dict[Member, Set[Role]]: combinations = guild_data.combinations # Dict[Role, Set[Member]] state_by_role = dict() for reaction in reactions: emoji = reaction.emoji try: combination = ImprovedList(combinations).get_item( v=emoji.id if hasattr(emoji, "id") else str(emoji), key=lambda c: c.emoji ) except ValueError: # emoji not registered in combinations list continue else: role = guild.get_role(combination.role) if role: # role might have been deleted, so excluding None case # Set[Member] members = set() async for user in reaction.users(): # excluding non-Members and self-reaction cases if isinstance(user, Member) and user != guild.me: members.add(user) state_by_role[role] = members return revert_dict(state_by_role) @staticmethod def _rbr_message_content(guild: Guild, guild_data: GuildData) -> Embed: title = guild_data.title combinations = guild_data.combinations if combinations: content = "" for combination in combinations: emoji = combination.emoji try: emoji = ImprovedList(guild.emojis).get_item( emoji, key=lambda e: e.id ) except ValueError: pass role = guild.get_role(combination.role) if not role: # Role does not exist case continue content += f"{emoji} - {role.name}" + "\n" else: content = "No combination registered yet" return Embed( title=title, description=content ) @staticmethod async def import_emoji(ctx: Context, emoji: EmojiType) -> Union[str, Emoji]: emoji = str(emoji) try: emoji = await EmojiConverter().convert(ctx, emoji) except BadArgument: temp = demojize(emoji) if any([emoji == temp, # Not an emoji temp.count(":") != 2, # More or less than an emoji not temp.startswith(":"), # More than an emoji not temp.endswith(":")]): # More than an emoji raise InvalidArguments( ctx=ctx, title="Emoji Error", message=f"Couldn't load {emoji} emoji" ) return emoji @staticmethod async def import_role(ctx: Context, role: RoleType) -> Role: try: return await RoleConverter().convert(ctx, str(role)) except BadArgument: raise InvalidArguments( ctx=ctx, title="Role Error", message=f"Couldn't load {role} role" )
class Event(Cog): ######################################### CONSTRUCTOR ######################################### def __init__(self, bot): self.bot = bot self.config = Cfg(self) self.defaults_guild = GuildData(events=[]) self.config.defaults_guild(self.defaults_guild) self.on = True self.tasks = [] self.bot.loop.create_task(self.scheduler()) print("EVENT_COG: loaded") ########################################### UNLOADER ########################################## def cog_unload(self): self.on = False for t in self.tasks: t.cancel() del t del self print("EVENT_COG: unloaded") ########################################## SCHEDULER ########################################## async def scheduler(self): guilds_configs = self.config.all_guilds() for guild_id, guild_config in guilds_configs.items(): guild = self.bot.get_guild(guild_id) if guild: await self.add_events(guild, *(guild_config.get().events)) ######################################## EVENT COMMANDS ####################################### @admin_or_permissions(manage_roles=True) @group() async def event(self, ctx: Context): pass @admin_or_permissions(manage_roles=True) @event.command() async def add(self, ctx: Context, channel: Union[int, str, TextChannel], time: str, title: str, *participants: Union[int, str, Member, Role]): guild = ctx.guild try: # parsing title title = title[:200] # parsing date date = EventData.timestamp(time) # parsing participants (Member(s) or Role(s)) async def convert(p: Union[int, str, Member, Role]): try: return await RoleConverter().convert(ctx, str(p)) except BadArgument: try: return await MemberConverter().convert(ctx, str(p)) except BadArgument: raise InvalidArguments( ctx=ctx, title="Participant Error", message="Couldn't find provided participants") participants = [await convert(p) for p in participants] # parsing channel try: channel = await TextChannelConverter().convert( ctx, str(channel)) channel_id = channel.id except BadArgument: try: # keeping 0 value as a no-announcement-channel criterium channel_id = int(channel) if channel_id: raise ValueError except ValueError: raise InvalidArguments( ctx=ctx, title="Channel Error", message="Couldn't find provided channel") guild_config = self.config.guild(guild) guild_data = guild_config.get() # checking if same event already exists events = ImprovedList(guild_data.events) try: events.index((title.lower(), date), key=lambda e: (e.title.lower(), e.date)) raise InvalidArguments( ctx=ctx, title="Name Error", message="There is another event on same date with same name" ) except ValueError: pass # appending event event = EventData(channel=channel_id, date=date, participants=[p.id for p in participants], title=title) guild_data.events.append(event) guild_config.set(guild_data) # starting event scheduler await self.add_events(guild, event) embed = Embed( title="Event added", description= (f"Title: {title}" + "\n" f"Date: {event.datetime()}" + "\n" f"Participants: {' '.join(map(lambda p: p.mention, participants))}" )) await ctx.send(embed=embed) except InvalidArguments as error: await error.execute() @admin_or_permissions(manage_roles=True) @event.command(name="list") async def list_(self, ctx: Context): guild = ctx.guild guild_config = self.config.guild(guild) guild_data = guild_config.get() events = [e for e in guild_data.events if not e.elapsed()] events = lexsorted(events, key=lambda e: (e.date, e.title)) if events: def convert(p: int): ret = guild.get_member(p) if not ret: ret = guild.get_role(p) return ret message = "" for event in events: participants = [ c for p in event.participants if (c := convert(p)) ] message += ( f"{event.datetime()} - {event.title}: " f"{', '.join(map(lambda p: p.mention, participants))}" + "\n")
class Birthday(Cog): ######################################### CONSTRUCTOR ######################################### def __init__(self, bot: Bot): self.bot = bot self.config = Cfg(self) self.defaults_guild = GuildData(channel=0, role=0) self.config.defaults_guild(self.defaults_guild) self.defaults_member = MemberData(birthday=Date(day=None, month=None), name="Unknown") self.config.defaults_member(self.defaults_member) self.on = True self.task = self.bot.loop.create_task(self.scheduler()) print("BIRTHDAY_COG: loaded") ########################################### UNLOADER ########################################## def cog_unload(self): self.on = False self.task.cancel() del self.task del self print("BIRTHDAY_COG: unloaded") ########################################## SCHEDULER ########################################## async def scheduler(self): while self.on: await self.wait_for_tomorrow(self.bot.loop) if self.on: print("New day!") guilds_configs = self.config.all_guilds() for guild_id, guild_config in guilds_configs.items(): guild = self.bot.get_guild(guild_id) if guild: await self.treat_guild(guild, guild_config.get(), self.config.all_members(guild)) ###################################### BIRTHDAY COMMANDS ###################################### @group() async def birthday(self, ctx: Context): pass @admin() @birthday.command() async def check(self, ctx: Context): guild = ctx.guild await self.treat_guild(guild, self.config.guild(guild).get(), self.config.all_members(guild)) @admin() @birthday.command() async def channel(self, ctx: Context, *, channel: Union[int, str, TextChannel]): try: if not isinstance(channel, TextChannel): channel = await TextChannelConverter().convert( ctx, str(channel)) except BadArgument: error = InvalidArguments( ctx=ctx, title='Channel Error', message='Channel not found or not provided') await error.execute() return else: guild_config = self.config.guild(ctx.guild) guild_data = guild_config.get() guild_data.channel = channel.id guild_config.set(guild_data) embed = Embed( title='Channel Changed', description=f'Successfully updated channel to {channel.mention}' ) await ctx.send(embed=embed) @admin() @birthday.command() async def role(self, ctx: Context, *, role: Union[int, str, Role]): try: if not isinstance(role, Role): role = await RoleConverter().convert(ctx, str(role)) except BadArgument: error = InvalidArguments(ctx=ctx, title='Role Error', message='Role not found or not provided') await error.execute() else: guild_config = self.config.guild(ctx.guild) guild_data = guild_config.get() guild_data.role = role.id guild_config.set(guild_data) embed = Embed( title='Role Changed', description=f'Successfully updated role to {role.mention}') await ctx.send(embed=embed) async def _set_birthday(self, ctx: Context, member: Member, day: int, month: int): try: date = Date.convert_date(day=day, month=month) except ValueError: error = InvalidArguments( ctx=ctx, title="Date Error", message="Couldn't understand provided date") await error.execute() else: member_config = self.config.member(member) member_data = MemberData(birthday=date, name=member.name) member_config.set(member_data) if ctx.author == member: desc = f"Birthday set to {date}" else: desc = f"Birthday of {member} has been set to {date}" embed = Embed(title="Birthday Set", description=desc) await ctx.send(embed=embed) @birthday.command(name='set') async def set_(self, ctx: Context, day: int, month: int): await self._set_birthday(ctx, ctx.author, day, month) @admin() @birthday.command() async def forceset(self, ctx: Context, member_id: int, day: int, month: int): try: member = ctx.guild.get_member(int(member_id)) if member: await self._set_birthday(ctx, member, day, month) else: raise InvalidArguments(ctx=ctx, title="Member Error", message="Couldn't find provided member") except InvalidArguments as error: await error.execute() except TypeError: error = InvalidArguments( ctx=ctx, title="Argument Error", message="Provided member id couldn't be parsed") @birthday.command() async def remove(self, ctx: Context): member = ctx.author member_config = self.config.member(member) new = self.defaults_member.copy() new.name = member.name member_config.set(new) embed = Embed(title="Birthday Reset", description="Birthday has been removed from database") await ctx.send(embed=embed) @birthday.command(name='list') async def list_(self, ctx: Context): guild = ctx.guild members_configs = self.config.all_members(guild) # Dict[int, MemberData] members_data = {i: g.get() for i, g in members_configs.items()} # List[MemberData] to_sort = [] for member_id, member_data in members_data.items(): if not member_data.birthday: # None case -> skip continue member = guild.get_member(member_id) if member: # Member found case -> directly using their name member_data.name = member.name to_sort.append(member_data) if to_sort: # sorting lexicographically birthdays list for better readability def key(m: MemberData): return (m.birthday.month, m.birthday.day, m.name) # List[MemberData] sorted_birthdays = lexsorted(to_sort, key=key) message = "\n".join(map(str, sorted_birthdays)) embed = Embed(title="Birthdays List", description=message) await ctx.send(embed=embed) else: embed = Embed(title="Birthdays List", description="No birthday recorded yet") await ctx.send(embed=embed) ######################################## CLASS METHODS ######################################## @classmethod async def treat_guild(cls, guild: Guild, guild_data: GuildData, members_configs: Dict[int, Group]): # importing Members from their ids # Dict[Member, Group] members_configs = { guild.get_member(i): g for i, g in members_configs.items() } # Dict[Member, Group] with None cases filtered out members_configs = {m: g for m, g in members_configs.items() if m} # keeping trace of member names if they leave the server cls.update_names(members_configs) # matching current date with birthdays now = dt.now() today = Date(day=now.day, month=now.month) # Dict[Member, Date] members_birthdays = { m: g.get().birthday for m, g in members_configs.items() } # List[Member] to_treat = [m for m, b in members_birthdays.items() if today == b] # sending birthday message channel_id = guild_data.channel channel = guild.get_channel(channel_id) if channel: await cls.treat_members(channel, to_treat) # updating roles role_id = guild_data.role role = guild.get_role(role_id) if role and can_give_role(role, guild.me): await cls.treat_role(role, to_treat) ######################################## STATIC METHODS ####################################### @staticmethod async def treat_members(channel: TextChannel, to_treat: List[Member]): for member in to_treat: await channel.send( f":tada: Happy birthday {member.mention}!!! :cake:") @staticmethod async def treat_role(role: Role, to_treat: List[Member]): for member in role.guild.members: roles = member.roles if (member in to_treat) and (role not in roles): await member.add_roles(role) elif (member not in to_treat) and (role in roles): await member.remove_roles(role) @staticmethod def update_names(members_configs: Dict[Member, Group]): for member, group in members_configs.items(): member_data = group.get() member_data.name = member.name group.set(member_data) @staticmethod async def wait_for_tomorrow(loop: AbstractEventLoop): now = dt.now() date = dt.fromordinal(dt.today().toordinal()) next_day = date + td(days=1) wait_time = (next_day - now).total_seconds() await sleep(wait_time, loop=loop)