async def add_first_emojis(message): """ Scans the message for menu entries (see `activate_side_games` command) and adds the appropriate emoji reactions to the message, allowing other members to click on the emoji icons. Note that messages made by non-admins and messages in channels for which the side games voice channel feature is not enabled are ignored. """ if not checks.is_admin(message.author): return # ignore non-admin message if checks.author_is_me(message): return # ignore bot messages if checks.is_event_channel(message.channel): translations = get_emoji_event_channels_translations(message) for emoji in translations.keys(): await message.add_reaction(emoji) return # return if message in event channel if not checks.is_side_games_channel(message.channel): return # ignore messages in non-games channels translations = get_emoji_side_game_translations(message) for emoji in translations.keys(): await message.add_reaction(emoji)
class IceCream(commands.Cog): def __init__(self, client): self.client = client @commands.command() @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin()) async def icecream(self, ctx: commands.Context, day: Optional[str] = None) -> None: """Gives the ice-cream flavors on the menu for the given day. Parameters ----------- ctx: `commands.Context` A class containing metadata about the command invocation. day: `str` The day to find icecream flavors for. """ POSSIBLE_DAYS: List[str] = [d.upper() for d in calendar.day_name] EST = datetime.now(timezone("US/Eastern")) TODAY = EST.strftime("%A").upper() if day is None: day = TODAY day = day.strip().upper() if day in POSSIBLE_DAYS: pass elif day in "TOMORROW": day = POSSIBLE_DAYS[(POSSIBLE_DAYS.index(TODAY) + 1) % len(POSSIBLE_DAYS)] else: await ctx.send("Error: Not a valid day.") return flavors = nu_dining.ICE_CREAM_FLAVORS[day] await ctx.send(f"There is {flavors} on {day}.")
async def close_party(rp: ReactionPayload) -> None: """ Emoji handler that implements the party close feature. If the reacting member is not the party leader or a bot admin as specified in `config.BOT_ADMIN_ROLES`, the emoji is removed and no further action is taken. Otherwise, the party message the party affiliations (membership, leadership) are deleted and an appropriate message is posted to the party matchmaking channel. """ party = await Party.from_party_message(rp.message) channel = party.channel if party.leader != rp.member and not checks.is_admin(rp.member): await rp.message.remove_reaction(Emojis.NO_ENTRY_SIGN, rp.member) return if rp.member != party.leader: message = await channel.send(f"> {rp.member.mention} has just force " f"closed {party.leader.mention}'s party!") else: message = await channel.send(f"> {rp.member.mention} has just " f"disbanded their party!\n") await rp.message.delete() for m in party.members: db.party_channels[channel.id].clear_party_message_of_user(m) db.party_channels[channel.id].clear_party_message_of_user(party.leader) scheduling.message_delayed_delete(message)
class Clear(commands.Cog): def __init__(self, client): self.client = client self.MESSAGE_DELETE_CAPACITY = 500 self.MEMBER_DELETE_CAPACITY = 100 @commands.command() @commands.guild_only() @commands.check_any(is_admin(), is_mod()) async def clear(self, ctx: commands.Context, amount: int = 1, member: discord.Member = None) -> None: """Deletes a set amount of message from the current channel. Parameters ------------ ctx: `commands.Context` A class containing metadata about the command invocation. amount: `int` The amount of message to delete from the current channel. member: `discord.Member` Will delete messages only from the given member. """ channel = ctx.channel await ctx.message.delete() if amount <= 0: await ctx.send("Cannot delete less than 1 message.") return deleted_messages: List[discord.Message] = [] if member is None: if amount > self.MESSAGE_DELETE_CAPACITY: await ctx.send( f"Cannot delete more than {self.MESSAGE_DELETE_CAPACITY} " "messages at a time.") return deleted_messages = await channel.purge(limit=amount) else: if amount > self.MEMBER_DELETE_CAPACITY: await ctx.send( f"Cannot delete more than {self.MEMBER_DELETE_CAPACITY} " "messages when a member is mentioned.") return num_deleted: int = 0 async for message in channel.history(limit=None): if num_deleted == amount: break if message.author == member: deleted_messages.append(message) num_deleted += 1 await channel.delete_messages(deleted_messages) await ctx.send(f"{len(deleted_messages)} messages deleted.", delete_after=5)
class Misc(commands.Cog): def __init__(self, client): self.client = client self.start_time = datetime.utcnow() @commands.command() @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod()) async def ping(self, ctx: commands.Context): """Sends a message which contains the Discord WebSocket protocol latency. Parameters ------------- ctx: `commands.Context` A class containing metadata about the command invocation. """ await ctx.send(f"Pong! {round(self.client.latency * 1000)}ms") @commands.command() @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod()) async def uptime(self, ctx: commands.Context): """Calculates the amount of time the bot has been up since it last started. Parameters ------------- ctx: `commands.Context` A class containing metadata about the command invocation. """ delta_uptime = datetime.utcnow() - self.start_time hours, remainder = divmod(int(delta_uptime.total_seconds()), 3600) minutes, seconds = divmod(remainder, 60) days, hours = divmod(hours, 24) await ctx.send(f"Uptime: `{days}d, {hours}h, {minutes}m, {seconds}s`") @is_admin() @commands.command() async def echo(self, ctx: commands.Context) -> None: """Repeats the message given after the command. Will use the escaped content which does not mention anything. Parameters ------------- ctx: `commands.Context` A class containing metadata about the command invocation. """ message: discord.Message = ctx.message await message.delete() content: str = message.clean_content[6:] if content: await ctx.send(content) else: await ctx.send("You didn't give anything to repeat", delete_after=5) @commands.command() @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod()) async def flip(self, ctx: commands.Context) -> None: """Flips an imaginary coin and sends the result. Parameters ------------- ctx: `commands.Context` A class containing metadata about the command invocation. """ results = ["Heads!", "Tails!"] outcome = random.randint(0, 1) await ctx.send(results[outcome]) @commands.command() @commands.check_any(is_dm(), in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod()) async def menu(self, ctx: commands.Context) -> None: """Sends a link to the Northeastern dining hall menu. Parameters ------------- ctx: `commands.Context` A class containing metadata about the command invocation. """ await ctx.send("https://nudining.com/public/menus")
class HallOfFame(commands.Cog): """Handles the automation behind the hall-of-fame channel. If at least [self.reaction_threshold] reactions with the 🏆 emoji are added to a message, then an embedded message with the content of the message, a link to the channel, and a link to the message are posted in the hall-of-fame channel. Note: - If the user who reacts to the message is the same person who sent it, it will not count. - If a moderator reacts with the designated mod hall-of-fame emoji, then it will automatically be send in the hall-of-fame channel regardless of emoji count. Attributes ---------- client : `commands.Bot` a client connection to Discord to interact with the Discord WebSocket and APIs reaction_threshold: `int` the number of emojis that need to be reacted with (excluding the user who sent the message) in order to enter the hall-of-fame. hof_emoji: `str` The emoji which is tracked in order to determine whether to send it to the hall-of-fame or not. mod_hof_emoji: `str` An override emoji. When reacted to a message with it will automatically be sent in hall-of-fame regardless of the emoji count on the current message. """ def __init__(self, client: Bot): self.client = client self.reaction_threshold: int = 5 self.hof_emoji: str = "🏆" self.mod_hof_emoji: str = "🏅" self.hof_blacklist: Dict[int, Set[int]] = defaultdict(set) for document in self.client.db.get_hof_blacklist(): guild_id: int = document["guild_id"] channels: List[int] = document["channels"] self.hof_blacklist[guild_id] = set(channels) @commands.is_owner() @commands.command(aliases=["setHOFThreshold"]) @required_configs(ChannelType.HOF) async def set_hof_threshold(self, ctx: commands.Context, threshold: int) -> None: """Sets the new reaction threshold.""" await ctx.send( f"Hall of Fame reaction threshold set to `{threshold}` from `{self.reaction_threshold}`" ) self.reaction_threshold = threshold @commands.check_any(is_admin(), is_mod()) @commands.command(aliases=["addHOFBlacklist"]) @required_configs(ChannelType.HOF) async def add_hof_blacklist(self, ctx: commands.Context, channel: discord.TextChannel): guild: discord.Guild = ctx.guild if channel.id in self.hof_blacklist[guild.id]: return await ctx.send( "This channel is already being blacklisted by HOF.") self.client.db.add_to_hof_blacklist(guild.id, channel.id) self.hof_blacklist[guild.id].add(channel.id) await ctx.send(f"{channel.mention} has been added to the HOF blacklist" ) @commands.command(aliases=["removeHOFBlacklist", "rmHOFBlacklist"]) @required_configs(ChannelType.HOF) async def remove_hof_blacklist(self, ctx: commands.Context, channel: discord.TextChannel): guild: discord.Guild = ctx.guild if channel.id not in self.hof_blacklist[guild.id]: return await ctx.send( "This channel was never blacklisted in the first place") self.client.db.remove_from_hof_blacklist(ctx.guild.id, channel.id) self.hof_blacklist[ctx.guild.id].remove(channel.id) await ctx.send( f"{channel.mention} has been removed from the HOF blacklist") @commands.command(aliases=["listHOFBlacklist", "lsHOFBlacklist"]) @required_configs(ChannelType.HOF) async def list_hof_blacklist(self, ctx: commands.Context): guild: discord.Guild = ctx.guild blacklisted_channel_mentions: List[str] = [ guild.get_channel(channel_id).mention for channel_id in self.hof_blacklist[guild.id] ] await ctx.send( f"There are {len(blacklisted_channel_mentions)} channels being blacklisted for HOF currently: {','.join(blacklisted_channel_mentions)}" ) @commands.Cog.listener() @required_configs(ChannelType.HOF) async def on_raw_reaction_add( self, payload: discord.RawReactionActionEvent) -> None: """Checks if enough reactions were added and if so sends a message in the hall-of-fame channel. Parameters ------------- payload: `discord.RawReactionActionEvent` The reaction payload with information about the event. """ guild_id: int = payload.guild_id channel_id: int = payload.channel_id if channel_id in self.hof_blacklist[guild_id]: return emoji: discord.PartialEmoji = payload.emoji mod_emoji_used: bool = emoji.name == self.mod_hof_emoji if emoji.name != self.hof_emoji and not mod_emoji_used: return message_id: int = payload.message_id if self.client.db.message_in_hof(guild_id, message_id): return guild: discord.Guild = self.client.get_guild(guild_id) channel: discord.TextChannel = guild.get_channel(channel_id) message: discord.Message = await channel.fetch_message(message_id) member: discord.Member = payload.member author: discord.Member = message.author if author == member: return mod: Optional[discord.Role] = discord.utils.get(member.roles, name="Moderator") send_message: bool = False if mod_emoji_used: if not mod: return else: send_message = True else: reaction: discord.Reaction = next(r for r in message.reactions if str(r.emoji) == emoji.name) reaction_count: int = reaction.count if reaction_count > self.reaction_threshold: send_message = True elif reaction_count == self.reaction_threshold: send_message = True async for user in reaction.users(): if user == author: send_message = False break if send_message: HALL_OF_FAME_CHANNEL: discord.TextChannel = self.client.get_hof_channel( guild_id) embed = discord.Embed(color=discord.Color.red(), timestamp=message.created_at) embed.set_author(name=author, icon_url=author.avatar_url) attachments: List[discord.Attachment] = message.attachments if attachments: embed.set_image(url=attachments[0].proxy_url) message_content: str = message.content if message_content: if len(message_content) > 1024: message_content = message_content[:1020] + "..." embed.add_field(name="Message", value=message_content) embed.add_field(name="Channel", value=channel.mention) embed.add_field(name="Jump To", value=f"[Link]({message.jump_url})") embed.set_footer(text=f"Message ID: {message_id}") await HALL_OF_FAME_CHANNEL.send(embed=embed) self.client.db.add_message_to_hof(guild_id, message_id)
class CourseSelection(commands.Cog): """Handles the interaction surrounding selecting courses or other roles from course-regisration via commands and clean-up. Attributes ------------ delete_self_message: `bool` Flag to determine whether to delete message sent by itself in the course-registration channel or not. """ def __init__(self, client: commands.Bot): self.client = client self.delete_self_message: bool = True @commands.Cog.listener() async def on_guild_channel_update(self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel) -> None: """Updates course enrollment count when a member joins or leaves. Parameters ----------- before: `discord.abc.GuildChannel` The channel before. after: `discord.abc.GuildChannel` The channel after. """ if not isinstance(after, discord.TextChannel): return if before.topic is None or after.topic is None: return if not IS_COURSE_TOPIC.match( before.topic) or not IS_COURSE_TOPIC.match(after.topic): return if before.overwrites == after.overwrites: return enrolled_count: int = len( [x for x in after.overwrites if isinstance(x, discord.Member)]) await after.edit( topic=re.sub(r"\(\d+", f"({enrolled_count}", after.topic)) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Deletes any message sent within 5 seconds in the course-registration channel. Will delete messages sent by itself when the delete_self_message flag is True, else it will not. Parameters ----------- message: `discord.Message` The sent message. """ channel: discord.TextChannel = message.channel if channel.id == COURSE_REGISTRATION_CHANNEL_ID: author: discord.Member = message.author admin: bool = author.permissions_in(channel).administrator if self.delete_self_message and author == self.client.user: await message.delete(delay=5) elif not admin and author != self.client.user: await message.delete(delay=5) @is_admin() @commands.guild_only() @commands.command(aliases=["toggleAD"]) async def toggle_ad(self, ctx: commands.Context) -> None: """Toggles the delete_self_message flag.""" self.delete_self_message = not self.delete_self_message await ctx.send( f"Deleting self-messages was toggled to: {self.delete_self_message}" ) @commands.command() @commands.guild_only() @commands.check_any(in_channel(COURSE_REGISTRATION_CHANNEL_ID), is_admin()) async def choose(self, ctx: commands.Context, *, name: str) -> None: """Command to add or remove any roles from the person invoking the command. If the author is not an admin, they can only toggle roles which are available in the course-registration channel. Parameters ------------ ctx: `commands.Context` A class containing metadata about the command invocation. name: `str` The name of the role/course to be toggled. """ message: discord.Message = ctx.message guild: discord.Guild = ctx.guild author: discord.Member = message.author ADMIN_CHANNEL = guild.get_channel(ADMIN_CHANNEL_ID) await message.delete() course_channel: Optional[discord.TextChannel] = None try: course_channel = await CourseChannelConverter().convert(ctx, name) except commands.BadArgument as e: if "invalid course format" not in str(e): await ADMIN_CHANNEL.send( f"{author.mention} just tried to add `{name}` using the `.choose` command. " "Consider adding this course.") await ctx.send( f"The course `{name}` is not available but I have notified the admin team to add it.", delete_after=5, ) return COURSE_REGISTRATION_CHANNEL: discord.TextChannel = guild.get_channel( COURSE_REGISTRATION_CHANNEL_ID) school_or_color: bool = False async for message in COURSE_REGISTRATION_CHANNEL.history( limit=4, oldest_first=True): if (f"({name.upper()})" in message.content) or (f"-> {name.title()}" in message.content): school_or_color = True break if school_or_color: role: discord.Role = await CaseInsensitiveRoleConverter().convert( ctx, name) if role in author.roles: await author.remove_roles(role) await ctx.send(f"`{role.name}` has been removed.", delete_after=5) else: await author.add_roles(role) await ctx.send(f"`{role.name}` has been added!", delete_after=5) elif course_channel: overwrites: discord.PermissionOverwrite = course_channel.overwrites_for( author) if overwrites.is_empty(): await course_channel.set_permissions(author, read_messages=True, send_messages=True) await ctx.send( f"You have enrolled in `{course_channel.topic}`", delete_after=5) else: await course_channel.set_permissions(author, overwrite=None) await ctx.send( f"You have unenrolled in `{course_channel.topic}`", delete_after=5, ) else: await ctx.send(f"`{name}` is neither a toggleable role/course.", delete_after=5)
class Stats(commands.Cog): def __init__(self, client: Bot): self.client = client @commands.command() @commands.guild_only() @commands.check_any(is_admin(), is_mod()) async def serverinfo(self, ctx: commands.Context) -> None: """ Sends an embedded message containing some stats about the server. Includes: Server ID, Server Owner, Region, Num of Channel Categories, Text Channels, Voice Channels, Roles, Members, Humans, Bots, Online/Idle/Dnd Members, Not Registered, New Accounts, Emojis, Verification Level, Active Invites, 2FA Status Parameters ----------- ctx: `commands.Context` A class containing metadata about the command invocation. Note: A New Account is considered to be an account which was created within 1 day of joining the server. """ guild: discord.Guild = ctx.guild REGISTERED_ROLE: discord.Role = discord.utils.get(guild.roles, name="Registered") new_accounts: int = Counter([(m.joined_at - m.created_at).days <= 1 for m in guild.members])[True] not_registered_count: int = guild.member_count - len( REGISTERED_ROLE.members) num_bots: int = Counter([m.bot for m in guild.members])[True] statuses = Counter([(m.status, m.is_on_mobile()) for m in guild.members]) online_mobile: int = statuses[(discord.Status.online, True)] idle_mobile: int = statuses[(discord.Status.idle, True)] dnd_mobile: int = statuses[(discord.Status.dnd, True)] online: int = statuses[(discord.Status.online, False)] + online_mobile idle: int = statuses[(discord.Status.idle, False)] + idle_mobile dnd: int = statuses[(discord.Status.dnd, False)] + dnd_mobile embed = discord.Embed(color=discord.Color.red(), timestamp=guild.created_at) embed.set_author(name=guild, icon_url=guild.icon_url) embed.set_footer(text=f"Server ID: {guild.id} | Server Created") embed.add_field(name="Server Owner", value=guild.owner.mention) embed.add_field(name="Region", value=guild.region) embed.add_field(name="Channel Categories", value=len(guild.categories)) embed.add_field(name="Text Channels", value=len(guild.text_channels)) embed.add_field(name="Voice Channels", value=len(guild.voice_channels)) embed.add_field(name="Roles", value=len(guild.roles)) embed.add_field(name="Members", value=guild.member_count) embed.add_field(name="Humans", value=guild.member_count - num_bots) embed.add_field(name="Bots", value=num_bots) embed.add_field(name="Online", value=f"{online} | Mobile: {online_mobile}") embed.add_field(name="Idle", value=f"{idle} | Mobile: {idle_mobile}") embed.add_field(name="Dnd", value=f"{dnd} | Mobile: {dnd_mobile}") embed.add_field(name="Not Registered", value=not_registered_count) embed.add_field(name="New Accounts", value=new_accounts) embed.add_field(name="Emojis", value=f"{len(guild.emojis)}/{guild.emoji_limit}") embed.add_field(name="Verification Level", value=guild.verification_level) embed.add_field(name="Active Invites", value=len(await guild.invites())) embed.add_field(name="2FA", value=bool(guild.mfa_level)) await ctx.send(embed=embed) @commands.command(aliases=[ "orderedListMembers", "lsMembers", "listMembers", "list_members" ]) @commands.guild_only() @commands.check_any(is_admin(), is_mod()) async def ordered_list_members(self, ctx: commands.Context, num: int = 10, output_type: str = "nickname") -> None: """ Sends an embedded message containing a list of members in order by when the joined the server. Parameters ----------- ctx: `commands.Context` A class containing metadata about the command invocation. num: `int` An integer representing the number of members to be displayed. output_type: `str` Specifies the format of the embedded message to display the users. Can be: "nickname", "nick", "name", "mention" Nickname will give a list of nicknames Name will give a list of usernames Mention will give a list of mentioned users. """ msg: str = "" count: int = 0 embed = discord.Embed(color=discord.Color.red(), timestamp=datetime.utcnow()) for member in sorted(ctx.guild.members, key=lambda m: m.joined_at): if count < num: if output_type == "nickname" or output_type == "nick": msg += member.display_name + ", " elif output_type == "name": msg += member.name + ", " elif output_type == "mention": msg += member.mention + ", " else: await ctx.send( "Valid display type not given. Try: nickname/nick/name/mention" ) return count += 1 if count % 10 == 0: embed.add_field(name=f"__{count - 9}-{count}:__", value=msg[:-2]) msg = "" if count % 100 == 0: await ctx.send(embed=embed) embed = discord.Embed(color=discord.Color.red(), timestamp=datetime.utcnow()) # if an even 10 people was not reached if msg != "": embed.add_field(name=f"__{count - (count % 10) + 1}-{count}:__", value=msg[:-2]) await ctx.send(embed=embed) # if less than 100 members was reached elif msg == "": await ctx.send(embed=embed) @commands.command(aliases=["whoam"]) @commands.guild_only() @commands.check_any(in_channel(BOT_SPAM_CHANNEL_ID), is_admin(), is_mod()) async def whois(self, ctx: commands.Context, *, member_name: str = None) -> None: """ Sends an embedded message containing information about the given user. Parameters ----------- ctx: `commands.Context` A class containing metadata about the command invocation. member_parts `Tuple`: The member's name as a tuple of strings. """ member: Optional[discord.Member] = None if member_name is None or member_name.upper() == "I": member = ctx.author else: try: member = await FuzzyMemberConverter().convert(ctx, member_name) except discord.ext.commands.errors.BadArgument as e: await ctx.send(e) return member_permissions: discord.Permissions = ctx.author.permissions_in( ctx.channel) if (ctx.author == member or member_permissions.administrator or member_permissions.view_guild_insights): join_position: int = member_join_position(member) roles: str = member_mentioned_roles(member) permissions: str = "" for perm in member.guild_permissions: if perm[1]: perm_name = perm[0].replace("_", " ").title() permissions += perm_name + ", " permissions = permissions[:-2] embed = discord.Embed( color=member.color, timestamp=datetime.utcnow(), description=member.mention, ) embed.set_thumbnail(url=member.avatar_url) embed.set_author(name=member, icon_url=member.avatar_url) embed.set_footer(text=f"Member ID: {member.id}") embed.add_field(name="Status", value=member.status) embed.add_field(name="Joined", value=timestamp_format(member.joined_at)) embed.add_field(name="Join Position", value=join_position) embed.add_field(name="Created At", value=timestamp_format(member.created_at)) embed.add_field(name=f"Roles ({len(member.roles) - 1})", value=roles) embed.add_field(name="Key Permissions", value=permissions) await ctx.send(embed=embed) else: await ctx.message.delete() await ctx.send("Stop snooping around where you shouldn't 🙃", delete_after=5) @commands.command(aliases=["joinNo", "joinPosition", "joinPos"]) @commands.guild_only() @commands.check_any(is_admin(), is_mod()) async def join_no(self, ctx: commands.Context, join_no: int) -> None: """ Sends an embedded message containing information about the user who joined the server at the given join position. Parameters ----------- ctx: `commands.Context` A class containing metadata about the command invocation. join_no: `int` The join position of the user to get information about. """ guild: discord.Guild = ctx.guild if join_no <= 0: await ctx.send("Number must be a positive non-zero number.") return try: member: discord.Member = sorted( guild.members, key=lambda m: m.joined_at)[join_no - 1] except IndexError: await ctx.send(f"{guild} only has {guild.member_count} members!") return await self.whois(ctx, member.name)