async def import_bans(self, ctx: 'CustomContext'): """ Import bans from the server banlist. If possible and available, also include the reason from the audit logs. This is only available to servers administrators, and can only be done once per guild. :param ctx: :return: """ if await self.bot.settings.get(ctx.guild, 'imported_bans'): await ctx.send("You already imported your guild bans. " "If you think this is an error, join the support server and ask!") return else: await self.bot.settings.set(ctx.guild, 'imported_bans', True) await ctx.send(f"Doing that, it may take a long time, please wait!") bans = await ctx.guild.bans() i = 0 t = len(bans) for ban in bans: user = ban.user reason = ban.reason if not reason: reason = "No reason was provided in the audit logs" await self.api.add_action(ctx.guild, user, 'ban', reason, responsible_moderator=LikeUser(did=0, name="BanList Import", guild=ctx.guild)) i += 1 await ctx.send(f"{i}/{t} bans imported from the server ban list.")
async def unmute_task(self, task: dict): arguments = json.loads(task['arguments']) # {"target": 514557845111570447, "guild": 512328935304855555, "reason": "Time is up (1 week, 2 days and 23 hours)"} guild_id = arguments["guild"] guild: discord.Guild = self.bot.get_guild(guild_id) if guild: member = guild.get_member(arguments["target"]) if member: tasks_user = LikeUser(did=5, name="DoItLater", guild=guild) act = await full_process(self.bot, unmute, member, tasks_user, arguments["reason"], automod_logs=f"Task number #{task['id']}") return True
async def run_actions(self, guild: discord.Guild, bad_members: typing.List[discord.Member]): if len(bad_members) == 0: return 'No members to act on.' logging_channel = await self.bot.get_cog( 'Logging').get_logging_channel(guild, "logs_autoinspect_channel_id") if not logging_channel: return 'No logging channel configured for AutoInspect/AntiRaid.' action = await self.bot.settings.get(guild, "autoinspect_antiraid") if action == 1: return 'Nothing to do.' autoinspect_user = LikeUser(did=4, name="AutoInspector", guild=guild) # await full_process(self.bot, note, context["member"], autoinspect_user, reason=f"Automatic note by AutoInspect {name}, following a positive check.") embed = discord.Embed() embed.colour = discord.colour.Color.red() embed.title = f"AutoInspect AntiRaid | Positive Result" embed.description = f"AutoInspect AntiRaid triggered!" embed.set_author(name=self.bot.user.name) embed.add_field(name="Member(s) ID(s)", value=str([m.id for m in bad_members])) await logging_channel.send(embed=embed) for member in bad_members: if action == 3: await full_process( self.bot, softban, member, autoinspect_user, reason=f"Automatic softban by AutoInspect AntiRaid") return False elif action == 4: await full_process( self.bot, ban, member, autoinspect_user, reason=f"Automatic ban by AutoInspect AntiRaid") return False
async def check_and_act(self, check: typing.Callable[[discord.Member], typing.Awaitable], name: str, context: dict) -> bool: """ This returns true if the search should continue, else False. """ action = await self.bot.settings.get(context["guild"], name) if action == 1: return True check_result = await check(context["member"]) if check_result: if await self.bot.settings.get(context['guild'], 'autoinspect_bypass_enable'): logs = f"To prevent False Positives, AutoInspect added a mark on this account for 600 seconds. " \ f"If the user {context['member'].name}#{context['member'].discriminator} tries to rejoin the server in the " \ f"next 10 minutes, AutoInspect rules will not apply on him." else: logs = "As requested by the server settings, no exceptions are allowed on AutoInspect." autoinspect_user = LikeUser(did=4, name="AutoInspector", guild=context["guild"]) # await full_process(self.bot, note, context["member"], autoinspect_user, reason=f"Automatic note by AutoInspect {name}, following a positive check.") embed = discord.Embed() embed.colour = discord.colour.Color.red() embed.title = f"AutoInspect | Positive Result" embed.description = f"{context['member'].name}#{context['member'].discriminator} ({context['member'].id}) AutoInspect check was positive for check {name}" embed.set_author(name=self.bot.user.name) embed.add_field(name="Member ID", value=context['member'].id) embed.add_field(name="Check name", value=name) embed.add_field(name="Info", value=logs, inline=False) await context["logging_channel"].send(embed=embed) if action == 3: await full_process(self.bot, softban, context["member"], autoinspect_user, reason=f"Automatic softban by AutoInspect {name}", automod_logs=logs) return False elif action == 4: await full_process(self.bot, ban, context["member"], autoinspect_user, reason=f"Automatic ban by AutoInspect {name}", automod_logs=logs) return False return True
async def add_moderator(self, ctx: 'CustomContext', user: typing.Union[discord.Member, discord.Role]): """ Add a moderator on this server. Moderators can do things such as banning, kicking, warning, softbanning... You can manage the members you gave some access to in your server settings in the webinterface. See `m+urls` """ if isinstance(user, discord.Role): user = LikeUser(did=user.id, name=f"[ROLE] {user.name}", guild=ctx.guild, discriminator='0000', do_not_update=False) await self.api.add_to_staff(ctx.guild, user, 'moderators') await ctx.send_to( ':ok_hand: Done. You can edit staff on the web interface.')
async def add_admin(self, ctx: 'CustomContext', user: typing.Union[discord.Member, discord.Role]): """ Add some server admins. They can moderate the server but also edit other moderator reasons on the webinterface. You can manage the members you gave some access to in your server settings in the webinterface. See `m+urls` """ if isinstance(user, discord.Role): user = LikeUser(did=user.id, name=f"[ROLE] {user.name}", guild=ctx.guild, discriminator='0000', do_not_update=False) await self.api.add_to_staff(ctx.guild, user, 'admins') await ctx.send_to( ':ok_hand: Done. You can edit staff on the web interface.')
async def convert( self, ctx: CustomContext, argument) -> typing.Union[discord.Member, FakeMember, LikeUser]: try: m = await commands.MemberConverter().convert(ctx, argument) return m except commands.BadArgument: try: did = int(argument, base=10) if did < 10 * 15: # Minimum 21154535154122752 (17 digits, but we are never too safe) raise commands.BadArgument( f"The discord ID {argument} provided is too small to be a real discord user-ID. Please check your input and try again." ) if not self.may_be_banned: if discord.utils.find(lambda u: u.user.id == did, await ctx.guild.bans()): raise commands.BadArgument( f"The member {argument} is already banned.") try: u = ctx.bot.get_user(did) if u: return FakeMember(u, ctx.guild) else: u = await ctx.bot.fetch_user(did) return FakeMember(u, ctx.guild) except: ctx.logger.exception( "An error happened trying to convert a discord ID to a User instance. " "Relying on a LikeUser") return LikeUser(did=int(argument, base=10), name="Unknown member", guild=ctx.guild) except ValueError: raise commands.BadArgument( f"{argument} is not a valid member or member ID." ) from None except Exception as e: raise
async def unban_task(self, task: dict): arguments = json.loads(task['arguments']) # {"target": 514557845111570447, "guild": 512328935304855555, "reason": "Time is up (1 week, 2 days and 23 hours)"} guild_id = arguments["guild"] guild: discord.Guild = self.bot.get_guild(guild_id) if guild: user = await self.bot.fetch_user(int(arguments["target"])) if user: if not user.id in [b.user.id for b in await guild.bans()]: return True # Already unbanned fake_member = FakeMember(user, guild) tasks_user = LikeUser(did=5, name="DoItLater", guild=guild) act = await full_process(self.bot, unban, fake_member, tasks_user, arguments["reason"], automod_logs=f"Task number #{task['id']}") return True # Failed because no such guild/user return True # Anyway
async def massjoins(self, member: discord.Member): guild = member.guild if not await self.bot.settings.get(guild, 'autoinspect_enable') or not await self.bot.settings.get(guild, 'autoinspect_massjoins'): return 'AutoInspect disabled on this guild.' autoinspect_user = LikeUser(did=4, name="AutoInspector", guild=guild) self.joins_cache[guild] = (self.joins_cache.get(guild, []) + [member])[:] joins_count = len(self.joins_cache[guild]) if self.protected_cache.get(guild, False): await full_process(self.bot, kick, member, autoinspect_user, reason=f"Automatic kick by AutoInspect because of too many joins", automod_logs=f"{joins_count} joins in the last 3 minutes") return else: if joins_count >= 15: self.protected_cache[guild] = True tokick = self.joins_cache[guild] for member in tokick: await full_process(self.bot, kick, member, autoinspect_user, reason=f"Automatic kick by AutoInspect because of too many joins", automod_logs=f"{joins_count} joins in the last 3 minutes")
async def check_message(self, message: discord.Message, act: bool = True) -> Union[CheckMessage, str]: await self.bot.wait_until_ready() author = message.author if author.bot: return "You are a bot" # ignore messages from other bots if message.guild is None: return "Not in a guild" # ignore messages from PMs if not await self.bot.settings.get( message.guild, 'automod_enable') and not "[getbeaned:enable_automod]" in str( message.channel.topic): return "Automod disabled here" if "[getbeaned:disable_automod]" in str(message.channel.topic): return "`[getbeaned:disable_automod]` in topic, Automod Disabled here" current_permissions = message.guild.me.permissions_in(message.channel) wanted_permissions = discord.permissions.Permissions.none() wanted_permissions.update(kick_members=True, ban_members=True, read_messages=True, send_messages=True, manage_messages=True, embed_links=True, attach_files=True, read_message_history=True, external_emojis=True, change_nickname=True) cond = current_permissions >= wanted_permissions if not cond: return "No permissions to act" check_message = CheckMessage(self.bot, message) ctx = await self.bot.get_context(message, cls=context.CustomContext) author_level = await get_level(ctx, check_message.message.author) automod_ignore_level = await self.bot.settings.get( message.guild, 'automod_ignore_level') if author_level >= automod_ignore_level and act: return "Author level is too high, I'm not going further" if author.status is discord.Status.offline: check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_offline') check_message.debug("Author is offline (probably invisible)") if author.created_at > datetime.datetime.now() - datetime.timedelta( days=7): check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_new_account') check_message.debug("Author account is less than a week old") if author.joined_at > datetime.datetime.now() - datetime.timedelta( days=1): check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_just_joined') check_message.debug("Author account joined less than a day ago") if author.is_avatar_animated(): check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_have_nitro') check_message.debug( "Author account is nitro'd (or at least I can detect an animated avatar)" ) if len(author.roles) > 2: # Role duckies is given by default check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_have_roles') check_message.debug("Author account have a role in the server") if author_level == 0: check_message.multiplicator += await self.bot.settings.get( message.guild, 'automod_multiplictor_bot_banned') check_message.debug("Author is bot-banned") if check_message.multiplicator <= 0: check_message.debug( "Multiplicator is <= 0, exiting without getting score") return check_message # Multiplicator too low! check_message.debug("Multiplicator calculation done") ## Multiplicator calculation done! total_letters = len(message.content) total_captial_letters = sum(1 for c in message.content if c.isupper()) # If no letters, then 100% caps. caps_percentage = total_captial_letters / total_letters if total_letters > 0 else 1 if caps_percentage >= 0.7 and total_letters > 10: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_caps') check_message.debug( f"Message is written in CAPS LOCK (% of caps: {round(caps_percentage * 100, 3)} —" f" total length: {total_letters})") # if len(message.embeds) >= 1 and any([e.type == "rich" for e in message.embeds]): # check_message.score += await self.bot.settings.get(message.guild, 'automod_score_embed') # check_message.debug(f"Message from a USER contain an EMBED !? (Used to circumvent content blocking)") if "@everyone" in message.content and not message.mention_everyone: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_everyone') check_message.debug( f"Message contains an ATeveryone that discord did not register as a ping (failed attempt)" ) mentions = set(mention for mention in message.mentions if mention.id != author.id) if len(mentions) > 3: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_too_many_mentions') m_list = [a.name + '#' + a.discriminator for a in mentions] check_message.debug( f"Message mentions more than 3 people ({m_list})") if "[getbeaned:disable_invite_detection]" not in str( message.channel.topic ): # They can add multiple channels separated by a " " invites_count = await self.get_invites_count(check_message) if invites_count >= 1: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_contain_invites') * invites_count check_message.debug( f"Message contains invite(s) ({check_message.invites_code})" ) if message.content and "[getbeaned:disable_spam_detection]" not in str( message.channel.topic): # TODO: Check images repeat repeat = [ m.content for m in self.message_history[check_message.message.author] ].count(check_message.message.content) if repeat >= 3: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_repeated') * repeat check_message.debug( f"Message was repeated by the author {repeat} times") bad_words_matches = await self.bot.settings.get_bad_word_matches( message.guild, check_message.message.content) bad_words_count = len(bad_words_matches) if bad_words_count >= 1: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_bad_words') * bad_words_count bad_words_list = [] for match in bad_words_matches: string, pattern = match bad_words_list.append(f"{string} matched by {pattern}") check_message.debug( f"Message contains {bad_words_count} bad words ({', '.join(bad_words_list)})" ) spam_cond = (not check_message.message.content.lower().startswith( ("dh", "!", "?", "§", "t!", ">", "<", "-", "+")) or len(message.mentions) or len(check_message.message.content) > 30) and ( check_message.message.content.lower() not in [ 'yes', 'no', 'maybe', 'hey', 'hi', 'hello', 'oui', 'non', 'bonjour', '\o', 'o/', ':)', ':D', ':(', 'ok', 'this', 'that', 'yup' ]) and act if spam_cond: # Not a command or something self.message_history[check_message.message.author].append( check_message.message) # Add content for repeat-check later. self.message_history.reset_expiry(check_message.message.author) if len([ mention for mention in message.mentions if mention.id != author.id ]): historic_mentions_users = [] for historic_message in self.message_history[ check_message.message.author]: historic_mentions_users.extend( mention for mention in historic_message.mentions if mention.id != author.id) historic_mentions_total = len(historic_mentions_users) historic_mentions_users = set(historic_mentions_users) historic_mentions_different = len(historic_mentions_users) if historic_mentions_total > 7: # He mentioned 7 times in the last 7 messages check_message.score += await self.bot.settings.get( message.guild, 'automod_score_multimessage_too_many_mentions') check_message.debug( f"Message history contains too many mentions (historic_mentions_total={historic_mentions_total})" ) if historic_mentions_different > 5: # He mentioned 5 different users in the last 7 messages check_message.score += await self.bot.settings.get( message.guild, 'automod_score_multimessage_too_many_users_mentions') check_message.debug( f"Message history contains too many mentions (historic_mentions_different={historic_mentions_different} | users_mentionned: {historic_mentions_users})" ) contains_zalgo, zalgo_score = await self.contains_zalgo(message.content ) if contains_zalgo: check_message.score += await self.bot.settings.get( message.guild, 'automod_score_zalgo') check_message.debug( f"Message contains zalgo (zalgo_score={zalgo_score})") if await self.bot.settings.get(message.guild, 'autotrigger_enable'): check_message.debug("Running AutoTrigger checks") instancied_triggers = [t(check_message) for t in TRIGGERS_ENABLED] for trigger in instancied_triggers: score = await trigger.run() if score != 0: check_message.score += score check_message.debug("Score calculation done") check_message.debug( f"Total for message is {check_message.total}, applying actions if any" ) automod_user = LikeUser(did=1, name="AutoModerator", guild=message.guild) # Do we need to delete the message ? automod_delete_message_score = await self.bot.settings.get( message.guild, 'automod_delete_message_score') if check_message.total >= automod_delete_message_score > 0: check_message.debug( f"Deleting message because score " f"**{check_message.total}** >= {automod_delete_message_score}") try: if act: await message.delete() if await self.bot.settings.get( message.guild, 'automod_note_message_deletions'): await full_process( ctx.bot, note, message.author, automod_user, reason="Automod deleted a message from this user.", automod_logs="\n".join(check_message.logs)) except discord.errors.NotFound: check_message.debug(f"Message already deleted!") else: # Too low to do anything else return check_message # That's moderation acts, where the bot grabs his BIG HAMMER and throw it in the user face # Warning automod_warn_score = await self.bot.settings.get( message.guild, 'automod_warn_score') automod_kick_score = await self.bot.settings.get( message.guild, 'automod_kick_score') automod_softban_score = await self.bot.settings.get( message.guild, 'automod_softban_score') automod_ban_score = await self.bot.settings.get( message.guild, 'automod_ban_score') # Lets go in descending order: if check_message.total >= automod_ban_score > 0: check_message.debug( f"Banning user because score **{check_message.total}** >= {automod_ban_score}" ) if act: r = f"Automatic action from automod. " \ f"Banning user because score **{check_message.total}** >= {automod_ban_score}" await full_process(ctx.bot, ban, message.author, automod_user, reason=r, automod_logs="\n".join(check_message.logs)) elif check_message.total >= automod_softban_score > 0: check_message.debug( f"SoftBanning user because score **{check_message.total}** >= {automod_softban_score}" ) if act: r = f"Automatic action from automod. " \ f"SoftBanning user because score **{check_message.total}** >= {automod_softban_score}" await full_process(ctx.bot, softban, message.author, automod_user, reason=r, automod_logs="\n".join(check_message.logs)) elif check_message.total >= automod_kick_score > 0: check_message.debug( f"Kicking user because score **{check_message.total}** >= {automod_kick_score}" ) if act: r = f"Automatic action from automod. " \ f"Kicking user because score **{check_message.total}** >= {automod_kick_score}" await full_process(ctx.bot, kick, message.author, automod_user, reason=r, automod_logs="\n".join(check_message.logs)) elif check_message.total >= automod_warn_score > 0: check_message.debug( f"Warning user because score **{check_message.total}** >= {automod_warn_score}" ) if act: r = f"Automatic action from automod. " \ f"Warning user because score **{check_message.total}** >= {automod_warn_score}" await full_process(ctx.bot, warn, message.author, automod_user, reason=r, automod_logs="\n".join(check_message.logs)) ctx.logger.info("Automod acted on a message, logs follow.") ctx.logger.info("\n".join(check_message.logs)) return check_message
async def dehoist_user_in_guild(self, user: typing.Union[discord.User, discord.Member], guild: discord.Guild) -> bool: if await self.bot.settings.get(guild, "dehoist_enable"): member = guild.get_member(user.id) if user.id in self.bypass[guild]: return False if await get_level(FakeCtx(guild, self.bot), member) >= int(await self.bot.settings.get( guild, "dehoist_ignore_level")): return False intensity = int(await self.bot.settings.get(guild, "dehoist_intensity")) previous_nickname = member.display_name new_nickname = previous_nickname if intensity >= 1: for pos, char in enumerate(new_nickname): if char in [ "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/" ]: new_nickname = new_nickname[1:] continue else: break if intensity >= 2: for pos, char in enumerate(new_nickname): if char not in string.ascii_letters: new_nickname = new_nickname[1:] continue else: break if intensity >= 3: new_nickname += "zz" while new_nickname.lower()[:2] == "aa": new_nickname = new_nickname[2:] new_nickname = new_nickname[:-2] if previous_nickname != new_nickname: if len(new_nickname) == 0: new_nickname = "z_Nickname_DeHoisted" self.bot.logger.info( f"Dehoisted user {previous_nickname} -> {new_nickname} in {guild}" ) reason = f"Automatic nickname DeHoist from {previous_nickname} to {new_nickname}. " \ f"Please try not to use special chars at the start of your nickname to appear at the top of the list of members." await member.edit(nick=new_nickname, reason=reason) actions_to_take = { "note": note, "warn": warn, "message": None, "nothing": None } action_name = await self.bot.settings.get( guild, "dehoist_action") action_coroutine = actions_to_take[action_name] if action_coroutine: moderator = LikeUser(did=3, name="DeHoister", guild=guild) await full_process(self.bot, action_coroutine, member, moderator, reason) if action_name != "nothing": try: await member.send( f"Your nickname/username was dehoisted on {guild.name}. " f"Please try not to use special chars at the start of your nickname to appear at the top of the list of members. " f"Thanks! Your new nickname is now `{new_nickname}`" ) except discord.Forbidden: pass return True else: return False else: return False
async def thresholds_enforcer(bot, victim: discord.Member, action_type: str): if not await bot.settings.get(victim.guild, 'thresholds_enable'): return False reason = "Thresholds enforcing" mod_user = LikeUser(did=2, name="ThresholdsEnforcer", guild=victim.guild) counters = await bot.api.get_counters(victim.guild, victim) if action_type == 'note' or action_type == 'unban': return False elif action_type == 'warn': thresholds_warns_to_kick = await bot.settings.get( victim.guild, 'thresholds_warns_to_kick') if thresholds_warns_to_kick and counters[ 'warn'] % thresholds_warns_to_kick == 0: logs = f"Total of {counters['warn']}, we want {counters['warn']}%{thresholds_warns_to_kick} = {counters['warn'] % thresholds_warns_to_kick} = 0" await full_process(bot, kick, victim, mod_user, reason=reason, automod_logs=logs) elif action_type == 'mute': thresholds_mutes_to_kick = await bot.settings.get( victim.guild, 'thresholds_mutes_to_kick') if thresholds_mutes_to_kick and counters[ 'mute'] % thresholds_mutes_to_kick == 0: logs = f"Total of {counters['mute']}, we want {counters['mute']}%{thresholds_mutes_to_kick} = {counters['mute'] % thresholds_mutes_to_kick} = 0" await full_process(bot, kick, victim, mod_user, reason=reason, automod_logs=logs) elif action_type == 'kick': thresholds_kicks_to_bans = await bot.settings.get( victim.guild, 'thresholds_kicks_to_bans') if thresholds_kicks_to_bans and counters[ 'kick'] % thresholds_kicks_to_bans == 0: logs = f"Total of {counters['kick']}, we want {counters['kick']}%{thresholds_kicks_to_bans} = {counters['kick'] % thresholds_kicks_to_bans} = 0" await full_process(bot, ban, victim, mod_user, reason=reason, automod_logs=logs) elif action_type == 'softban': thresholds_softbans_to_bans = await bot.settings.get( victim.guild, 'thresholds_softbans_to_bans') if thresholds_softbans_to_bans and counters[ 'softban'] % thresholds_softbans_to_bans == 0: logs = f"Total of {counters['softban']}, we want {counters['softban']}%{thresholds_softbans_to_bans} = {counters['softban'] % thresholds_softbans_to_bans} = 0" await full_process(bot, ban, victim, mod_user, reason=reason, automod_logs=logs) else: return False return True