async def mod_loop(self): guild: Guild = self.bot.guilds[0] async for ban in await db.stream(filter_by(Ban, active=True)): if ban.days != -1 and utcnow() >= ban.timestamp + timedelta( days=ban.days): await Ban.deactivate(ban.id) try: user = await self.bot.fetch_user(ban.member) except NotFound: user = ban.member, ban.member_name if isinstance(user, User): try: await guild.unban(user) except Forbidden: await send_alert( guild, t.cannot_unban_user_permissions( user.mention, user.id)) await send_to_changelog_mod(guild, None, Colors.unban, t.log_unbanned, user, t.log_unbanned_expired) mute_role: Optional[Role] = guild.get_role(await RoleSettings.get("mute")) if mute_role is None: return try: check_role_assignable(mute_role) except CommandError: await send_alert( guild, t.cannot_assign_mute_role(mute_role, mute_role.id)) return async for mute in await db.stream(filter_by(Mute, active=True)): if mute.days != -1 and utcnow() >= mute.timestamp + timedelta( days=mute.days): if member := guild.get_member(mute.member): await member.remove_roles(mute_role) else: member = mute.member, mute.member_name await send_to_changelog_mod(guild, None, Colors.unmute, t.log_unmuted, member, t.log_unmuted_expired) await Mute.deactivate(mute.id)
async def count(cls): if cls is Report: active = await db.count(filter_by(cls, reporter=user_id)) else: active = await db.count(filter_by(cls, mod=user_id)) passive = await db.count(filter_by(cls, member=user_id)) if cls is Kick: if auto_kicks := await db.count( filter_by(cls, member=user_id, mod=None)): return t.active_passive( active, passive - auto_kicks) + "\n" + t.autokicks(cnt=auto_kicks)
async def update(member): await Join.update(member.id, str(member), member.joined_at) relevant_join: Optional[Join] = await db.first( filter_by(Join, member=member.id).order_by(Join.timestamp.asc()) ) if not relevant_join: return timestamp = relevant_join.timestamp + timedelta(seconds=10) if await db.exists(filter_by(Verification, member=member.id, accepted=True, timestamp=timestamp)): return await db.add(Verification(member=member.id, member_name=str(member), accepted=True, timestamp=timestamp))
async def unmute(self, ctx: Context, user: UserMemberConverter, *, reason: str): """ unmute a user """ user: Union[Member, User] if len(reason) > 900: raise CommandError(t.reason_too_long) mute_role: Role = await get_mute_role(ctx.guild) was_muted = False if isinstance(user, Member) and mute_role in user.roles: was_muted = True check_role_assignable(mute_role) await user.remove_roles(mute_role) async for mute in await db.stream( filter_by(Mute, active=True, member=user.id)): await Mute.deactivate(mute.id, ctx.author.id, reason) was_muted = True if not was_muted: raise UserCommandError(user, t.not_muted) server_embed = Embed(title=t.unmute, description=t.unmuted_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.display_avatar.url) await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.unmute, t.log_unmuted, user, reason)
async def unban(self, ctx: Context, user: UserMemberConverter, *, reason: str): """ unban a user """ user: Union[Member, User] if len(reason) > 900: raise CommandError(t.reason_too_long) if not ctx.guild.me.guild_permissions.ban_members: raise CommandError(t.cannot_unban_permissions) was_banned = True try: await ctx.guild.unban(user, reason=reason) except HTTPException: was_banned = False async for ban in await db.stream( filter_by(Ban, active=True, member=user.id)): was_banned = True await Ban.deactivate(ban.id, ctx.author.id, reason) if not was_banned: raise UserCommandError(user, t.not_banned) server_embed = Embed(title=t.unban, description=t.unbanned_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.display_avatar.url) await reply(ctx, embed=server_embed) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.unban, t.log_unbanned, user, reason)
async def on_member_join(self, member: Member): mute_role: Optional[Role] = member.guild.get_role( await RoleSettings.get("mute")) if mute_role is None: return if await db.exists(filter_by(Mute, active=True, member=member.id)): await member.add_roles(mute_role)
async def handle_get_userlog_entries(self, user_id: int, _) -> list[tuple[datetime, str]]: out: list[tuple[datetime, str]] = [] deletion: MediaOnlyDeletion async for deletion in await db.stream(filter_by(MediaOnlyDeletion, member=user_id)): out.append((deletion.timestamp, t.ulog_deletion(f"<#{deletion.channel}>"))) return out
async def on_member_role_remove(self, member: Member, role: Role): if (member.id, role.id) in self.removed_perma_roles: self.removed_perma_roles.remove((member.id, role.id)) return if not await db.exists( filter_by(PermaRole, member_id=member.id, role_id=role.id)): return await reassign(member, role)
async def on_member_join(self, member: Member): guild: Guild = member.guild perma_role: PermaRole async for perma_role in await db.stream( filter_by(PermaRole, member_id=member.id)): if not (role := guild.get_role(perma_role.role_id)): await db.delete(perma_role) continue await reassign(member, role)
async def handle_get_ulog_entries(self, user_id: int, _): out = [] async for log in await db.stream( filter_by(InviteLog, applicant=user_id)): # type: InviteLog if log.approved: out.append((log.timestamp, t.ulog_invite_approved(f"<@{log.mod}>", log.guild_name))) else: out.append((log.timestamp, t.ulog_invite_removed(f"<@{log.mod}>", log.guild_name))) post: IllegalInvitePost async for post in await db.stream( filter_by(IllegalInvitePost, member=user_id)): out.append((post.timestamp, t.ulog_illegal_post(f"<#{post.channel}>", post.name))) return out
async def on_member_role_add(self, member: Member, role: Role): if role.id != await RoleSettings.get("verified"): return asyncio.create_task(self.update_verification_reaction(member, add=True)) last_verification: Optional[Verification] = await db.first( filter_by(Verification, member=member.id).order_by(Verification.timestamp.desc()) ) if last_verification and last_verification.accepted: return await Verification.create(member.id, str(member), True)
async def on_ready(self): guild: Guild = self.bot.guilds[0] mute_role: Optional[Role] = guild.get_role(await RoleSettings.get("mute")) if mute_role is not None: async for mute in await db.stream(filter_by(Mute, active=True)): member: Optional[Member] = guild.get_member(mute.member) if member is not None: await member.add_roles(mute_role) try: self.mod_loop.start() except RuntimeError: self.mod_loop.restart()
async def handle_get_ulog_entries(self, user_id: int, _): out = [] log: BadWordPost async for log in await db.stream(filter_by(BadWordPost, member=user_id)): if log.deleted_message: out.append((log.timestamp, t.ulog_message_deleted(log.content, log.channel))) else: out.append( (log.timestamp, t.ulog_message(log.content, log.channel))) return out
async def add(self, ctx: Context, regex: RegexConverter, delete: bool, *, description: str): regex: str if await db.exists(filter_by(BadWord, regex=regex)): raise CommandError(t.already_blacklisted) if len(description) > 500: raise CommandError(t.description_length) await BadWord.create(regex, description, delete) await add_reactions(ctx.message, "white_check_mark") await send_to_changelog( ctx.guild, t.log_content_filter_added(regex, ctx.author.mention))
async def roles_remove(self, ctx: Context, member: Member, *, role: Role): if role not in member.roles: raise CommandError(t.role_not_assigned) if not await is_authorized(ctx.author, role, perma=False): raise CommandError(t.role_not_authorized) check_role_assignable(role) if await db.exists( filter_by(PermaRole, member_id=member.id, role_id=role.id)): raise CommandError(t.cannot_remove_perma(await get_prefix())) await member.remove_roles(role) await ctx.message.add_reaction(name_to_emoji["white_check_mark"])
async def regex(self, ctx: Context, pattern: ContentFilterConverter, *, new_regex: RegexConverter): pattern: BadWord new_regex: str if await db.exists(filter_by(BadWord, regex=new_regex)): raise CommandError(t.already_blacklisted) old = pattern.regex pattern.regex = new_regex await sync_redis() await add_reactions(ctx.message, "white_check_mark") await send_to_changelog(ctx.guild, t.log_regex_updated(old, pattern.regex))
async def on_member_role_remove(self, member: Member, role: Role): link: RoleNotification async for link in await db.stream( filter_by(RoleNotification, role_id=role.id)): channel: Optional[TextChannel] = self.bot.get_channel( link.channel_id) if channel is None: continue role_name = role.mention if link.ping_role else f"`@{role}`" user_name = member.mention if link.ping_user else f"`@{member}`" try: await channel.send(t.rn_role_removed(role_name, user_name)) except Forbidden: await send_alert(member.guild, t.cannot_send(channel.mention))
async def joined(self, ctx: Context, member: Member = None): """ Returns a rough estimate for the user's time on the server """ member = member or ctx.author verification: Optional[Verification] = await db.first( filter_by(Verification, member=member.id).order_by(Verification.timestamp.desc()) ) ts: datetime = verification.timestamp if verification else member.joined_at embed = Embed( title=t.userinfo, description=f"{member.mention} {date_diff_to_str(utcnow(), ts)}", color=Colors.joined ) embed.set_author(name=str(member), icon_url=member.display_avatar.url) await reply(ctx, embed=embed)
async def reactionrole_reinialize(self, ctx: Context, msg: Message, emoji: Optional[EmojiConverter]): if emoji: emoji: PartialEmoji if not await ReactionRole.get(msg.channel.id, msg.id, str(emoji)): raise CommandError(t.rr_link_not_found) for reaction in msg.reactions: if str(reaction) == str(emoji): try: await reaction.clear() except Forbidden: raise CommandError(t.could_not_remove_reactions) break try: await msg.add_reaction(emoji) except Forbidden: raise CommandError(t.could_not_add_reactions) await add_reactions(ctx, "white_check_mark") return links: list[ReactionRole] = await db.all( filter_by(ReactionRole, channel_id=msg.channel.id, message_id=msg.id)) if not links: raise CommandError(t.rr_link_not_found) try: await msg.clear_reactions() except Forbidden: raise CommandError(t.could_not_remove_reactions) for link in links: try: await msg.add_reaction(link.emoji) except Forbidden: raise CommandError(t.could_not_add_reactions) await add_reactions(ctx, "white_check_mark")
async def role_notifications_add(self, ctx: Context, role: Role, channel: TextChannel, ping_role: bool, ping_user: bool): """ add a role notification link """ check_message_send_permissions(channel) if await db.exists( filter_by(RoleNotification, role_id=role.id, channel_id=channel.id)): raise CommandError(t.link_already_exists) await RoleNotification.create(role.id, channel.id, ping_role, ping_user) await ctx.message.add_reaction(name_to_emoji["white_check_mark"]) await send_to_changelog(ctx.guild, t.log_rn_created(role, channel.mention))
async def on_member_join(self, member: Member): self.join_events[member.id].clear() join: Join = await Join.create(member.id, str(member), member.joined_at.replace(microsecond=0)) async def trigger_join_event(): await db.wait_for_close_event() self.join_id[member.id] = join.id self.join_events[member.id].set() asyncio.create_task(trigger_join_event()) last_verification: Optional[Verification] = await db.first( filter_by(Verification, member=member.id).order_by(Verification.timestamp.desc()) ) if not last_verification or not last_verification.accepted: return role: Optional[Role] = member.guild.get_role(await RoleSettings.get("verified")) if role: await member.add_roles(role)
async def roles_perma_add(self, ctx: Context, member: UserMemberConverter, *, role: Role): member: Union[User, Member] if not await is_authorized(ctx.author, role, perma=True): raise CommandError(t.role_not_authorized) check_role_assignable(role) if await db.exists( filter_by(PermaRole, member_id=member.id, role_id=role.id)): raise CommandError(t.role_already_assigned) self.removed_perma_roles.discard((member.id, role.id)) await PermaRole.add(member.id, role.id) if isinstance(member, Member): await member.add_roles(role) await send_to_changelog( ctx.guild, t.added_perma_role(role.mention, member.mention, member)) await ctx.message.add_reaction(name_to_emoji["white_check_mark"])
async def roles_list(self, ctx: Context, *, role: Role): member_ids: set[int] = {member.id for member in role.members} perma: dict[int, str] = {} perma_role: PermaRole async for perma_role in await db.stream( filter_by(PermaRole, role_id=role.id)): try: user = await self.bot.fetch_user(perma_role.member_id) except NotFound: continue member_ids.add(user.id) perma[user.id] = str(user) members: list[Member] = [] for member_id in [*member_ids]: if not (member := ctx.guild.get_member(member_id)): continue members.append(member) member_ids.remove(member_id)
async def exists(channel_id: int) -> bool: return await db.exists(filter_by(LogExclude, channel_id=channel_id))
class RedditCog(Cog, name="Reddit"): CONTRIBUTORS = [ Contributor.Scriptim, Contributor.Defelo, Contributor.wolflu, Contributor.Anorak ] async def on_ready(self): interval = await RedditSettings.interval.get() await self.start_loop(interval) @tasks.loop() @db_wrapper async def reddit_loop(self): await self.pull_hot_posts() async def pull_hot_posts(self): logger.info("pulling hot reddit posts") limit = await RedditSettings.limit.get() async for reddit_channel in await db.stream(select(RedditChannel) ): # type: RedditChannel text_channel: Optional[TextChannel] = self.bot.get_channel( reddit_channel.channel) if text_channel is None: await db.delete(reddit_channel) continue try: check_message_send_permissions(text_channel, check_embed=True) except CommandError: await send_alert(self.bot.guilds[0], t.cannot_send(text_channel.mention)) continue posts = await fetch_reddit_posts(reddit_channel.subreddit, limit) if posts is None: await send_alert(self.bot.guilds[0], t.could_not_fetch(reddit_channel.subreddit)) continue for post in posts: if await RedditPost.post(post["id"]): await text_channel.send(embed=create_embed(post)) await RedditPost.clean() async def start_loop(self, interval): self.reddit_loop.cancel() self.reddit_loop.change_interval(hours=interval) try: self.reddit_loop.start() except RuntimeError: self.reddit_loop.restart() @commands.group() @RedditPermission.read.check @guild_only() async def reddit(self, ctx: Context): """ manage reddit integration """ if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.reddit, colour=Colors.Reddit) interval = await RedditSettings.interval.get() embed.add_field(name=t.interval, value=t.x_hours(cnt=interval)) limit = await RedditSettings.limit.get() embed.add_field(name=t.limit, value=str(limit)) filter_nsfw = await RedditSettings.filter_nsfw.get() embed.add_field(name=t.nsfw_filter, value=tg.enabled if filter_nsfw else tg.disabled, inline=False) out = [] async for reddit_channel in await db.stream(select(RedditChannel) ): # type: RedditChannel text_channel: Optional[TextChannel] = self.bot.get_channel( reddit_channel.channel) if text_channel is None: await db.delete(reddit_channel) else: sub = reddit_channel.subreddit out.append( f":small_orange_diamond: [r/{sub}](https://reddit.com/r/{sub}) -> {text_channel.mention}" ) embed.add_field(name=t.reddit_links, value="\n".join(out) or t.no_reddit_links, inline=False) await reply(ctx, embed=embed) @reddit.command(name="add", aliases=["a", "+"]) @RedditPermission.write.check async def reddit_add(self, ctx: Context, subreddit: str, channel: TextChannel): """ create a link between a subreddit and a channel """ if not (subreddit := await get_subreddit_name(subreddit)): raise CommandError(t.subreddit_not_found) check_message_send_permissions(channel, check_embed=True) if await db.exists( filter_by(RedditChannel, subreddit=subreddit, channel=channel.id)): raise CommandError(t.reddit_link_already_exists) await RedditChannel.create(subreddit, channel.id) embed = Embed(title=t.reddit, colour=Colors.Reddit, description=t.reddit_link_created) await reply(ctx, embed=embed) await send_to_changelog( ctx.guild, t.log_reddit_link_created(subreddit, channel.mention))
channel: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) @staticmethod async def add(channel: int): await redis.setex(f"mediaonly:channel={channel}", CACHE_TTL, 1) await db.add(MediaOnlyChannel(channel=channel)) @staticmethod async def exists(channel: int) -> bool: if result := await redis.get(key := f"mediaonly:channel={channel}"): return result == "1" result = await db.exists(filter_by(MediaOnlyChannel, channel=channel)) await redis.setex(key, CACHE_TTL, int(result)) return result @staticmethod async def stream() -> AsyncIterable[int]: row: MediaOnlyChannel async with redis.pipeline() as pipe: async for row in await db.stream(select(MediaOnlyChannel)): channel = row.channel await pipe.setex(f"mediaonly:channel={channel}", CACHE_TTL, 1) yield channel await pipe.execute() @staticmethod async def remove(channel: int):
async def handle_get_userlog_entries( self, user_id: int, author: Member) -> list[tuple[datetime, str]]: out: list[tuple[datetime, str]] = [] if await is_teamler(author): report: Report async for report in await db.stream( filter_by(Report, member=user_id)): out.append((report.timestamp, t.ulog.reported(f"<@{report.reporter}>", report.reason))) warn: Warn async for warn in await db.stream(filter_by(Warn, member=user_id)): out.append( (warn.timestamp, t.ulog.warned(f"<@{warn.mod}>", warn.reason))) mute: Mute async for mute in await db.stream(filter_by(Mute, member=user_id)): text = t.ulog.muted.upgrade if mute.is_upgrade else t.ulog.muted.first if mute.days == -1: out.append( (mute.timestamp, text.inf(f"<@{mute.mod}>", mute.reason))) else: out.append((mute.timestamp, text.temp(f"<@{mute.mod}>", mute.reason, cnt=mute.days))) if not mute.active and not mute.upgraded: if mute.unmute_mod is None: out.append( (mute.deactivation_timestamp, t.ulog.unmuted_expired)) else: out.append((mute.deactivation_timestamp, t.ulog.unmuted(f"<@{mute.unmute_mod}>", mute.unmute_reason))) kick: Kick async for kick in await db.stream(filter_by(Kick, member=user_id)): if kick.mod is not None: out.append((kick.timestamp, t.ulog.kicked(f"<@{kick.mod}>", kick.reason))) else: out.append((kick.timestamp, t.ulog.autokicked)) ban: Ban async for ban in await db.stream(filter_by(Ban, member=user_id)): text = t.ulog.banned.upgrade if ban.is_upgrade else t.ulog.banned.first if ban.days == -1: out.append((ban.timestamp, text.inf(f"<@{ban.mod}>", ban.reason))) else: out.append((ban.timestamp, text.temp(f"<@{ban.mod}>", ban.reason, cnt=ban.days))) if not ban.active and not ban.upgraded: if ban.unban_mod is None: out.append( (ban.deactivation_timestamp, t.ulog.unbanned_expired)) else: out.append((ban.deactivation_timestamp, t.ulog.unbanned(f"<@{ban.unban_mod}>", ban.unban_reason))) return out
async def mute(self, ctx: Context, user: UserMemberConverter, days: DurationConverter, *, reason: str): """ mute a user set days to `inf` for a permanent mute """ user: Union[Member, User] days: Optional[int] if len(reason) > 900: raise CommandError(t.reason_too_long) mute_role: Role = await get_mute_role(ctx.guild) if user == self.bot.user or await is_teamler(user): raise UserCommandError(user, t.cannot_mute) if isinstance(user, Member): check_role_assignable(mute_role) await user.add_roles(mute_role) await user.move_to(None) active_mutes: List[Mute] = await db.all( filter_by(Mute, active=True, member=user.id)) for mute in active_mutes: if mute.days == -1: raise UserCommandError(user, t.already_muted) ts = mute.timestamp + timedelta(days=mute.days) if days is not None and utcnow() + timedelta(days=days) <= ts: raise UserCommandError(user, t.already_muted) for mute in active_mutes: await Mute.upgrade(mute.id, ctx.author.id) user_embed = Embed(title=t.mute, colour=Colors.ModTools) server_embed = Embed(title=t.mute, description=t.muted_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.display_avatar.url) if days is not None: await Mute.create(user.id, str(user), ctx.author.id, days, reason, bool(active_mutes)) user_embed.description = t.muted(ctx.author.mention, ctx.guild.name, reason, cnt=days) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.mute, t.log_muted, user, reason, duration=t.log_field.days(cnt=days)) else: await Mute.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_mutes)) user_embed.description = t.muted_inf(ctx.author.mention, ctx.guild.name, reason) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.mute, t.log_muted, user, reason, duration=t.log_field.days_infinity) try: await user.send(embed=user_embed) except (Forbidden, HTTPException): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error await reply(ctx, embed=server_embed)
async def handle_can_respond_on_reaction(self, channel: TextChannel) -> bool: return not await db.exists(filter_by(MediaOnlyChannel, channel=channel.id))
async def ban(self, ctx: Context, user: UserMemberConverter, ban_days: DurationConverter, delete_days: int, *, reason: str): """ ban a user set ban_days to `inf` for a permanent ban """ ban_days: Optional[int] user: Union[Member, User] if not ctx.guild.me.guild_permissions.ban_members: raise CommandError(t.cannot_ban_permissions) if len(reason) > 900: raise CommandError(t.reason_too_long) if not 0 <= delete_days <= 7: raise CommandError(tg.invalid_duration) if user == self.bot.user or await is_teamler(user): raise UserCommandError(user, t.cannot_ban) if isinstance(user, Member) and (user.top_role >= ctx.guild.me.top_role or user.id == ctx.guild.owner_id): raise UserCommandError(user, t.cannot_ban) active_bans: List[Ban] = await db.all( filter_by(Ban, active=True, member=user.id)) for ban in active_bans: if ban.days == -1: raise UserCommandError(user, t.already_banned) ts = ban.timestamp + timedelta(days=ban.days) if ban_days is not None and utcnow() + timedelta( days=ban_days) <= ts: raise UserCommandError(user, t.already_banned) for ban in active_bans: await Ban.upgrade(ban.id, ctx.author.id) async for mute in await db.stream( filter_by(Mute, active=True, member=user.id)): await Mute.upgrade(mute.id, ctx.author.id) user_embed = Embed(title=t.ban, colour=Colors.ModTools) server_embed = Embed(title=t.ban, description=t.banned_response, colour=Colors.ModTools) server_embed.set_author(name=str(user), icon_url=user.display_avatar.url) if ban_days is not None: await Ban.create(user.id, str(user), ctx.author.id, ban_days, reason, bool(active_bans)) user_embed.description = t.banned(ctx.author.mention, ctx.guild.name, reason, cnt=ban_days) await send_to_changelog_mod( ctx.guild, ctx.message, Colors.ban, t.log_banned, user, reason, duration=t.log_field.days(cnt=ban_days)) else: await Ban.create(user.id, str(user), ctx.author.id, -1, reason, bool(active_bans)) user_embed.description = t.banned_inf(ctx.author.mention, ctx.guild.name, reason) await send_to_changelog_mod(ctx.guild, ctx.message, Colors.ban, t.log_banned, user, reason, duration=t.log_field.days_infinity) try: await user.send(embed=user_embed) except (Forbidden, HTTPException): server_embed.description = t.no_dm + "\n\n" + server_embed.description server_embed.colour = Colors.error await ctx.guild.ban(user, delete_message_days=delete_days, reason=reason) await revoke_verification(user) await reply(ctx, embed=server_embed)