class RemindMe(BaseModule): ID = __name__.split(".")[-1] NAME = "RemindMe" DESCRIPTION = "Allows users to create reminders" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="max_reminders_per_user", label="Maximum reminders per user", type="number", placeholder="", default=3, ), ModuleSetting( key="cost", label="Points required to add a reminder", type="number", placeholder="", default="0", ), ModuleSetting( key="emoji", label="Emoji for reminder", type="text", placeholder="🔔", default="🔔", ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.redis = RedisManager.get() self.reminder_tasks = {} @property def help(self): help_desc = f""" `Syntax: {self.bot.command_prefix}remindme <time> <text>` Send you <text> when the time is up. Accepts: seconds, minutes, hours, days, weeks Examples: - {self.bot.command_prefix}remindme 2min Do that thing in 2 minutes - {self.bot.command_prefix}remindme 3h40m Do that thing in 3 hours and 40 minutes """ data = discord.Embed( title="RemindMe help Menu", description=help_desc, colour=discord.Colour.red(), ) data.set_thumbnail(url=self.bot.discord_bot.client.user.avatar_url) return data async def create_reminder(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] try: reminders_list = json.loads( self.redis.get(f"{self.bot.bot_name}:remind-me-reminders")) """ { user_id: [ { "message_id": message_id, "channel_id": channel_id, "message": message, "date_of_reminder": date_of_reminder, "date_reminder_set": date_reminder_set }, ], } """ except: self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps({})) reminders_list = {} user_reminders = (reminders_list[str(author.id)] if str(author.id) in reminders_list else []) if len(user_reminders) >= int(self.settings["max_reminders_per_user"]): await self.bot.say( channel, f"{author.mention} you already have {len(user_reminders)} reminders!", ) return False if len(command_args) == 0: await self.bot.say(channel, embed=self.help) return False time_delta = utils.parse_timedelta(command_args[0]) if not time_delta: await self.bot.say( channel, f"{author.mention} invalid time: {command_args[0]}") return False await self.bot.say( channel, f"{author.mention} ill remind you that in {utils.seconds_to_resp(time_delta.total_seconds())}", ) bot_message = await self.bot.say( channel, f"If anyone else wants to be reminded click the {self.settings['emoji']}", ) salt = utils.random_string() await bot_message.add_reaction(self.settings["emoji"]) reminder = { "message_id": bot_message.id, "channel_id": bot_message.channel.id, "salt": salt, "message": " ".join(command_args[1:]), "date_of_reminder": str(utils.now() + time_delta), "date_reminder_set": str(utils.now()), } user_reminders.append(reminder) reminders_list[str(author.id)] = user_reminders self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps(reminders_list)) self.reminder_tasks[salt] = ScheduleManager.execute_delayed( time_delta.total_seconds(), self.execute_reminder, args=[salt, author.id, reminder], ) async def forgetme(self, bot, author, channel, message, args): try: reminders_list = json.loads( self.redis.get(f"{self.bot.bot_name}:remind-me-reminders")) """ { user_id: [ { "message_id": message_id, "channel_id": channel_id, "salt": salt, "message": message, "date_of_reminder": date_of_reminder, "date_reminder_set": date_reminder_set }, ], } """ except: self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps({})) reminders_list = {} user_reminders = reminders_list[str(author.id)] if str( author.id) else [] for reminder in user_reminders: self.reminder_tasks.pop(reminder["salt"]).remove() try: channel = self.bot.discord_bot.guild.get_channel( int(reminder["channel_id"])) bot_message = await channel.fetch_message( int(reminder["message_id"])) await bot_message.delete() except Exception as e: log.error(f"Failed to delete message from bot: {e}") reminders_list[str(author.id)] = [] self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps(reminders_list)) await self.bot.say(channel, f"{author.mention} you have been forgotten") def load_commands(self, **options): self.commands["remindme"] = Command.raw_command( self.create_reminder, command="remindme", delay_all=0, delay_user=0, cost=int(self.settings["cost"]), can_execute_with_whisper=False, description="Creates a reminder", ) self.commands["forgetme"] = Command.raw_command( self.forgetme, command="forgetme", delay_all=0, delay_user=0, can_execute_with_whisper=False, description="Creates a reminder", ) async def execute_reminder(self, salt, user_id, reminder): self.reminder_tasks.pop(salt) try: channel = self.bot.discord_bot.guild.get_channel( int(reminder["channel_id"])) bot_message = await channel.fetch_message( int(reminder["message_id"])) except: return message = reminder["message"] for reaction in bot_message.reactions: if reaction.emoji == self.settings["emoji"]: users = await reaction.users().flatten() users.remove(self.bot.discord_bot.client.user) sender = await self.bot.discord_bot.get_user(user_id) if sender and sender not in users: users.append(sender) for user in users: date_of_reminder = utils.parse_date( reminder["date_of_reminder"]) date_reminder_set = utils.parse_date( reminder["date_reminder_set"]) seconds = int( round((date_of_reminder - date_reminder_set).total_seconds())) response_str = utils.seconds_to_resp(seconds) await self.bot.private_message( user, f"Hello! You asked me to remind you {response_str} ago:\n{message}", ) break try: await bot_message.delete() except Exception as e: log.error(f"Failed to delete message from bot: {e}") try: reminders_list = json.loads( self.redis.get(f"{self.bot.bot_name}:remind-me-reminders")) """ { user_id: [ { "message_id": message_id, "channel_id": channel_id, "salt": salt, "message": message, "date_of_reminder": date_of_reminder, "date_reminder_set": date_reminder_set }, ], } """ except: self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps({})) reminders_list = {} user_reminders = (reminders_list[str(user_id)] if str(user_id) in reminders_list else []) for _reminder in user_reminders: if _reminder == reminder: user_reminders.remove(_reminder) break reminders_list[str(user_id)] = user_reminders self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps(reminders_list)) def enable(self, bot): if not bot: return try: reminders_list = json.loads( self.redis.get(f"{self.bot.bot_name}:remind-me-reminders")) """ { user_id: [ { "message_id": message_id, "channel_id": channel_id, "salt": salt, "message": message, "date_of_reminder": date_of_reminder, "date_reminder_set": date_reminder_set }, ], } """ except: self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps({})) reminders_list = {} new_reminders_list = {} for user_id in reminders_list: user_reminders = reminders_list[user_id] new_user_reminders = [] for reminder in user_reminders: salt = reminder["salt"] date_of_reminder = utils.parse_date( reminder["date_of_reminder"]) if date_of_reminder < utils.now(): continue new_user_reminders.append(reminder) self.reminder_tasks[salt] = ScheduleManager.execute_delayed( (date_of_reminder - utils.now()).total_seconds(), self.execute_reminder, args=[salt, user_id, reminder], ) new_reminders_list[user_id] = new_user_reminders self.redis.set(f"{self.bot.bot_name}:remind-me-reminders", json.dumps(new_reminders_list)) def disable(self, bot): if not bot: return
class AdvancedAdminLog(BaseModule): ID = __name__.split(".")[-1] NAME = "AdvancedAdminLog" DESCRIPTION = "Logs Everything" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="ingore_channels", label="Channels to ignore for message edit/delete seperated by a space", type="text", placeholder="", default="", ), ModuleSetting( key="output_channel", label="Channels to send logs to", type="text", placeholder="", default="", ), ModuleSetting( key="level_to_query", label="Level required to query", type="number", placeholder=500, default=500, ), ModuleSetting( key="log_edit_message", label="Log Edit Message Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_delete_message", label="Log Delete Message Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_member_update", label="Log Member Update Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_member_update_nickname", label="Log Member Update Nickname Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_role_update", label="Log Role Update Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_role_create", label="Log Role Create Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_voice_change", label="Log Voice Change Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_member_join", label="Log Member Join Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_member_remove", label="Log Member Leave Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_channel_update", label="Log Channel Update Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_channel_create", label="Log Channel Create Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_channel_delete", label="Log Channel Delete Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_guild_update", label="Log Guild Update Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_emoji_update", label="Log Emoji Update Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_invite_create", label="Log Invite Create Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_invite_delete", label="Log Invite Delete Event", type="boolean", placeholder="", default=True, ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot async def message_delete(self, payload): if not self.settings["log_delete_message"]: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] message_id = payload.message_id with DBManager.create_session_scope() as db_session: db_message = Message._get(db_session, message_id) if not db_message: return content = json.loads(db_message.content) author_id = db_message.user_id sent_in_channel = self.bot.filters.get_channel([int(payload.channel_id)], None, {})[0] channels = ( self.settings["ingore_channels"].split(" ") if self.settings["ingore_channels"] != "" else [] ) if not sent_in_channel or (len(channels) > 0 and str(sent_in_channel.id) in channels): return author = self.bot.discord_bot.get_member(int(author_id)) if author == self.bot.discord_bot.client.user: return embed = discord.Embed( colour=await self.get_event_colour(author.guild, "message_delete"), timestamp=utils.now(), ) embed.add_field(name="Message", value=content[-1] if content[-1] else "None", inline=False) embed.add_field(name="Channel", value=sent_in_channel, inline=False) embed.add_field( name="ID", value=f"```Message ID: {message_id}\nUser ID: {author_id}\nChannel ID: {sent_in_channel.id}```", inline=False, ) action = discord.AuditLogAction.message_delete perp = None async for _log in self.bot.discord_bot.guild.audit_logs(limit=2, action=action): same_chan = _log.extra.channel.id == sent_in_channel.id if _log.target.id == int(author_id) and same_chan: perp = f"{_log.user}({_log.user.id})" break if perp: embed.add_field(name="Deleted by", value=perp, inline=False) embed.set_footer(text="User ID: " + str(author_id)) embed.set_author( name=f"{author} ({author.id})- Deleted Message", icon_url=str(author.avatar_url), ) await self.bot.say(out_channel, embed=embed) async def message_edit(self, payload): if not self.settings["log_edit_message"]: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] sent_in_channel = self.bot.filters.get_channel([int(payload.data["channel_id"])], None, {})[0] channels = ( self.settings["ingore_channels"].split(" ") if self.settings["ingore_channels"] != "" else [] ) if not sent_in_channel or (len(channels) > 0 and str(sent_in_channel.id) in channels): return message_id = payload.message_id guild_id = payload.data.get("guild_id", None) message = await sent_in_channel.fetch_message(int(message_id)) if not guild_id or self.bot.discord_bot.guild.id != int(guild_id): return with DBManager.create_session_scope() as db_session: db_message = Message._get(db_session, str(message_id)) if not db_message: return content = json.loads(db_message.content) author_id = db_message.user_id author = self.bot.discord_bot.get_member(int(author_id)) if int(author_id) == self.bot.discord_bot.client.user.id: return embed = discord.Embed( description=f"{author} updated their message in {sent_in_channel}", colour=await self.get_event_colour(author.guild, "message_edit"), timestamp=utils.now(), ) embed.add_field(name="Now:", value=f"{content[-1]}" if content[-1] else "None", inline=False) embed.add_field(name="Previous:", value=f"{content[-2]}" if content[-2] else "None", inline=False) embed.add_field( name="Channel:", value=f"{sent_in_channel.mention} ({sent_in_channel})\n[Jump to message]({message.jump_url})", inline=False, ) embed.add_field( name="ID", value=f"```User ID = {author.id}\nMessage ID = {message.id}\nChannel ID = {sent_in_channel.id}```", inline=False, ) embed.set_author( name=f"{author} ({author.id})", icon_url=str(author.avatar_url), ) await self.bot.say(out_channel, embed=embed) async def member_update(self, before, after): if not self.settings["log_member_update"]: return guild = before.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] embed = discord.Embed( colour=await self.get_event_colour(guild, "user_change"), timestamp=utils.now(), ) emb_msg = f"{before} ({before.id}) updated" embed.set_author(name=emb_msg, icon_url=before.avatar_url) member_updates = {"nick": "Nickname:", "roles": "Roles:"} perp = None reason = None worth_sending = False for attr, name in member_updates.items(): if attr == "nick" and not self.settings["log_member_update"]: continue before_attr = getattr(before, attr) after_attr = getattr(after, attr) if before_attr != after_attr: worth_sending = True if attr == "roles": b = set(before.roles) a = set(after.roles) before_roles = [list(b - a)][0] after_roles = [list(a - b)][0] if before_roles: for role in before_roles: embed.description = role.mention + " Role removed." if after_roles: for role in after_roles: embed.description = role.mention + " Role applied." action = discord.AuditLogAction.member_role_update async for _log in self.bot.discord_bot.guild.audit_logs( limit=5, action=action ): if _log.target.id == before.id: perp = _log.user if _log.reason: reason = _log.reason break else: action = discord.AuditLogAction.member_update async for _log in self.bot.discord_bot.guild.audit_logs( limit=5, action=action ): if _log.target.id == before.id: perp = _log.user if _log.reason: reason = _log.reason break embed.add_field( name="Before " + name, value=str(before_attr)[:1024], inline=False, ) embed.add_field( name="After " + name, value=str(after_attr)[:1024], inline=False ) if not worth_sending: return if perp: embed.add_field(name="Updated by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def role_update(self, before, after): if not self.settings["log_role_update"]: return guild = before.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] perp = None reason = None action = discord.AuditLogAction.role_update async for _log in guild.audit_logs(limit=5, action=action): if _log.target.id == before.id: perp = _log.user if _log.reason: reason = _log.reason break if perp == self.bot.discord_bot.client.user: return embed = discord.Embed( description=after.mention, colour=after.colour, timestamp=utils.now() ) if after is guild.default_role: embed.set_author(name="Updated @everyone role ") else: embed.set_author(name=f"Updated {before.name} ({before.id}) role ") if perp: embed.add_field(name="Updated by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) role_updates = { "name": "Name:", "color": "Colour:", "mentionable": "Mentionable:", "hoist": "Is Hoisted:", } worth_updating = False for attr, name in role_updates.items(): before_attr = getattr(before, attr) after_attr = getattr(after, attr) if before_attr != after_attr: worth_updating = True if before_attr == "": before_attr = "None" if after_attr == "": after_attr = "None" embed.add_field( name="Before " + name, value=str(before_attr), inline=False ) embed.add_field( name="After " + name, value=str(after_attr), inline=False ) p_msg = await self.get_role_permission_change(before, after) if p_msg != "": worth_updating = True embed.add_field(name="Permissions", value=p_msg[:1024], inline=False) if not worth_updating: return await self.bot.say(channel=out_channel, embed=embed) async def role_create(self, role): if not self.settings["log_role_create"]: return guild = role.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] perp = None reason = None action = discord.AuditLogAction.role_create async for _log in guild.audit_logs(limit=5, action=action): if _log.target.id == role.id: perp = _log.user if _log.reason: reason = _log.reason break embed = discord.Embed( description=role.mention, colour=role.colour, timestamp=utils.now(), ) embed.set_author(name=f"Role created {role.name} ({role.id})") if perp: embed.add_field(name="Created by", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def role_delete(self, role): if not self.settings["log_role_create"]: return guild = role.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] perp = None reason = None action = discord.AuditLogAction.role_create async for _log in guild.audit_logs(limit=5, action=action): if _log.target.id == role.id: perp = _log.user if _log.reason: reason = _log.reason break embed = discord.Embed( description=role.name, colour=role.colour, timestamp=utils.now(), ) embed.set_author(name=f"Role deleted {role.name} ({role.id})") if perp: embed.add_field(name="Deleted by", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def voice_change(self, member, before, after): if not self.settings["log_voice_change"]: return guild = member.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] embed = discord.Embed( timestamp=utils.now(), colour=await self.get_event_colour(guild, "voice_change"), ) embed.set_author(name=f"{member} ({member.id}) Voice State Update") change_type = None worth_updating = False if before.deaf != after.deaf: worth_updating = True change_type = "deaf" if after.deaf: embed.description = member.mention + " was deafened. " else: embed.description = member.mention + " was undeafened. " if before.mute != after.mute: worth_updating = True change_type = "mute" if after.mute: embed.description = member.mention + " was muted. " else: embed.description = member.mention + " was unmuted. " if before.channel != after.channel: worth_updating = True change_type = "channel" if before.channel is None: embed.description = member.mention + " has joined " + after.channel.name elif after.channel is None: embed.description = member.mention + " has left " + before.channel.name else: embed.description = ( member.mention + " has moved from " + before.channel.name + " to " + after.channel.name ) if not worth_updating: return perp = None reason = None action = discord.AuditLogAction.member_update async for _log in guild.audit_logs(limit=5, action=action): is_change = getattr(_log.after, change_type, None, {}) if _log.target.id == member.id and is_change: perp = _log.user if _log.reason: reason = _log.reason break if perp: embed.add_field(name="Updated by", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def member_join(self, member): if not self.settings["log_member_join"]: return guild = member.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] users = len(guild.members) created_at = member.created_at.replace(tzinfo=datetime.timezone.utc) since_created = (utils.now() - created_at).days user_created = created_at.strftime("%d %b %Y %H:%M") created_on = f"{user_created}\n({since_created} days ago)" embed = discord.Embed( description=f"@{member}", colour=await self.get_event_colour(guild, "user_join"), timestamp=member.joined_at if member.joined_at else utils.now(), ) embed.add_field(name="Total Users:", value=str(users), inline=False) embed.add_field(name="Account created on:", value=created_on, inline=False) embed.set_footer(text="User ID: " + str(member.id)) embed.set_author( name=f"{member} ({member.id}) has joined the guild", url=member.avatar_url, icon_url=member.avatar_url, ) embed.set_thumbnail(url=member.avatar_url) await self.bot.say(channel=out_channel, embed=embed) async def member_remove(self, member): if not self.settings["log_member_remove"]: return guild = member.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] embed = discord.Embed( description=f"@{member}", colour=await self.get_event_colour(guild, "user_left"), timestamp=utils.now(), ) perp = None reason = None banned = False async for _log in guild.audit_logs(limit=5): if _log.action not in [ discord.AuditLogAction.kick, discord.AuditLogAction.ban, ]: continue if _log.target.id == member.id: perp = _log.user reason = _log.reason break embed.add_field( name="Total Users:", value=str(len(guild.members)), inline=False ) if perp: embed.add_field( name="Kicked By" if not banned else "Banned By", value=perp.mention, inline=False, ) if reason: embed.add_field(name="Reason", value=str(reason), inline=False) embed.set_footer(text="User ID: " + str(member.id)) embed.set_author( name=f"{member} has left the guild", url=member.avatar_url, icon_url=member.avatar_url, ) embed.set_thumbnail(url=member.avatar_url) await self.bot.say(channel=out_channel, embed=embed) async def channel_update(self, before, after): if not self.settings["log_channel_update"]: return guild = before.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] channel_type = str(after.type).title() embed = discord.Embed( description=after.mention, timestamp=utils.now(), colour=await self.get_event_colour(guild, "channel_create"), ) embed.set_author( name=f"{channel_type} Channel Updated {before.name} ({before.id})" ) perp = None reason = None worth_updating = False actions = [ discord.AuditLogAction.channel_update, discord.AuditLogAction.overwrite_create, discord.AuditLogAction.overwrite_update, discord.AuditLogAction.overwrite_delete, ] async for _log in guild.audit_logs(limit=5): if _log.action not in actions: continue if _log.target.id == before.id: perp = _log.user if _log.reason: reason = _log.reason break if perp.id == self.bot.discord_bot.client.user.id: return if type(before) == discord.TextChannel: text_updates = { "name": "Name:", "topic": "Topic:", "category": "Category:", "slowmode_delay": "Slowmode delay:", } for attr, name in text_updates.items(): before_attr = getattr(before, attr) after_attr = getattr(after, attr) if before_attr != after_attr: worth_updating = True if before_attr == "": before_attr = "None" if after_attr == "": after_attr = "None" embed.add_field( name="Before " + name, value=str(before_attr)[:1024], inline=False, ) embed.add_field( name="After " + name, value=str(after_attr)[:1024], inline=False ) if before.is_nsfw() != after.is_nsfw(): worth_updating = True embed.add_field( name="Before " + "NSFW", value=str(before.is_nsfw()), inline=False ) embed.add_field( name="After " + "NSFW", value=str(after.is_nsfw()), inline=False ) p_msg = await self.get_permission_change(before, after) if p_msg != "": worth_updating = True embed.add_field(name="Permissions", value=p_msg[:1024], inline=False) if type(before) == discord.VoiceChannel: voice_updates = { "name": "Name:", "position": "Position:", "category": "Category:", "bitrate": "Bitrate:", "user_limit": "User limit:", } for attr, name in voice_updates.items(): before_attr = getattr(before, attr) after_attr = getattr(after, attr) if before_attr != after_attr: worth_updating = True embed.add_field( name="Before " + name, value=str(before_attr), inline=False ) embed.add_field( name="After " + name, value=str(after_attr), inline=False ) p_msg = await self.get_permission_change(before, after) if p_msg != "": worth_updating = True embed.add_field(name="Permissions", value=p_msg[:1024], inline=False) if perp: embed.add_field(name="Updated by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) if not worth_updating: return await self.bot.say(channel=out_channel, embed=embed) async def channel_create(self, channel): if not self.settings["log_channel_create"]: return guild = channel.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] channel_type = str(channel.type).title() embed = discord.Embed( description=f"{channel.mention} {channel.name}", timestamp=utils.now(), colour=await self.get_event_colour(guild, "channel_create"), ) embed.set_author( name=f"{channel_type} Channel Created {channel.name} ({channel.id})" ) perp = None reason = None action = discord.AuditLogAction.channel_create async for _log in guild.audit_logs(limit=5, action=action): if _log.target.id == channel.id: perp = _log.user if _log.reason: reason = _log.reason break embed.add_field(name="Type", value=channel_type, inline=False) if perp: embed.add_field(name="Created by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def channel_delete(self, channel): if not self.settings["log_channel_delete"]: return guild = channel.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] channel_type = str(channel.type).title() embed = discord.Embed( description=channel.name, timestamp=utils.now(), colour=await self.get_event_colour(guild, "channel_delete"), ) embed.set_author( name=f"{channel_type} Channel Deleted {channel.name} ({channel.id})" ) perp = None reason = None action = discord.AuditLogAction.channel_delete async for _log in guild.audit_logs(limit=5, action=action): if _log.target.id == channel.id: perp = _log.user if _log.reason: reason = _log.reason break embed.add_field(name="Type", value=channel_type, inline=False) if perp: embed.add_field(name="Deleted by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def guild_update(self, before, after): if not self.settings["log_guild_update"]: return if after != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] embed = discord.Embed( timestamp=utils.now(), colour=await self.get_event_colour(after, "guild_change"), ) embed.set_author(name="Updated Guild", icon_url=str(after.icon_url)) embed.set_thumbnail(url=str(after.icon_url)) guild_updates = { "name": "Name:", "region": "Region:", "afk_timeout": "AFK Timeout:", "afk_channel": "AFK Channel:", "icon_url": "Server Icon:", "owner": "Server Owner:", "splash": "Splash Image:", "system_channel": "Welcome message channel:", "verification_level": "Verification Level:", } worth_updating = False for attr, name in guild_updates.items(): before_attr = getattr(before, attr) after_attr = getattr(after, attr) if before_attr != after_attr: worth_updating = True embed.add_field( name="Before " + name, value=str(before_attr), inline=False ) embed.add_field( name="After " + name, value=str(after_attr), inline=False ) if not worth_updating: return perps = [] reasons = [] action = discord.AuditLogAction.guild_update async for _log in self.bot.discord_bot.guild.audit_logs( limit=int(len(embed.fields) / 2), action=action ): perps.append(_log.user) if _log.reason: reasons.append(_log.reason) if perps: embed.add_field( name="Updated by", value=", ".join(p.mention for p in perps), inline=False, ) if reasons: embed.add_field( name="Reasons ", value=", ".join(str(r) for r in reasons), inline=False ) await self.bot.say(channel=out_channel, embed=embed) async def emoji_update(self, guild, before, after): if not self.settings["log_emoji_update"]: return if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] perp = None time = datetime.datetime.utcnow() embed = discord.Embed( description="", timestamp=time, colour=await self.get_event_colour(guild, "emoji_change"), ) embed.set_author(name="Updated Server Emojis") worth_updating = False b = set(before) a = set(after) try: added_emoji = (a - b).pop() except KeyError: added_emoji = None try: removed_emoji = (b - a).pop() except KeyError: removed_emoji = None if added_emoji is not None: to_iter = before + (added_emoji,) else: to_iter = before changed_emoji = set((e, e.name, tuple(e.roles)) for e in after) changed_emoji.difference_update((e, e.name, tuple(e.roles)) for e in to_iter) try: changed_emoji = changed_emoji.pop()[0] except KeyError: changed_emoji = None else: for old_emoji in before: if old_emoji.id == changed_emoji.id: break else: changed_emoji = None action = None if removed_emoji is not None: worth_updating = True embed.description += ( f"`{removed_emoji}` (ID: {removed_emoji.id})" + " Removed from the guild\n" ) action = discord.AuditLogAction.emoji_delete elif added_emoji is not None: worth_updating = True embed.description += ( f"{added_emoji} `{added_emoji}`" + " Added to the guild\n" ) action = discord.AuditLogAction.emoji_create elif changed_emoji is not None: worth_updating = True new_msg = f"{changed_emoji} `{changed_emoji}`" if old_emoji.name != changed_emoji.name: new_msg += ( " Renamed from " + old_emoji.name + " to " + f"{changed_emoji.name}\n" ) action = discord.AuditLogAction.emoji_update embed.description += new_msg if old_emoji.roles != changed_emoji.roles: worth_updating = True if not changed_emoji.roles: new_msg = " Changed to unrestricted.\n" embed.description += new_msg elif not old_emoji.roles: embed.description += " Restricted to roles: " + " ".join( [role.mention for role in changed_emoji.roles] ) else: embed.description += ( " Role restriction changed from " + " ".join([role.mention for role in old_emoji.roles]) + " to " + " ".join([role.mention for role in changed_emoji.roles]) ) perp = None reason = None if not worth_updating: return if action: async for _log in guild.audit_logs(limit=1, action=action): perp = _log.user if _log.reason: reason = _log.reason break if perp: embed.add_field(name="Updated by ", value=perp.mention, inline=False) if reason: embed.add_field(name="Reason ", value=reason, inline=False) await self.bot.say(channel=out_channel, embed=embed) async def invite_create(self, invite): if not self.settings["log_invite_create"]: return guild = invite.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] invite_attrs = { "code": "Code:", "inviter": "Inviter:", "channel": "Channel:", "max_uses": "Max Uses:", } embed = discord.Embed( title="Invite Created", colour=await self.get_event_colour(guild, "invite_created"), ) worth_updating = False for attr, name in invite_attrs.items(): before_attr = getattr(invite, attr) if before_attr: worth_updating = True embed.add_field(name=name, value=str(before_attr), inline=False) if not worth_updating: return await self.bot.say(channel=out_channel, embed=embed) async def invite_delete(self, invite): if not self.settings["log_invite_delete"]: return guild = invite.guild if guild != self.bot.discord_bot.guild: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )-[0] invite_attrs = { "code": "Code: ", "inviter": "Inviter: ", "channel": "Channel: ", "max_uses": "Max Uses: ", "uses": "Used: ", } embed = discord.Embed( title="Invite Deleted", colour=await self.get_event_colour(guild, "invite_deleted"), ) worth_updating = False for attr, name in invite_attrs.items(): before_attr = getattr(invite, attr) if before_attr: worth_updating = True embed.add_field(name=name, value=str(before_attr), inline=False) if not worth_updating: return await self.bot.say(channel=out_channel, embed=embed) async def custom_event_log(self, message=None, embed=None): if message is None and embed is None: return out_channel = self.bot.filters.get_channel( [int(self.settings["output_channel"])], None, {} )[0] await self.bot.say(channel=out_channel, message=message, embed=embed) async def get_permission_change(self, before, after): p_msg = "" before_perms = {} after_perms = {} for o, p in before.overwrites.items(): before_perms[str(o.id)] = [i for i in p] for o, p in after.overwrites.items(): after_perms[str(o.id)] = [i for i in p] for entity in before_perms: entity_obj = before.guild.get_role(int(entity)) if not entity_obj: entity_obj = before.guild.get_member(int(entity)) if entity not in after_perms: p_msg += f"{entity_obj.mention} Overwrites removed.\n" continue if after_perms[entity] != before_perms[entity]: a = set(after_perms[entity]) b = set(before_perms[entity]) a_perms = list(a - b) for diff in a_perms: p_msg += f"{entity_obj.mention} {diff[0]} Set to {diff[1]}\n" for entity in after_perms: entity_obj = after.guild.get_role(int(entity)) if not entity_obj: entity_obj = after.guild.get_member(int(entity)) if entity not in before_perms: p_msg += f"{entity_obj.mention} Overwrites added.\n" continue return p_msg async def get_role_permission_change(self, before, after): permission_list = [ "create_instant_invite", "kick_members", "ban_members", "administrator", "manage_channels", "manage_guild", "add_reactions", "view_audit_log", "priority_speaker", "read_messages", "send_messages", "send_tts_messages", "manage_messages", "embed_links", "attach_files", "read_message_history", "mention_everyone", "external_emojis", "connect", "speak", "mute_members", "deafen_members", "move_members", "use_voice_activation", "change_nickname", "manage_nicknames", "manage_roles", "manage_webhooks", "manage_emojis", ] p_msg = "" for p in permission_list: if getattr(before.permissions, p) != getattr(after.permissions, p): change = getattr(after.permissions, p) p_msg += f"{p} Set to {change}\n" return p_msg async def get_event_colour(self, guild, event_type, changed_object=None): if guild.text_channels: cmd_colour = discord.Colour.blue() else: cmd_colour = discord.Colour.red() defaults = { "message_edit": discord.Colour.orange(), "message_delete": discord.Colour.dark_red(), "user_change": discord.Colour.greyple(), "role_change": changed_object.colour if changed_object else discord.Colour.blue(), "role_create": discord.Colour.blue(), "role_delete": discord.Colour.dark_blue(), "voice_change": discord.Colour.magenta(), "user_join": discord.Colour.green(), "user_left": discord.Colour.dark_green(), "channel_change": discord.Colour.teal(), "channel_create": discord.Colour.teal(), "channel_delete": discord.Colour.dark_teal(), "guild_change": discord.Colour.blurple(), "emoji_change": discord.Colour.gold(), "commands_used": cmd_colour, "invite_created": discord.Colour.blurple(), "invite_deleted": discord.Colour.blurple(), } colour = defaults[event_type] return colour def load_commands(self, **options): self.commands["message"] = Command.raw_command( self.querymessage, level=int(self.settings["level_to_query"]), can_execute_with_whisper=True, description="Queries a message", ) async def querymessage(self, bot, author, channel, message, args): embed = discord.Embed( colour=await self.get_event_colour(author.guild, "message_edit"), timestamp=utils.now(), ) embed.set_author(name=f"Message Query Result",) _args = message.split(" ") if message != "" else [] if not _args: embed.description = f"Invalid Message ID" await self.bot.say(channel=channel, embed=embed) return message_id = _args[0] with DBManager.create_session_scope() as db_session: db_message = Message._get(db_session, str(message_id)) if db_message: content = json.loads(db_message.content) author_id = db_message.user_id channel_id = db_message.channel_id if not db_message: embed.description = f"Message not found with message id {message_id}" await self.bot.say(channel=channel, embed=embed) return sent_in_channel = self.bot.filters.get_channel([int(channel_id)], None, {})[0] try: message = await sent_in_channel.fetch_message(int(message_id)) except (discord.NotFound, discord.Forbidden, discord.HTTPException): message = None embed.add_field(name="Message History:", value="\n".join(content), inline=False) jump_url = ( f"[Jump to message]({message.jump_url})" if message else "Message was deleted!" ) embed.add_field( name="Channel:", value=f"{sent_in_channel.mention} ({sent_in_channel})\n{jump_url}", inline=False, ) embed.add_field( name="ID", value=f"```User ID = {author_id}\nMessage ID = {message_id}\nChannel ID = {channel_id}```", inline=False, ) await self.bot.say(channel=channel, embed=embed) def enable(self, bot): if not bot: return HandlerManager.add_handler("discord_raw_message_edit", self.message_edit) HandlerManager.add_handler("discord_raw_message_delete", self.message_delete) HandlerManager.add_handler("discord_member_update", self.member_update) HandlerManager.add_handler("discord_guild_role_update", self.role_update) HandlerManager.add_handler("discord_guild_role_create", self.role_create) HandlerManager.add_handler("discord_guild_role_delete", self.role_delete) HandlerManager.add_handler("discord_voice_state_update", self.voice_change) HandlerManager.add_handler("discord_member_remove", self.member_remove) HandlerManager.add_handler("discord_member_join", self.member_join) HandlerManager.add_handler("discord_guild_channel_update", self.channel_update) HandlerManager.add_handler("discord_guild_channel_create", self.channel_create) HandlerManager.add_handler("discord_guild_channel_delete", self.channel_delete) HandlerManager.add_handler("discord_guild_update", self.guild_update) HandlerManager.add_handler("discord_guild_emojis_update", self.emoji_update) HandlerManager.add_handler("discord_invite_create", self.invite_create) HandlerManager.add_handler("discord_invite_delete", self.invite_delete) HandlerManager.add_handler("aml_custom_log", self.custom_event_log) def disable(self, bot): if not bot: return HandlerManager.remove_handler("discord_raw_message_edit", self.message_edit) HandlerManager.remove_handler("discord_raw_message_delete", self.message_delete) HandlerManager.remove_handler("discord_member_update", self.member_update) HandlerManager.remove_handler("discord_guild_role_update", self.role_update) HandlerManager.remove_handler("discord_guild_role_create", self.role_create) HandlerManager.remove_handler("discord_guild_role_delete", self.role_delete) HandlerManager.remove_handler("discord_voice_state_update", self.voice_change) HandlerManager.remove_handler("discord_member_remove", self.member_remove) HandlerManager.remove_handler("discord_member_join", self.member_join) HandlerManager.remove_handler( "discord_guild_channel_update", self.channel_update ) HandlerManager.remove_handler( "discord_guild_channel_create", self.channel_create ) HandlerManager.remove_handler("discord_guild_update", self.guild_update) HandlerManager.remove_handler("discord_guild_emojis_update", self.emoji_update) HandlerManager.remove_handler("discord_invite_create", self.invite_create) HandlerManager.remove_handler("discord_invite_delete", self.invite_delete) HandlerManager.remove_handler("aml_custom_log", self.custom_event_log)
class LinkCheckerModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Link Checker" DESCRIPTION = "Checks links if they're bad" ENABLED_DEFAULT = True CATEGORY = "Filter" SETTINGS = [ ModuleSetting( key="ban_pleb_links", label="Disallow links from non-subscribers", type="boolean", required=True, default=False, ), ModuleSetting( key="ban_sub_links", label="Disallow links from subscribers", type="boolean", required=True, default=False, ), ModuleSetting( key="timeout_length", label="Timeout length", type="number", required=True, placeholder="Timeout length in seconds", default=60, constraints={"min_value": 1, "max_value": 3600}, ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=500, constraints={"min_value": 100, "max_value": 1000}, ), ] def __init__(self, bot): super().__init__(bot) self.db_session = None self.links = {} self.blacklisted_links = [] self.whitelisted_links = [] self.cache = ( LinkCheckerCache() ) # cache[url] = True means url is safe, False means the link is bad self.safe_browsing_api = None def enable(self, bot): if not bot: return HandlerManager.add_handler("on_message", self.on_message, priority=100) HandlerManager.add_handler("on_commit", self.on_commit) if self.db_session is not None: self.db_session.commit() self.db_session.close() self.db_session = None self.db_session = DBManager.create_session() self.blacklisted_links = [] for link in self.db_session.query(BlacklistedLink): self.blacklisted_links.append(link) self.whitelisted_links = [] for link in self.db_session.query(WhitelistedLink): self.whitelisted_links.append(link) def disable(self, bot): if not bot: return greenbot.managers.handler.HandlerManager.remove_handler( "on_message", self.on_message ) greenbot.managers.handler.HandlerManager.remove_handler( "on_commit", self.on_commit ) if self.db_session is not None: self.db_session.commit() self.db_session.close() self.db_session = None self.blacklisted_links = [] self.whitelisted_links = [] def reload(self): log.info( f"Loaded {len(self.blacklisted_links)} bad links and {len(self.whitelisted_links)} good links" ) return self super_whitelist = [] def on_message(self, source, whisper, urls, **rest): if whisper: return if source.level >= self.settings["bypass_level"] or source.moderator is True: return if len(urls) > 0: do_timeout = False ban_reason = "You are not allowed to post links in chat" whisper_reason = "??? KKona" if self.settings["ban_pleb_links"] is True and source.subscriber is False: do_timeout = True whisper_reason = "You cannot post non-verified links in chat if you're not a subscriber." elif self.settings["ban_sub_links"] is True and source.subscriber is True: do_timeout = True whisper_reason = ( "You cannot post non-verified links in chat if you're a subscriber" ) if do_timeout is True: # Check if the links are in our super-whitelist. for url in urls: parsed_url = Url(url) if len(parsed_url.parsed.netloc.split(".")) < 2: continue whitelisted = False for whitelist in self.super_whitelist: if is_subdomain(parsed_url.parsed.netloc, whitelist): whitelisted = True break if whitelisted is False and self.is_whitelisted(url): whitelisted = True if whitelisted is False: self.bot.timeout( source, self.settings["timeout_length"], reason=ban_reason ) if source.time_in_chat_online >= timedelta(hours=1): self.bot.whisper(source, whisper_reason) return False for url in urls: # Action which will be taken when a bad link is found def action(): self.bot.timeout( source, self.settings["timeout_length"], reason="Banned link" ) # First we perform a basic check if self.simple_check(url, action) == self.RET_FURTHER_ANALYSIS: # If the basic check returns no relevant data, we queue up a proper check on the URL self.bot.action_queue.submit(self.check_url, url, action) def on_commit(self, **rest): if self.db_session is not None: self.db_session.commit() def delete_from_cache(self, url): if url in self.cache: del self.cache[url] def cache_url(self, url, safe): if url in self.cache and self.cache[url] == safe: return self.cache[url] = safe self.bot.execute_delayed(20, self.delete_from_cache, url) def counteract_bad_url( self, url, action=None, want_to_cache=True, want_to_blacklist=False ): log.debug(f"LinkChecker: BAD URL FOUND {url.url}") if action: action() if want_to_cache: self.cache_url(url.url, False) if want_to_blacklist: self.blacklist_url(url.url, url.parsed) return True def blacklist_url(self, url, parsed_url=None, level=0): if not ( url.lower().startswith("http://") or url.lower().startswith("https://") ): url = "http://" + url if parsed_url is None: parsed_url = urllib.parse.urlparse(url) if self.is_blacklisted(url, parsed_url): return False domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if domain.startswith("www."): domain = domain[4:] if path.endswith("/"): path = path[:-1] if path == "": path = "/" link = BlacklistedLink(domain, path, level) self.db_session.add(link) self.blacklisted_links.append(link) self.db_session.commit() def whitelist_url(self, url, parsed_url=None): if not ( url.lower().startswith("http://") or url.lower().startswith("https://") ): url = "http://" + url if parsed_url is None: parsed_url = urllib.parse.urlparse(url) if self.is_whitelisted(url, parsed_url): return domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if domain.startswith("www."): domain = domain[4:] if path.endswith("/"): path = path[:-1] if path == "": path = "/" link = WhitelistedLink(domain, path) self.db_session.add(link) self.whitelisted_links.append(link) self.db_session.commit() def is_blacklisted(self, url, parsed_url=None, sublink=False): if parsed_url is None: parsed_url = urllib.parse.urlparse(url) domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if path == "": path = "/" domain_split = domain.split(".") if len(domain_split) < 2: return False for link in self.blacklisted_links: if link.is_subdomain(domain): if link.is_subpath(path): if not sublink: return True elif ( link.level >= 1 ): # if it's a sublink, but the blacklisting level is 0, we don't consider it blacklisted return True return False def is_whitelisted(self, url, parsed_url=None): if parsed_url is None: parsed_url = urllib.parse.urlparse(url) domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if path == "": path = "/" domain_split = domain.split(".") if len(domain_split) < 2: return False for link in self.whitelisted_links: if link.is_subdomain(domain): if link.is_subpath(path): return True return False RET_BAD_LINK = -1 RET_FURTHER_ANALYSIS = 0 RET_GOOD_LINK = 1 def basic_check(self, url, action, sublink=False): """ Check if the url is in the cache, or if it's Return values: 1 = Link is OK -1 = Link is bad 0 = Link needs further analysis """ if url.url in self.cache: if not self.cache[url.url]: # link is bad self.counteract_bad_url(url, action, False, False) return self.RET_BAD_LINK return self.RET_GOOD_LINK if self.is_blacklisted(url.url, url.parsed, sublink): self.counteract_bad_url(url, action, want_to_blacklist=False) return self.RET_BAD_LINK if self.is_whitelisted(url.url, url.parsed): self.cache_url(url.url, True) return self.RET_GOOD_LINK return self.RET_FURTHER_ANALYSIS def simple_check(self, url, action): url = Url(url) if len(url.parsed.netloc.split(".")) < 2: # The URL is broken, ignore it return self.RET_FURTHER_ANALYSIS return self.basic_check(url, action) def check_url(self, url, action): url = Url(url) if len(url.parsed.netloc.split(".")) < 2: # The URL is broken, ignore it return try: self._check_url(url, action) except: log.exception("LinkChecker unhandled exception while _check_url") def _check_url(self, url, action): # XXX: The basic check is currently performed twice on links found in messages. Solve res = self.basic_check(url, action) if res == self.RET_GOOD_LINK: return elif res == self.RET_BAD_LINK: return connection_timeout = 2 read_timeout = 1 try: r = requests.head( url.url, allow_redirects=True, timeout=connection_timeout, headers={"User-Agent": self.bot.user_agent}, ) except: self.cache_url(url.url, True) return checkcontenttype = ( "content-type" in r.headers and r.headers["content-type"] == "application/octet-stream" ) checkdispotype = ( "disposition-type" in r.headers and r.headers["disposition-type"] == "attachment" ) if checkcontenttype or checkdispotype: # triggering a download not allowed self.counteract_bad_url(url, action) return redirected_url = Url(r.url) if is_same_url(url, redirected_url) is False: res = self.basic_check(redirected_url, action) if res == self.RET_GOOD_LINK: return elif res == self.RET_BAD_LINK: return if self.safe_browsing_api and self.safe_browsing_api.is_url_bad( redirected_url.url ): # harmful url detected log.debug("Google Safe Browsing API lists URL") self.counteract_bad_url(url, action, want_to_blacklist=False) self.counteract_bad_url(redirected_url, want_to_blacklist=False) return if "content-type" not in r.headers or not r.headers["content-type"].startswith( "text/html" ): return # can't analyze non-html content maximum_size = 1024 * 1024 * 10 # 10 MB receive_timeout = 3 html = "" try: response = requests.get( url=url.url, stream=True, timeout=(connection_timeout, read_timeout), headers={"User-Agent": self.bot.user_agent}, ) content_length = response.headers.get("Content-Length") if ( content_length and int(response.headers.get("Content-Length")) > maximum_size ): log.error("This file is too big!") return size = 0 start = pajbot.utils.now().timestamp() for chunk in response.iter_content(1024): if pajbot.utils.now().timestamp() - start > receive_timeout: log.error("The site took too long to load") return size += len(chunk) if size > maximum_size: log.error("This file is too big! (fake header)") return html += str(chunk) except requests.exceptions.ConnectTimeout: log.warning(f"Connection timed out while checking {url.url}") self.cache_url(url.url, True) return except requests.exceptions.ReadTimeout: log.warning(f"Reading timed out while checking {url.url}") self.cache_url(url.url, True) return except: log.exception("Unhandled exception") return try: soup = BeautifulSoup(html, "html.parser") except: return original_url = url original_redirected_url = redirected_url urls = [] for link in soup.find_all("a"): # get a list of links to external sites url = link.get("href") if url is None: continue if url.startswith("//"): urls.append("http:" + url) elif url.startswith("http://") or url.startswith("https://"): urls.append(url) for url in urls: # check if the site links to anything dangerous url = Url(url) if is_subdomain(url.parsed.netloc, original_url.parsed.netloc): # log.debug('Skipping because internal link') continue res = self.basic_check(url, action, sublink=True) if res == self.RET_BAD_LINK: self.counteract_bad_url(url) self.counteract_bad_url(original_url, want_to_blacklist=False) self.counteract_bad_url( original_redirected_url, want_to_blacklist=False ) return elif res == self.RET_GOOD_LINK: continue try: r = requests.head( url.url, allow_redirects=True, timeout=connection_timeout, headers={"User-Agent": self.bot.user_agent}, ) except: continue redirected_url = Url(r.url) if not is_same_url(url, redirected_url): res = self.basic_check(redirected_url, action, sublink=True) if res == self.RET_BAD_LINK: self.counteract_bad_url(url) self.counteract_bad_url(original_url, want_to_blacklist=False) self.counteract_bad_url( original_redirected_url, want_to_blacklist=False ) return elif res == self.RET_GOOD_LINK: continue if self.safe_browsing_api and self.safe_browsing_api.is_url_bad( redirected_url.url ): # harmful url detected log.debug(f"Evil sublink {url} by google API") self.counteract_bad_url(original_url, action) self.counteract_bad_url(original_redirected_url) self.counteract_bad_url(url) self.counteract_bad_url(redirected_url) return # if we got here, the site is clean for our standards self.cache_url(original_url.url, True) self.cache_url(original_redirected_url.url, True) return def load_commands(self, **options): self.commands["add"] = Command.multiaction_command( level=100, delay_all=0, delay_user=0, default=None, command="add", commands={ "link": Command.multiaction_command( command="add link", level=500, delay_all=0, delay_user=0, default=None, commands={ "blacklist": Command.raw_command( self.add_link_blacklist, command="add link blacklist", level=500, delay_all=0, delay_user=0, description="Blacklist a link", examples=[ CommandExample( None, "Add a link to the blacklist for a shallow search", chat="user:!add link blacklist --shallow scamlink.lonk/\n" "bot>user:Successfully added your links", description="Added the link scamlink.lonk/ to the blacklist for a shallow search", ).parse(), CommandExample( None, "Add a link to the blacklist for a deep search", chat="user:!add link blacklist --deep scamlink.lonk/\n" "bot>user:Successfully added your links", description="Added the link scamlink.lonk/ to the blacklist for a deep search", ).parse(), ], ), "whitelist": Command.raw_command( self.add_link_whitelist, command="add link whitelist", level=500, delay_all=0, delay_user=0, description="Whitelist a link", examples=[ CommandExample( None, "Add a link to the whitelist", chat="user:!add link whitelink safelink.lonk/\n" "bot>user:Successfully added your links", description="Added the link safelink.lonk/ to the whitelist", ).parse() ], ), }, ) }, ) self.commands["remove"] = Command.multiaction_command( level=100, delay_all=0, delay_user=0, default=None, command="remove", commands={ "link": Command.multiaction_command( command="remove link", level=500, delay_all=0, delay_user=0, default=None, commands={ "blacklist": Command.raw_command( self.remove_link_blacklist, command="remove link blacklist", level=500, delay_all=0, delay_user=0, description="Remove a link from the blacklist.", examples=[ CommandExample( None, "Remove a link from the blacklist.", chat="user:!remove link blacklist 20\n" "bot>user:Successfully removed blacklisted link with id 20", description="Remove a link from the blacklist with an ID", ).parse() ], ), "whitelist": Command.raw_command( self.remove_link_whitelist, command="remove link whitelist", level=500, delay_all=0, delay_user=0, description="Remove a link from the whitelist.", examples=[ CommandExample( None, "Remove a link from the whitelist.", chat="user:!remove link whitelist 12\n" "bot>user:Successfully removed blacklisted link with id 12", description="Remove a link from the whitelist with an ID", ).parse() ], ), }, ) }, ) def add_link_blacklist(self, bot, source, message, **rest): options, new_links = self.parse_link_blacklist_arguments(message) if new_links: parts = new_links.split(" ") try: for link in parts: if len(link) > 1: self.blacklist_url(link, **options) AdminLogManager.post( "Blacklist link added", source.discord_id, link ) bot.whisper(source, "Successfully added your links") return True except: log.exception("Unhandled exception in add_link_blacklist") bot.whisper(source, "Some error occurred while adding your links") return False else: bot.whisper(source, "Usage: !add link blacklist LINK") return False def add_link_whitelist(self, bot, source, message, **rest): parts = message.split(" ") try: for link in parts: self.whitelist_url(link) AdminLogManager.post("Whitelist link added", source.discord_id, link) except: log.exception("Unhandled exception in add_link") bot.whisper(source, "Some error occurred white adding your links") return False bot.whisper(source, "Successfully added your links") def remove_link_blacklist(self, bot, source, message, **rest): if not message: bot.whisper(source, "Usage: !remove link blacklist ID") return False id = None try: id = int(message) except ValueError: pass link = self.db_session.query(BlacklistedLink).filter_by(id=id).one_or_none() if link: self.blacklisted_links.remove(link) self.db_session.delete(link) self.db_session.commit() else: bot.whisper(source, "No link with the given id found") return False AdminLogManager.post("Blacklist link removed", source.discord_id, link.domain) bot.whisper(source, f"Successfully removed blacklisted link with id {link.id}") def remove_link_whitelist(self, bot, source, message, **rest): if not message: bot.whisper(source, "Usage: !remove link whitelist ID") return False id = None try: id = int(message) except ValueError: pass link = self.db_session.query(WhitelistedLink).filter_by(id=id).one_or_none() if link: self.whitelisted_links.remove(link) self.db_session.delete(link) self.db_session.commit() else: bot.whisper(source, "No link with the given id found") return False AdminLogManager.post("Whitelist link removed", source.discord_id, link.domain) bot.whisper(source, f"Successfully removed whitelisted link with id {link.id}") @staticmethod def parse_link_blacklist_arguments(message): parser = argparse.ArgumentParser() parser.add_argument("--deep", dest="level", action="store_true") parser.add_argument("--shallow", dest="level", action="store_false") parser.set_defaults(level=False) try: args, unknown = parser.parse_known_args(message.split()) except SystemExit: return False, False except: log.exception("Unhandled exception in add_link_blacklist") return False, False # Strip options of any values that are set as None options = {k: v for k, v in vars(args).items() if v is not None} response = " ".join(unknown) if "level" in options: options["level"] = int(options["level"]) return options, response
class RoleToLevel(BaseModule): ID = __name__.split(".")[-1] NAME = "RoleToLevel" DESCRIPTION = "Gives level based on roles in discord" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="level", label="Level required to manage roles", type="number", placeholder=1500, default=1500, ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.redis = RedisManager.get() if self.bot: self.bot.roles = {} async def add_role_level(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] role = self.bot.filters.get_role([command_args[0]], None, {})[0] if not role: await self.bot.say(channel, f"Invalid role id {command_args[0]}") return try: self.bot.roles = json.loads( self.redis.get(f"{self.bot.bot_name}:role-level") ) except: self.redis.set(f"{self.bot.bot_name}:role-level", json.dumps({})) self.bot.roles = {} if str(role.id) in self.bot.roles: await self.bot.say( channel, f"Level, {self.bot.roles.get(str(role.id))} has already been assigned to a {role.mention}", ) return level = int(command_args[1]) if level >= args["user_level"]: await self.bot.say( channel, f"You cant set a level higher then your current level" ) return self.bot.roles[str(role.id)] = level self.redis.set(f"{self.bot.bot_name}:role-level", json.dumps(self.bot.roles)) await self.bot.say( channel, f"Level, {command_args[1]} assigned to role, {role.mention}" ) async def remove_role_level(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] if command_args[0] in self.bot.roles: del self.bot.roles[str(command_args[0])] self.redis.set( f"{self.bot.bot_name}:role-level", json.dumps(self.bot.roles) ) await self.bot.say(channel, f"Removed role with id {command_args[0]}") return role = self.bot.filters.get_role([command_args[0]], None, {})[0] if not role: await self.bot.say(channel, f"Invalid role id {command_args[0]}") return try: self.bot.roles = json.loads( self.redis.get(f"{self.bot.bot_name}:role-level") ) except: self.redis.set(f"{self.bot.bot_name}:role-level", json.dumps({})) self.bot.roles = {} if str(role.id) not in self.bot.roles: await self.bot.say(channel, f"{role.mention} doesnt have a level assigned") return del self.bot.roles[str(role.id)] self.redis.set(f"{self.bot.bot_name}:role-level", json.dumps(self.bot.roles)) await self.bot.say(channel, f"{role.mention} no longer has a level") def load_commands(self, **options): self.commands["addrolelevel"] = Command.raw_command( self.add_role_level, command="addrolelevel", delay_all=0, delay_user=0, level=int(self.settings["level"]), can_execute_with_whisper=True, description="Adds Level to role", ) self.commands["removerolelevel"] = Command.raw_command( self.remove_role_level, command="removerolelevel", delay_all=0, delay_user=0, level=int(self.settings["level"]), can_execute_with_whisper=True, description="Removes Level from role", ) def enable(self, bot): if not bot: return try: self.bot.roles = json.loads( self.redis.get(f"{self.bot.bot_name}:role-level") ) except: self.redis.set(f"{self.bot.bot_name}:role-level", json.dumps({})) self.bot.roles = {} def disable(self, bot): if not bot: return self.bot.roles = {}
class ActivityTracker(BaseModule): ID = __name__.split(".")[-1] NAME = "RemindMe" DESCRIPTION = "Allows users to create reminders" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="max_reminders_per_user", label="Maximum reminders per user", type="int", placeholder="", default="3", ), ModuleSetting( key="cost", label="Points required to add a reminder", type="number", placeholder="", default="0", ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.redis = RedisManager.get() self.reminder_tasks = {} def create_reminder(self, bot, author, channel, message, args): pass def myreminders(self, bot, author, channel, message, args): pass def forgetme(self, bot, author, channel, message, args): pass def load_commands(self, **options): self.commands["remindme"] = Command.raw_command( self.create_reminder, delay_all=0, delay_user=0, cost=self.settings["cost"], can_execute_with_whisper=False, description="Creates a reminder", ) self.commands["myreminders"] = Command.raw_command( self.myreminders, delay_all=0, delay_user=0, cost=self.settings["cost"], description="Creates a reminder", ) self.commands["forgetme"] = Command.raw_command( self.forgetme, delay_all=0, delay_user=0, can_execute_with_whisper=False, description="Creates a reminder", ) def execute_reminder(self, salt, reminder): self.reminder_tasks.pop(salt) def enable(self, bot): if not bot: return try: reminders_list = json.loads(self.redis.get("remind-me-reminders")) """ { user_id: { "message_id": message_id, "message": message, "date_of_reminder": date_of_reminder, }, } """ except: self.redis.set("remind-me-reminders", json.dumps({})) reminders_list = {} new_reminders_list = {} for user in reminders_list: user_reminders = reminders_list[user] new_user_reminders = [] for reminder in user_reminders: salt = random_string() date_of_reminder = reminder["date_of_reminder"] if ":" in date_of_reminder[-5:]: date_of_reminder = f"{date_of_reminder[:-5]}{date_of_reminder[-5:-3]}{date_of_reminder[-2:]}" date_of_reminder = datetime.strptime(date_of_reminder, "%Y-%m-%d %H:%M:%S.%f%z") if date_of_reminder < utils.now(): continue new_user_reminders.append(reminder) self.reminder_tasks[salt] = ScheduleManager.execute_delayed((date_of_reminder-utils.now()).seconds, self.execute_reminder, args=[salt, reminder]) def disable(self, bot): if not bot: return
class ActivityTracker(BaseModule): ID = __name__.split(".")[-1] NAME = "ActivityTracker" DESCRIPTION = "Gives points for being active" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="sub_role_id", label="ID of the sub role", type="text", placeholder="", default="", ), ModuleSetting( key="regular_role_id", label="ID of the regular role", type="text", placeholder="", default="", ), ModuleSetting( key="channels_to_listen_in", label="Channel IDs to listen in seperated by a ' '", type="text", placeholder="", default="", ), ModuleSetting( key="hourly_credit", label= "If he wrote enough messages this hour, the hour will be credited with this many points", type="number", placeholder="", default="8", ), ModuleSetting( key="daily_max_msgs", label="Maximum hours(msgs) that can be credited per day", type="number", placeholder="", default="12", ), ModuleSetting( key="daily_limit", label="If user reached daily max msgs he will get this many points", type="number", placeholder="", default="10", ), ModuleSetting( key="min_msgs_per_week", label= "If user can't keep up this many credited hours per week he loses the role", type="number", placeholder="", default="10", ), ModuleSetting( key="min_regular_points", label="Points required for the regular role", type="number", placeholder="", default="10", ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.process_messages_job = None async def process_messages(self): with DBManager.create_session_scope() as db_session: regular_role = self.bot.filters.get_role( [self.settings["regular_role_id"]], None, {})[0] sub_role = self.bot.filters.get_role( [self.settings["sub_role_id"]], None, {})[0] counts_by_week = Message._get_week_count_by_user(db_session) for member in regular_role.members: count = counts_by_week.get(str(member.id), 0) if (count < self.settings["min_msgs_per_week"] or sub_role not in member.roles): await self.bot.remove_role( member, regular_role, "They failed to meet the requirements to keep the role" ) channels_to_listen_in = ( self.settings["channels_to_listen_in"].split(" ") if len(self.settings["channels_to_listen_in"]) != 0 else None) messages = Message._get_last_hour(db_session, channels_to_listen_in) counts_by_day = Message._get_day_count_by_user(db_session) for message in messages: count = counts_by_day.get(message.user_id, 0) if message.user_id != str(self.bot.bot_id): if count < self.settings["daily_max_msgs"] - 1: message.user.points += self.settings["hourly_credit"] elif count == self.settings["daily_max_msgs"] - 1: message.user.points += self.settings["daily_limit"] message.credited = True counts_by_day[message.user_id] = count + 1 for user in User._get_users_with_points( db_session, self.settings["min_regular_points"]): member = self.bot.filters.get_member([user.discord_id], None, {})[0] if not member: continue count = counts_by_week.get(str(member.id), 0) if (sub_role not in member.roles or (regular_role in member.roles or count < self.settings["min_msgs_per_week"])): continue await self.bot.add_role( member, regular_role, "They met the requirements to get the role") def enable(self, bot): if not bot: return self.process_messages_job = ScheduleManager.execute_every( 3600, self.process_messages) # Checks every hour def disable(self, bot): if not bot: return self.process_messages_job.remove() self.process_messages_job = None
class Twitter(BaseModule): ID = __name__.split(".")[-1] NAME = "Twitter" DESCRIPTION = "Fetches Tweets from users and displays them" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="users", label="Users to follow", type="text", placeholder="", default="", ), ModuleSetting( key="output_format", label="Format for output, where {username} is the username and {tweet_url} is the url of the tweet", type="text", placeholder="@here {username} just tweeted {tweet_url}", default="@here {username} just tweeted {tweet_url}", ), ModuleSetting( key="output_channel", label="Channel ID to output to", type="text", placeholder="", default="", ), ModuleSetting( key="send_replies", label="Log Replies to Tweets", type="boolean", placeholder="", default=True, ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.redis = RedisManager.get() self.stream = None self.process = None if not self.bot: return async def on_status(self, tweet): try: if tweet.in_reply_to_status_id is not None: if not self.settings["send_replies"]: return username = tweet.author.screen_name tweet_url = f"https://twitter.com/{username}/status/{tweet.id}" out_channel = self.bot.filters.get_channel([int(self.settings["output_channel"])], None, {})[0] message = self.settings["output_format"].format( username=username, tweet_url=tweet_url ) await self.bot.say(channel=out_channel, message=message, ignore_escape=True) except Exception as e: log.error(e) def load_commands(self, **options): if not self.bot: return ScheduleManager.execute_now(self.update_manager) async def update_manager(self): await HandlerManager.trigger("twitter_follows", usernames=self.settings["users"].split(" ") if self.settings["users"] else []) def get_users_to_follow(self, usernames): return [ str(self.bot.twitter_manager.api.get_user(username).id) for username in usernames ] def enable(self, bot): if not bot: return HandlerManager.add_handler("twitter_on_status", self.on_status) def disable(self, bot): if not bot: return HandlerManager.remove_handler("twitter_on_status", self.on_status)
class Memes(BaseModule): ID = __name__.split(".")[-1] NAME = "Memes" DESCRIPTION = "Fun Module" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="mod_role_id", label="Mod Role ID", type="text", placeholder="", default="", ), ModuleSetting( key="cost_mod_pride", label="Points required to execute the modpride command", type="number", placeholder="", default=0, ), ModuleSetting( key="dank_role_id", label="Dank Role ID", type="text", placeholder="", default="", ), ModuleSetting( key="cost_dank", label="Points required to execute the dank command", type="number", placeholder="", default=0, ), ModuleSetting( key="dank_cooldown", label="Cooldown for the dank command", type="number", placeholder="", default=0, ), ModuleSetting( key="emote_for_vroom", label="Emote used for vroom", type="text", placeholder=":wheelchair:", default=":wheelchair:", ), ModuleSetting( key="max_vroom_races", label="Max vroom races", type="number", placeholder="", default=3, ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.mod_pride_running = False self.vroom_races = [] async def modpride(self, bot, author, channel, message, args): if self.mod_pride_running: return False self.mod_pride_running = True role = self.bot.discord_bot.guild.get_role( int(self.settings["mod_role_id"])) r, g, b = role.color.to_rgb() for c in rainbowcolors: dcol = discord.Colour(c) await asyncio.sleep(0.2) await role.edit(colour=dcol) await role.edit(colour=discord.Colour.from_rgb(r, g, b)) self.mod_pride_running = False return True async def vroom(self, bot, author, channel, message, args): if len(self.vroom_races) < self.settings["max_vroom_races"]: start_time = utils.now() m = await self.bot.say(channel=channel, message="﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏") self.vroom_races.append(m) await asyncio.sleep(0.5) await m.edit( content=f"{self.settings['emote_for_vroom']}﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏﹏" ) for _ in range(19): newtick = m.content[:-1] newtick = "﹏" + newtick await asyncio.sleep(random.randint(5, 30) / 10) await m.edit(content=newtick) elapsed_time = utils.now() - start_time await m.edit( content= f"{author.mention} finished in {round(elapsed_time.total_seconds(), 2)}s" ) self.vroom_races.remove(m) else: await self.bot.say( channel=channel, message= f"{author.mention} there can only be up to {self.settings['max_vroom_races']} races at the same time. Try later...", ignore_escape=True, ) async def dank(self, bot, author, channel, message, args): role = self.bot.discord_bot.guild.get_role( int(self.settings["dank_role_id"])) dcol = discord.Colour.from_rgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) await role.edit(colour=dcol) return True if role not in (self.bot.filters.get_member( [author.id], None, {})[0].roles) else "return currency" def load_commands(self, **options): if self.settings["mod_role_id"]: self.commands["modpride"] = Command.raw_command( self.modpride, command="modpride", delay_all=0, delay_user=0, cost=self.settings["cost_mod_pride"], can_execute_with_whisper=False, description="KappaPride Mods", ) if self.settings["dank_role_id"]: self.commands["dank"] = Command.raw_command( self.dank, command="dank", delay_all=self.settings["dank_cooldown"], delay_user=self.settings["dank_cooldown"], cost=self.settings["cost_dank"], can_execute_with_whisper=False, description="Messes with the dank roles color", ) self.commands["vroom"] = Command.raw_command( self.vroom, command="vroom", delay_all=0, delay_user=0, can_execute_with_whisper=False, description="VROOOOOOOOOOM", ) def enable(self, bot): if not bot: return def disable(self, bot): if not bot: return
class GiveawayModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Giveaway" DESCRIPTION = "Create Giveaways" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="level", label="Level required to start and stop giveaways", type="number", placeholder="500", default=500, ), ModuleSetting( key="regular_role_id", label="Role ID for regular role", type="text", placeholder="", default="", ), ModuleSetting( key="regular_role_tickets", label="Regular role tickets", type="number", placeholder="", default=5, ), ModuleSetting( key="tier1_sub_role_id", label="Role ID for tier 1 sub role", type="text", placeholder="", default="", ), ModuleSetting( key="tier1_sub_role_tickets", label="Tier 1 sub role tickets", type="number", placeholder="", default=1, ), ModuleSetting( key="tier2_sub_role_id", label="Role ID for tier 2 sub role", type="text", placeholder="", default="", ), ModuleSetting( key="tier2_sub_role_tickets", label="Tier 2 sub role tickets", type="number", placeholder="", default=2, ), ModuleSetting( key="tier3_sub_role_id", label="Role ID for tier 3 sub role", type="text", placeholder="", default="", ), ModuleSetting( key="tier3_sub_role_tickets", label="Tier 3 sub role tickets", type="number", placeholder="", default=4, ), ModuleSetting( key="valid_channels", label="Valid channels for typing commands", type="text", placeholder="", default="", ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot async def giveaway_info(self, bot, author, channel, message, args): embed = discord.Embed(description=("Giveaway Info"), colour=discord.Colour.dark_gold()) with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) embed.add_field( name=("Current Giveaway"), value= f"**{current_giveaway.giveaway_item}** ending **{current_giveaway.giveaway_deadline}**" if current_giveaway else "No giveaway is running right now", inline=False, ) role_dict = { "regular_role_id": self.settings["regular_role_tickets"], "tier1_sub_role_id": self.settings["tier1_sub_role_tickets"], "tier2_sub_role_id": self.settings["tier2_sub_role_tickets"], "tier3_sub_role_id": self.settings["tier3_sub_role_tickets"], } role_dict = dict(sorted(role_dict.items(), key=operator.itemgetter(1))) chances_value = "@everyone 1 entry\n" for role_name in role_dict: role_id = self.settings[role_name] if not role_id: continue role = self.bot.filters.get_role([role_id], None, {})[0] if role_id else None if not role: continue entries = role_dict[role_name] chances_value += f"{role.mention} {entries} entr{'ies' if entries > 1 else 'y'}\n" embed.add_field( name=("Chances to win!"), value=chances_value[:-1], inline=False, ) await self.bot.say(channel=channel, embed=embed) async def giveaway_join(self, bot, author, channel, message, args): with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if not current_giveaway: await self.bot.say( channel=channel, message= f"{author.mention}, there is no giveaway running right now.", ignore_escape=True) return False if current_giveaway.locked: await self.bot.say( channel=channel, message= f"{author.mention}, the current giveaway is locked.", ignore_escape=True) return False registered = GiveawayEntry.is_entered(db_session, str(author.id), current_giveaway.id) if registered: await self.bot.say( channel=channel, message= f"{author.mention}, you already joined the giveaway.", ignore_escape=True) return False tickets = self.get_highest_ticket_count(author) giveaway_entry = GiveawayEntry._create(db_session, str(author.id), current_giveaway.id, tickets) if giveaway_entry: await self.bot.say( channel=channel, message= f"{author.mention}, you joined the giveaway for **{current_giveaway.giveaway_item}** with **{tickets}** entr{'y' if tickets == 1 else 'ies' }! The giveaway will end **{current_giveaway.giveaway_deadline}** and you will be notified if you win. Good Luck! :wink:", ignore_escape=True) return True await self.bot.say( channel=channel, message= f"{author.mention} failed to add you to the giveaway dm a mod for help :smile:", ignore_escape=True) return False def get_highest_ticket_count(self, member): role_dict = { "regular_role_id": self.settings["regular_role_tickets"], "tier1_sub_role_id": self.settings["tier1_sub_role_tickets"], "tier2_sub_role_id": self.settings["tier2_sub_role_tickets"], "tier3_sub_role_id": self.settings["tier3_sub_role_tickets"], } role_dict = dict(sorted(role_dict.items(), key=operator.itemgetter(1))) tickets = 1 for role_name in role_dict: role_id = self.settings[role_name] if not role_id: continue role = self.bot.filters.get_role([role_id], None, {})[0] if role_id else None if not role: continue tickets = max(tickets, role_dict[role_name] ) if role and role in member.roles else tickets return tickets async def giveaway_start(self, bot, author, channel, message, args): with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if current_giveaway: await self.bot.say( channel=channel, message= "There is already a giveaway running. Please use !wipegiveaway before you start a new one. (Don't forget to chose a winner before you end!)", ignore_escape=True) return False desc_array = re.findall(r'"([^"]*)"', message) if not len(desc_array) == 2: await self.bot.say( channel=channel, message= 'Please set 2 arguments between quotation marks. `!startgiveaway "<item>" "<deadline>"`\nExample: `!startgiveaway "a new GTX2900" "10 days"`', ignore_escape=True) return False if Giveaway._create(db_session, str(author.id), desc_array[0], desc_array[1]): await self.bot.say( channel=channel, message= "New giveaway was started! Use !giveawaywinner to chose a winner when the time has passed.", ignore_escape=True) return True await self.bot.say( channel=channel, message= "An unknown error has occurred, please contact a moderator", ignore_escape=True) return False async def giveaway_wipe(self, bot, author, channel, message, args): with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if not current_giveaway: await self.bot.say(channel=channel, message="There is no giveaway running.") return False current_giveaway._disable(db_session) await self.bot.say(channel=channel, message="The current giveaway has been wiped") return True async def giveaway_winner(self, bot, author, channel, message, args): args_split = message.split(" ") try: count = int(args_split[0]) except: count = 1 with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if not current_giveaway: await self.bot.say(channel=channel, message="There is no giveaway running.") return False if not current_giveaway.locked: current_giveaway._lock_state(db_session, True) db_session.commit() pool = [] for entry in current_giveaway.entries: member = self.bot.filters.get_member([int(entry.user_id)], None, {})[0] for _ in range( max(entry.tickets, self.get_highest_ticket_count(member))): pool.append(entry) winning_users = [] while len(winning_users) < count: if len(pool) == 0: break winning_entry = random.choice(pool) winning_user = self.bot.filters.get_member( [int(winning_entry.user_id)], None, {})[0] if winning_user and winning_user not in winning_users: winning_users.append(winning_user) winning_entry._remove(db_session) pool = list(filter(lambda x: x != winning_entry, pool)) if len(winning_users) == 0: await self.bot.say( channel=channel, message="The giveaway ended but nobody entered!", ignore_escape=True) return True await self.bot.say(channel=channel, message="Shuffling giveaway list...", ignore_escape=True) await asyncio.sleep(5) await self.bot.say(channel=channel, message="*Shuffling intensifies...*", ignore_escape=True) await asyncio.sleep(5) await self.bot.say( channel=channel, message= f"**And the winner{'s are' if count > 1 else ' is'}...**", ignore_escape=True) await asyncio.sleep(5) for winning_user in winning_users: await self.bot.say( channel=channel, message= f"Congratulations {winning_user.mention} you won **{current_giveaway.giveaway_item}**!!!", ignore_escape=True) return True async def giveaway_lock(self, bot, author, channel, message, args): with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if not current_giveaway: await self.bot.say(channel=channel, message="There is no giveaway running.") return False if current_giveaway.locked: await self.bot.say( channel=channel, message="The current giveaway has already been locked") return False current_giveaway.locked = True await self.bot.say(channel=channel, message="The current giveaway has been locked") return True async def giveaway_unlock(self, bot, author, channel, message, args): with DBManager.create_session_scope() as db_session: current_giveaway = Giveaway._get_current_giveaway(db_session) if not current_giveaway: await self.bot.say(channel=channel, message="There is no giveaway running.") return False if not current_giveaway.locked: await self.bot.say( channel=channel, message="The current giveaway is not locked") return False current_giveaway.locked = False await self.bot.say(channel=channel, message="The current giveaway has been unlocked") return True def load_commands(self, **options): self.commands["giveaway"] = Command.multiaction_command( delay_all=0, delay_user=0, default="join", can_execute_with_whisper=False, command="giveaway", commands={ "join": Command.raw_command( self.giveaway_join, command="giveaway join", delay_all=0, delay_user=0, channels=json.dumps( self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Joins the current giveaway", ), "info": Command.raw_command( self.giveaway_info, command="giveaway info", delay_all=0, delay_user=0, channels=json.dumps( self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Info about the current giveaway", ) }, ) self.commands["startgiveaway"] = Command.raw_command( self.giveaway_start, command="startgiveaway", delay_all=0, delay_user=0, level=self.settings["level"], channels=json.dumps(self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Start a giveaway", ) self.commands["wipegiveaway"] = Command.raw_command( self.giveaway_wipe, command="wipegiveaway", delay_all=0, delay_user=0, level=self.settings["level"], channels=json.dumps(self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Clears the current giveaway", ) self.commands["giveawaywinner"] = Command.raw_command( self.giveaway_winner, command="giveawaywinner", delay_all=0, delay_user=0, level=self.settings["level"], channels=json.dumps(self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Chooses a winner(s) for the current giveaway", ) self.commands["lockgiveaway"] = Command.raw_command( self.giveaway_lock, command="lockgiveaway", delay_all=0, delay_user=0, level=self.settings["level"], channels=json.dumps(self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Locks the current giveaway", ) self.commands["unlockgiveaway"] = Command.raw_command( self.giveaway_unlock, command="unlockgiveaway", delay_all=0, delay_user=0, level=self.settings["level"], channels=json.dumps(self.settings["valid_channels"].split(" ")), can_execute_with_whisper=False, description="Unlocks the current giveaway", ) def enable(self, bot): if not bot: return def disable(self, bot): if not bot: return
class MovieNight(BaseModule): ID = __name__.split(".")[-1] NAME = "MovieNight" DESCRIPTION = "Fun Module" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="cdn_server_address", label="Server address for cdn", type="text", placeholder="", default="", ), ModuleSetting( key="cdn_stream_key", label="Stream key for cdn", type="text", placeholder="", default="", ), ModuleSetting( key="cdn_username", label="Username for cdn", type="text", placeholder="", default="", ), ModuleSetting( key="cdn_password", label="Password for cdn", type="text", placeholder="", default="", ), ModuleSetting( key="player_domain", label="Playser Hosting Domain", type="text", placeholder="", default="", ), ModuleSetting( key="logo_url", label="Discord Bot Logo URL", type="text", placeholder="", default="", ), ModuleSetting( key="alert_channel_id", label="Alert Channel ID", type="text", placeholder="", default="", ), ModuleSetting( key="level", label="Level required to execute mod commands", type="number", placeholder="500", default=500, ), ModuleSetting( key="valid_channels", label="Channel IDs commands can be executed in", type="text", placeholder="", default="", ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot async def movienight(self, bot, author, channel, message, args): # query api, returns is_online, is_ull, key is_online, is_ull, key = self.bot.movienight_api.is_online() if not is_online: await self.bot.say(channel=channel, message="Movienight is offline right now :(") return False if is_ull: embed = discord.Embed( title="Movienight is online!", color=0x0600FF, description="Try the normal player if you are having problems with the low latency one.", ) if self.settings["logo_url"]: embed.set_thumbnail(url=self.settings["logo_url"]) embed.add_field( name="Web (low latency):", value=f"https://{self.settings['player_domain']}/ull_player.html?key={key}", inline=False, ) embed.add_field( name="Web:", value=f"https://{self.settings['player_domain']}/ull_player-hls-forced.html?key={key}", inline=True, ) else: embed = discord.Embed( title="Movienight is online!", color=0x0600FF, description="Use the following link to watch: ", ) if self.settings["logo_url"]: embed.set_thumbnail(url=self.settings["logo_url"]) embed.add_field( name="Web:", value=f"https://{self.settings['player_domain']}/cdn_player.html?key={key}", inline=True, ) await self.bot.say(channel=channel, embed=embed) return True async def moviestart_ull(self, bot, author, channel, message, args): await self.bot.private_message(user=author, message="Starting movienight ull, this usually takes less than a minute...") server, stream_key = await self.bot.movienight_api.create_ull_target() embed = discord.Embed( title="A new Wowza ULL target has been created! Use the following OBS settings:", color=0x008000, ) embed.set_author(name="Movienight (ULL)") embed.add_field(name="Server:", value=server, inline=False) embed.add_field(name="Stream Key:", value=stream_key, inline=False) embed.add_field(name="Authentication:", value="Disabled", inline=False) await self.bot.private_message(user=author, embed=embed) return True async def moviestart_cdn(self, bot, author, channel, message, args): await self.bot.private_message(user=author, message="Starting transcoder, this usually takes less than a minute...") start_transcoder = await self.bot.movienight_api.start_cdn_target() if start_transcoder: embed = discord.Embed( title="Wowza CDN target ready. Here are the OBS settings:", color=0x008000, ) embed.set_author(name="Movienight (CDN)") embed.add_field( name="Server:", value=self.settings["cdn_server_address"] if self.settings["cdn_server_address"] else "Not Specified", inline=False #TODO ) embed.add_field(name="Stream Key:", value=self.settings["cdn_stream_key"] if self.settings["cdn_stream_key"] else "Not Specified", inline=False) embed.add_field(name="Authentication:", value="Enabled", inline=False) embed.add_field(name="Username:"******"cdn_username"] if self.settings["cdn_username"] else "Not Specified", inline=False) embed.add_field(name="Password:"******"cdn_password"] if self.settings["cdn_password"] else "Not Specified", inline=False) await self.bot.private_message(user=author, message="Target Ready!", embed=embed) return True await self.bot.private_message(user=author, message="Was unable to start the transcoder, please check logs") return False async def movie_night_started_event(self): if not self.bot.movienight_api.active: log.error("API is not running!") return is_online, is_ull, key = self.bot.movienight_api.is_online() if not is_online: return out_chnanel = self.bot.filters.get_channel([self.settings["alert_channel_id"]], None, {})[0] if is_ull: embed = discord.Embed( title="Movienight is online!", color=0x0600FF, description="Use the normal player if you are having problems with the low latency one.", ) embed.add_field( name="Web (low latency):", value=f"https://movie.admiralbulldog.live/ull_player.html?key={key}", inline=False, ) embed.add_field( name="Web:", value=f"https://movie.admiralbulldog.live/ull_player-hls-forced.html?key={key}", inline=True, ) else: embed = discord.Embed( title="Movienight is about to start!", color=0x0600FF, description="Use the following link to watch: ", ) embed.add_field( name="Web:", value=f"https://movie.admiralbulldog.live/cdn_player.html?key={key}", inline=True, ) if self.settings["logo_url"]: embed.set_thumbnail(url=self.settings["logo_url"]) await self.bot.say(channel=out_chnanel, embed=embed) def load_commands(self, **options): self.commands["movienight"] = Command.raw_command( self.movienight, delay_all=0, delay_user=0, can_execute_with_whisper=False, channels=json.dumps(self.settings["valid_channels"].split(" ")), description="Shows thge movienight links", ) self.commands["moviestart"] = Command.multiaction_command( delay_all=0, delay_user=0, default=None, can_execute_with_whisper=False, level=self.settings["level"], command="moviestart", commands={ "ull": Command.raw_command( self.moviestart_ull, command="moviestart ull", delay_all=0, delay_user=0, level=self.settings["level"], can_execute_with_whisper=False, channels=json.dumps(self.settings["valid_channels"].split(" ")), description="Creates an ultra-low-latency target (lowest latency, source quality only)", ), "cdn": Command.raw_command( self.moviestart_cdn, command="moviestart cdn", delay_all=0, delay_user=0, level=self.settings["level"], can_execute_with_whisper=False, channels=json.dumps(self.settings["valid_channels"].split(" ")), description="Starts a normal cdn target with transcoder (higher latency, adaptive bitrate)", ), }, ) if not self.bot: return if not self.bot.movienight_api.active: log.error("API is not running!") return def enable(self, bot): if not bot: return if not self.bot.movienight_api.active: log.error("API is not running!") return HandlerManager.add_handler("movie_night_started", self.movie_night_started_event) def disable(self, bot): if not bot: return HandlerManager.remove_handler("movie_night_started", self.movie_night_started_event)
class TimeoutModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Timeout" DESCRIPTION = "Allows moderators to timeout users" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="punished_role_id", label="Role ID given when a user is timedout", type="text", placeholder="", default="", ), ModuleSetting( key="level_for_command", label="Level required to timeout user", type="number", placeholder="", default=500, ), ModuleSetting( key="log_timeout", label="Log timeout Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_untimeout", label="Log untimeout Event", type="boolean", placeholder="", default=True, ), ModuleSetting( key="log_timeout_update", label="Log timeout_update Event", type="boolean", placeholder="", default=True, ), ] def __init__(self, bot): super().__init__(bot) self.bot = bot self.redis = RedisManager.get() self.reminder_tasks = {} async def timeout_user(self, bot, author, channel, message, args): # !timeout <here> @username command_args = message.split(" ") if message else [] if len(command_args) == 0: await self.bot.say( channel=channel, message= f"!timeout (here) <User mention> <duration> (reason...)", ) return False member = self.bot.filters.get_member([command_args[0]], None, {})[0] if not member: await self.bot.say(channel=channel, message=f"Cant find member, {command_args[0]}") return False with DBManager.create_session_scope() as db_session: user_level = self.bot.psudo_level_member(db_session, member) if user_level >= args["user_level"]: await self.bot.say( channel=channel, message= f"You cannot timeout a member with a with a level the same or higher than you!", ) return False timedelta = (utils.parse_timedelta(command_args[1]) if len(command_args) > 1 else None) ban_reason = (" ".join(command_args[1:]) if not timedelta else " ".join(command_args[2:])) success, resp = await self.bot.timeout_manager.timeout_user( db_session, member, author, (utils.now() + timedelta) if timedelta else None, ban_reason) duration = f"timedout for {utils.seconds_to_resp(timedelta.total_seconds())}" if timedelta else "permanently muted" if success: await self.bot.say( channel=channel, message=f"Member {member.mention} has been {duration}") return True await self.bot.say(channel=channel, message=resp) return False async def untimeout_user(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] if len(command_args) == 0: await self.bot.say( channel=channel, message=f"!untimeout (here) <User mention> (reason...)") return False member = self.bot.filters.get_member([command_args[0]], None, {})[0] if not member: await self.bot.say(channel=channel, message=f"Cant find member, {command_args[0]}") return False unban_reason = " ".join(command_args[1:]) with DBManager.create_session_scope() as db_session: success, resp = await self.bot.timeout_manager.untimeout_user( db_session, member, author, unban_reason) if success: await self.bot.say( channel=channel, message=f"Member {member.mention} has been untimedout") return True self.bot.say(channel=channel, message=resp) return False async def query_timeouts(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] member = self.bot.filters.get_member([command_args[0]], None, {})[0] if not member: await self.bot.say(channel=channel, message=f"Cant find member, {command_args[0]}") return False with DBManager.create_session_scope() as db_session: timeouts = Timeout._by_user_id(db_session, str(member.id)) if not timeouts: await self.bot.say( channel=channel, message=f"The user {member} has no timeouts") return True for timeout in timeouts: embed = discord.Embed( description=f"Timeout #{timeout.id}", timestamp=timeout.created_at, colour=member.colour, ) embed.set_author( name=f"{member} ({member.id})", icon_url=str(member.avatar_url), ) embed.add_field( name="Banned on", value=str( timeout.created_at.strftime("%b %d %Y %H:%M:%S %Z")), inline=False, ) embed.add_field( name="Banned till" if timeout.active else "Unbanned on", value=str(timeout.until.strftime("%b %d %Y %H:%M:%S %Z")) if timeout.until else "Permanently", inline=False, ) if timeout.active: embed.add_field( name="Timeleft", value=str(utils.seconds_to_resp(timeout.time_left)), inline=False, ) if timeout.issued_by_id: issued_by = self.bot.filters.get_member( [int(timeout.issued_by_id)], None, {})[0] embed.add_field( name="Banned by", value=issued_by.mention if issued_by else f"{timeout.issued_by_id}", inline=False, ) if timeout.ban_reason: embed.add_field(name="Ban Reason", value=str(timeout.ban_reason), inline=False) if timeout.active and timeout.unban_reason: embed.add_field( name="Unban Reason", value=str(timeout.unban_reason), inline=False, ) if timeout.active and timeout.unbanned_by_id: unbanned_by = self.bot.filters.get_member( [int(timeout.unbanned_by_id)], None, {})[0] embed.add_field( name="Unban By", value=unbanned_by.mention if unbanned_by else f"{timeout.issued_by_id}", inline=False, ) await self.bot.say(channel=channel, embed=embed) async def is_timedout(self, bot, author, channel, message, args): command_args = message.split(" ") if message else [] member = self.bot.filters.get_member([command_args[0]], None, {})[0] if not member: await self.bot.say(channel=channel, message=f"Cant find member, {command_args[0]}") return False with DBManager.create_session_scope() as db_session: timeout = Timeout._is_timedout(db_session, str(member.id)) if not timeout: await self.bot.say( channel=channel, message=f"The user {member} has not currently timedout", ) return True embed = discord.Embed( description=f"Timeout #{timeout.id}", timestamp=timeout.created_at, colour=member.colour, ) embed.add_field( name="Banned on", value=str(timeout.created_at.strftime("%b %d %Y %H:%M:%S %Z")), inline=False, ) embed.add_field( name="Banned till" if timeout.time_left != 0 else "Unbanned on", value=str(timeout.until.strftime("%b %d %Y %H:%M:%S %Z")) if timeout.until else "Permanently", inline=False, ) if timeout.time_left != 0: embed.add_field( name="Timeleft", value=str(utils.seconds_to_resp(timeout.time_left)), inline=False, ) if timeout.issued_by: embed.add_field(name="Banned by", value=str(timeout.issued_by), inline=False) if timeout.ban_reason: embed.add_field(name="Ban Reason", value=str(timeout.ban_reason), inline=False) if timeout.time_left != 0 and timeout.unban_reason: embed.add_field(name="Unban Reason", value=str(timeout.unban_reason), inline=False) await self.bot.say(channel=channel, embed=embed) def load_commands(self, **options): self.commands["timeout"] = Command.raw_command( self.timeout_user, command="timeout", delay_all=0, delay_user=0, level=int(self.settings["level_for_command"]), can_execute_with_whisper=False, description="Adds a timeout to a user", ) self.commands["untimeout"] = Command.raw_command( self.untimeout_user, command="untimeout", delay_all=0, delay_user=0, level=int(self.settings["level_for_command"]), can_execute_with_whisper=False, description="Removes a timeout on a user", ) self.commands["timeouts"] = Command.raw_command( self.query_timeouts, command="timeouts", delay_all=0, delay_user=0, level=int(self.settings["level_for_command"]), can_execute_with_whisper=False, description="Queries timeouts of a user", ) self.commands["istimedout"] = Command.raw_command( self.is_timedout, command="istimedout", delay_all=0, delay_user=0, level=int(self.settings["level_for_command"]), can_execute_with_whisper=False, description="Checks if the user is currently timedout", ) def enable(self, bot): if not bot: return self.bot.timeout_manager.enable({ "enabled": True, "log_timeout": self.settings["log_timeout"], "log_untimeout": self.settings["log_untimeout"], "log_timeout_update": self.settings["log_timeout_update"], "punished_role_id": self.settings["punished_role_id"], }) def disable(self, bot): if not bot: return self.bot.timeout_manager.disable()