async def autoclear(self, ctx: Context): if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.autoclear, colour=Colors.AutoClear) out = [] async for autoclear in await db.stream(select(AutoClearChannel)): channel: Optional[TextChannel] = self.bot.get_channel( autoclear.channel) if not channel: await db.delete(autoclear) continue out.append( f":small_orange_diamond: {channel.mention} ({t.minutes(cnt=autoclear.minutes)})" ) if not out: embed.description = t.no_autoclear_channels embed.colour = Colors.error else: embed.description = "\n".join(out) await send_long_embed(ctx, embed=embed)
async def on_raw_reaction_add(self, message: Message, emoji: PartialEmoji, member: Member): if str(emoji) != EMOJI or member.bot or message.guild is None: return access: bool = await ReactionPinPermission.pin.check_permissions(member ) if not (await db.exists( select(ReactionPinChannel).filter_by( channel=message.channel.id)) or access): return blocked_role = await RoleSettings.get("mute") if access or (member == message.author and all(r.id != blocked_role for r in member.roles)): if message.type not in (MessageType.default, MessageType.reply): await message.remove_reaction(emoji, member) await message.channel.send( embed=make_error(t.msg_not_pinned_system)) raise StopEventHandling check_channel(message.channel) try: await message.pin() except HTTPException: await message.remove_reaction(emoji, member) await message.channel.send( embed=make_error(t.msg_not_pinned_limit)) else: await message.remove_reaction(emoji, member) raise StopEventHandling
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 aoc_link(self, ctx: Context): """ manage links between discord members and aoc users on the private leaderboard """ if len(ctx.message.content.lstrip(ctx.prefix).split()) > 2: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.links, colour=Colors.AdventOfCode) leaderboard = await AOCConfig.get_leaderboard() out = [] async for link in await db.stream(select(AOCLink)): # type: AOCLink if link.aoc_id not in leaderboard["members"]: continue if not (user := self.bot.get_user(link.discord_id)): continue member = leaderboard["members"][link.aoc_id] if member["name"]: name = f"{member['name']} (#{member['id']})" else: name = f"(anonymous user #{member['id']})" out.append(f"{name} = <@{link.discord_id}> (`@{user}`)")
async def role_notifications(self, ctx: Context): """ manage role notifications """ if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.rn_links, color=Colors.RoleNotifications) out = [] guild: Guild = ctx.guild link: RoleNotification async for link in await db.stream(select(RoleNotification)): if guild.get_channel(link.channel_id) is None or guild.get_role( link.role_id) is None: await db.delete(link) continue flags = [t.rn_ping_role] * link.ping_role + [t.rn_ping_user ] * link.ping_user out.append(f"<@&{link.role_id}> -> <#{link.channel_id}>" + (" (" + ", ".join(flags) + ")") * bool(flags)) if not out: embed.description = t.rn_no_links embed.colour = Colors.error else: embed.description = "\n".join(out) await send_long_embed(ctx, embed)
async def news_auth_list(self, ctx: Context): """ list authorized users and channels """ out = [] guild: Guild = ctx.guild async for authorization in await db.stream(select(NewsAuthorization)): text_channel: Optional[TextChannel] = guild.get_channel( authorization.channel_id) member: Optional[Member] = guild.get_member(authorization.user_id) if text_channel is None or member is None: await db.delete(authorization) continue line = f":small_orange_diamond: {member.mention} -> {text_channel.mention}" if authorization.notification_role_id is not None: role: Optional[Role] = guild.get_role( authorization.notification_role_id) if role is None: await db.delete(authorization) continue line += f" ({role.mention})" out.append(line) embed = Embed(title=t.news, colour=Colors.News) if out: embed.description = "\n".join(out) else: embed.colour = Colors.error embed.description = t.no_news_authorizations await send_long_embed(ctx, embed)
async def list_topics(guild: Guild) -> List[Role]: roles: List[Role] = [] async for btp_role in await db.stream(select(BTPRole)): if (role := guild.get_role(btp_role.role_id)) is None: await db.delete(btp_role) else: roles.append(role)
async def reactionpin_remove(self, ctx: Context, channel: TextChannel): """ remove channel from whitelist """ if not (row := await db.first( select(ReactionPinChannel).filter_by(channel=channel.id))): raise CommandError(t.channel_not_whitelisted)
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()
async def on_ready(self): guild: Guild = self.bot.guilds[0] async for perma_role in await db.stream(select(PermaRole)): if not (role := guild.get_role(perma_role.role_id)): await db.delete(perma_role) continue if not (member := guild.get_member(perma_role.member_id)): continue
async def news_send(self, ctx: Context, channel: TextChannel, color: Optional[Color] = None, *, message: Optional[str]): """ send a news message """ authorization: Optional[NewsAuthorization] = await db.first( select(NewsAuthorization).filter_by(user_id=ctx.author.id, channel_id=channel.id)) if authorization is None: raise CommandError(t.news_you_are_not_authorized) if message is None: message = "" embed = Embed(title=t.news, colour=Colors.News, description="") if not message and not ctx.message.attachments: embed.description = t.send_message await reply(ctx, embed=embed) message, files = await read_normal_message(self.bot, ctx.channel, ctx.author) else: files = [ await attachment_to_file(attachment) for attachment in ctx.message.attachments ] content = "" send_embed = Embed(title=t.news, description=message, colour=Colors.News) send_embed.set_footer(text=t.sent_by(ctx.author, ctx.author.id), icon_url=ctx.author.display_avatar.url) if authorization.notification_role_id is not None: role: Optional[Role] = ctx.guild.get_role( authorization.notification_role_id) if role is not None: content = role.mention send_embed.colour = color if color is not None else Colors.News if files and any(files[0].filename.lower().endswith(ext) for ext in ["jpg", "jpeg", "png", "gif"]): send_embed.set_image(url="attachment://" + files[0].filename) try: await channel.send(content=content, embed=send_embed, files=files) except (HTTPException, Forbidden): raise CommandError(t.msg_could_not_be_sent) else: embed.description = t.msg_sent await reply(ctx, embed=embed)
async def loop(self): autoclear: AutoClearChannel async for autoclear in await db.stream(select(AutoClearChannel)): channel = self.bot.get_channel(autoclear.channel) if not channel: await db.delete(autoclear) continue asyncio.create_task( clear_channel(channel, autoclear.minutes, limit=200))
async def verification(self, ctx: Context): """ configure verify command """ if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return password: str = await VerificationSettings.password.get() normal: List[Role] = [] reverse: List[Role] = [] async for vrole in await db.stream(select(VerificationRole) ): # type: VerificationRole role: Optional[Role] = ctx.guild.get_role(vrole.role_id) if role is None: await db.delete(vrole) else: [normal, reverse][vrole.reverse].append(role) embed = Embed(title=t.verification, colour=Colors.error) if not password or not normal + reverse: embed.add_field(name=tg.status, value=t.verification_disabled, inline=False) await reply(ctx, embed=embed) return embed.colour = Colors.Verification embed.add_field(name=tg.status, value=t.verification_enabled, inline=False) embed.add_field(name=t.password, value=f"`{password}`", inline=False) delay: int = await VerificationSettings.delay.get() val = t.x_seconds(cnt=delay) if delay != -1 else tg.disabled embed.add_field(name=tg.delay, value=val, inline=False) if normal: embed.add_field(name=t.roles_normal, value="\n".join( f":small_orange_diamond: {role.mention}" for role in normal)) if reverse: embed.add_field(name=t.roles_reverse, value="\n".join( f":small_blue_diamond: {role.mention}" for role in reverse)) await reply(ctx, embed=embed)
async def on_raw_reaction_remove(self, message: Message, emoji: PartialEmoji, member: Member): if str(emoji) != EMOJI or member.bot or message.guild is None: return access: bool = await ReactionPinPermission.pin.check_permissions(member ) is_reactionpin_channel = await db.exists( select(ReactionPinChannel).filter_by(channel=message.channel.id)) if message.pinned and (access or (is_reactionpin_channel and member == message.author)): check_channel(message.channel) await message.unpin() raise StopEventHandling
async def roles_auth(self, ctx: Context): if len(ctx.message.content.lstrip(ctx.prefix).split()) > 2: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.role_auth, colour=Colors.Roles) members: Dict[Member, List[tuple[Role, bool]]] = {} roles: Dict[Role, List[tuple[Role, bool]]] = {} auth: RoleAuth async for auth in await db.stream(select(RoleAuth)): source: Optional[Union[Member, Role]] = ctx.guild.get_member( auth.source) or ctx.guild.get_role(auth.source) target: Optional[Role] = ctx.guild.get_role(auth.target) if source is None or target is None: await db.delete(auth) else: [members, roles][isinstance(source, Role)].setdefault(source, []).append( (target, auth.perma_allowed)) if not members and not roles: embed.description = t.no_role_auth embed.colour = Colors.error await reply(ctx, embed=embed) return def make_field( auths: Dict[Union[Member, Role], List[tuple[Role, bool]]] ) -> List[str]: out = [] for src, targets in sorted(auths.items(), key=lambda a: a[0].name): line = f":small_orange_diamond: {src.mention} -> " line += ", ".join(role.mention + " :shield:" * perma for role, perma in targets) out.append(line) return out if roles: embed.add_field(name=t.role_auths, value="\n".join(make_field(roles)), inline=False) if members: embed.add_field(name=t.user_auths, value="\n".join(make_field(members)), inline=False) await reply(ctx, embed=embed)
async def register_topics(self, ctx: Context, *, topics: str): """ register one or more new topics """ guild: Guild = ctx.guild names = split_topics(topics) if not names: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_{|}~") to_be_created: List[str] = [] roles: List[Role] = [] for topic in names: if len(topic) > 100: raise CommandError(t.topic_too_long(topic)) if any(c not in valid_chars for c in topic): raise CommandError(t.topic_invalid_chars(topic)) for role in guild.roles: if role.name.lower() == topic.lower(): break else: to_be_created.append(topic) continue if await db.exists(select(BTPRole).filter_by(role_id=role.id)): raise CommandError(t.topic_already_registered(topic)) check_role_assignable(role) roles.append(role) for name in to_be_created: roles.append(await guild.create_role(name=name, mentionable=True)) for role in roles: await BTPRole.create(role.id) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_registered(cnt=len(roles)) await send_to_changelog( ctx.guild, t.log_topics_registered(cnt=len(roles), topics=", ".join(f"`{r}`" for r in roles))) await reply(ctx, embed=embed)
async def handle_get_userlog_entries( self, user_id: int, author: Member) -> list[tuple[datetime, str]]: if not await is_teamler(author): return [] out: list[tuple[datetime, str]] = [] note: UserNote async for note in await db.stream( select(UserNote).filter_by(member_id=user_id)): out.append( (note.timestamp, t.ulog_entry(f"<@{note.author_id}>", "\n" * ("\n" in note.content) + note.content))) return out
async def reactionrole(self, ctx: Context): if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.reactionrole, colour=Colors.ReactionRole) channels: Dict[TextChannel, Dict[Message, Set[str]]] = {} message_cache: Dict[Tuple[int, int], Message] = {} async for link in await db.stream(select(ReactionRole) ): # type: ReactionRole channel: Optional[TextChannel] = ctx.guild.get_channel( link.channel_id) if channel is None: await db.delete(link) continue key = link.channel_id, link.message_id if key not in message_cache: try: message_cache[key] = await channel.fetch_message( link.message_id) except HTTPException: await db.delete(link) continue msg = message_cache[key] if ctx.guild.get_role(link.role_id) is None: await db.delete(link) continue channels.setdefault(channel, {}).setdefault(msg, set()) channels[channel][msg].add(link.emoji) if not channels: embed.colour = Colors.error embed.description = t.no_reactionrole_links else: out = [] for channel, messages in channels.items(): value = channel.mention + "\n" for msg, emojis in messages.items(): value += f"[{msg.id}]({msg.jump_url}): {' '.join(emojis)}\n" out.append(value) embed.description = "\n".join(out) await send_long_embed(ctx, embed)
async def sync_redis() -> list[str]: out = [] async with redis.pipeline() as pipe: await pipe.delete(key := "content_filter") regex: BadWord async for regex in await db.stream(select(BadWord)): out.append(regex.regex) await pipe.lpush(key, regex.regex) await pipe.lpush(key, "") await pipe.expire(key, CACHE_TTL) await pipe.execute() return out
async def roles_perma_list(self, ctx: Context): guild: Guild = ctx.guild role_users: dict[Role, list[User]] = {} perma_role: PermaRole async for perma_role in await db.stream(select(PermaRole)): if not (role := guild.get_role(perma_role.role_id)): await db.delete(perma_role) continue try: user = await self.bot.fetch_user(perma_role.member_id) except NotFound: await db.delete(perma_role) continue role_users.setdefault(role, []).append(user)
async def is_authorized(author: Member, target_role: Role, *, perma: bool) -> bool: if not perma and author.guild_permissions.manage_roles and target_role < author.top_role: return True if await RolesPermission.auth_write.check_permissions(author): return True roles = {role.id for role in author.roles} | {author.id} auth: RoleAuth async for auth in await db.stream( select(RoleAuth).filter_by(target=target_role.id)): if perma and not auth.perma_allowed: continue if auth.source in roles: return True return False
async def news_auth_remove(self, ctx: Context, user: Member, channel: TextChannel): """ remove user authorization """ authorization: Optional[NewsAuthorization] = await db.first( select(NewsAuthorization).filter_by(user_id=user.id, channel_id=channel.id)) if authorization is None: raise CommandError(t.news_not_authorized) await db.delete(authorization) embed = Embed(title=t.news, colour=Colors.News, description=t.news_unauthorized) await reply(ctx, embed=embed) await send_to_changelog( ctx.guild, t.log_news_unauthorized(user.mention, channel.mention))
async def reactionpin_add(self, ctx: Context, channel: TextChannel): """ add channel to whitelist """ if not channel.permissions_for(ctx.guild.me).manage_messages: raise CommandError(t.no_permission) if await db.exists( select(ReactionPinChannel).filter_by(channel=channel.id)): raise CommandError(t.channel_already_whitelisted) await ReactionPinChannel.create(channel.id) embed = Embed(title=t.reactionpin, colour=Colors.ReactionPin, description=t.channel_whitelisted) await reply(ctx, embed=embed) await send_to_changelog(ctx.guild, t.log_channel_whitelisted_rp(channel.mention))
async def invites_list(self, ctx: Context): """ list allowed discord servers """ out = [] async for row in await db.stream(select(AllowedInvite)): out.append( f":small_orange_diamond: {row.guild_name} ({row.guild_id})") out.sort() embed = Embed(title=t.allowed_servers_title, colour=Colors.error) embed.description = t.allowed_servers_description if out: embed.colour = Colors.Invites embed.description += "\n".join(out) await send_long_embed(ctx, embed, paginate=True) else: embed.description = t.no_server_allowed await reply(ctx, embed=embed)
async def check(self, ctx: Context, pattern: ContentFilterConverter | int | RegexConverter, *, test_string: str): filters: list[BadWord | str] if isinstance(pattern, (BadWord, str)): filters = [pattern] elif pattern == -1: filters = await db.all(select(BadWord)) else: raise CommandError(t.invalid_pattern) out = [] for rule in filters: regex = rule.regex if isinstance(rule, BadWord) else rule if not (matches := findall(regex, test_string)): continue line = f"{rule.id}: " if isinstance(rule, BadWord) else "" line += f'`{regex}` -> {", ".join(f"`{m}`" for m in sorted(set(matches)))}' out.append(line)
async def unregister_roles(ctx: Context, topics: str, *, delete_roles: bool): guild: Guild = ctx.guild roles: List[Role] = [] btp_roles: List[BTPRole] = [] names = split_topics(topics) if not names: raise UserInputError for topic in names: for role in guild.roles: if role.name.lower() == topic.lower(): break else: raise CommandError(t.topic_not_registered(topic)) if (btp_role := await db.first( select(BTPRole).filter_by(role_id=role.id))) is None: raise CommandError(t.topic_not_registered(topic)) roles.append(role) btp_roles.append(btp_role)
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)
async def news_auth_add(self, ctx: Context, user: Member, channel: TextChannel, notification_role: Optional[Role]): """ authorize a new user to send news to a specific channel """ if await db.exists( select(NewsAuthorization).filter_by(user_id=user.id, channel_id=channel.id)): raise CommandError(t.news_already_authorized) if not channel.permissions_for(channel.guild.me).send_messages: raise CommandError(t.news_not_added_no_permissions) role_id = notification_role.id if notification_role is not None else None await NewsAuthorization.create(user.id, channel.id, role_id) embed = Embed(title=t.news, colour=Colors.News, description=t.news_authorized) await reply(ctx, embed=embed) await send_to_changelog( ctx.guild, t.log_news_authorized(user.mention, channel.mention))
async def convert(self, ctx: Context, argument: str) -> AllowedInvite: try: invite: Invite = await ctx.bot.fetch_invite(argument) if invite.guild is None: raise CommandError(t.invalid_invite) row = await db.get(AllowedInvite, guild_id=invite.guild.id) if row is not None: return row except (NotFound, HTTPException): pass if argument.isnumeric(): row = await db.get(AllowedInvite, guild_id=int(argument)) if row is not None: return row async for row in await db.stream(select(AllowedInvite) ): # type: AllowedInvite if row.guild_name.lower().strip() == argument.lower().strip( ) or row.code == argument: return row raise CommandError(t.allowed_server_not_found)
async def content_filter(self, ctx: Context): if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: raise UserInputError return embed = Embed(title=t.bad_word_list_header, colour=Colors.ContentFilter) reg: BadWord async for reg in await db.stream(select(BadWord)): embed.add_field( name=t.embed_field_name(reg.id, reg.description), value=t.embed_field_value( reg.regex, t.delete if reg.delete else t.not_delete), inline=False, ) if not embed.fields: embed.colour = Colors.error embed.description = t.no_pattern_listed await send_long_embed(ctx, embed, paginate=True, max_fields=6)