From 93ca4386fd2cc9b9d864a1eaabbe215c3f807955 Mon Sep 17 00:00:00 2001 From: eek7574 Date: Wed, 30 Nov 2022 21:01:16 +0000 Subject: [PATCH] Add starboard --- cogs/assignments.py | 8 +-- cogs/starboard.py | 140 ++++++++++++++++++++++++++++++++++++++++++++ main.py | 11 +++- utils/db.py | 17 ++++++ utils/views.py | 16 ++--- 5 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 cogs/starboard.py diff --git a/cogs/assignments.py b/cogs/assignments.py index 2dcdef2..d6f2e03 100644 --- a/cogs/assignments.py +++ b/cogs/assignments.py @@ -370,17 +370,15 @@ class AssignmentsCog(commands.Cog): else: option = 10080 name = textwrap.shorten(modal.create_kwargs["title"], width=100, placeholder="...") - tag = discord.utils.get(channel.available_tags, name=modal.create_kwargs['tutor'].name.title()) + tag = discord.utils.get(channel.available_tags, name=modal.create_kwargs["tutor"].name.title()) await channel.create_thread( name=name, content="Assignment name: {0}\nDue: {1} ({4})\nTutor: {2}\nCreated by: {3}".format( modal.create_kwargs["title"], - discord.utils.format_dt( - due_dt, style="F" - ), + discord.utils.format_dt(due_dt, style="F"), modal.create_kwargs["tutor"].name, ctx.user.mention, - discord.utils.format_dt(due_dt, "R") + discord.utils.format_dt(due_dt, "R"), ), auto_archive_duration=option, applied_tags=[tag] if tag else [], diff --git a/cogs/starboard.py b/cogs/starboard.py new file mode 100644 index 0000000..1b19046 --- /dev/null +++ b/cogs/starboard.py @@ -0,0 +1,140 @@ +import asyncio +import textwrap +from typing import Tuple + +import discord +from discord.ext import commands +from utils.db import StarBoardMessage + + +class StarBoardCog(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.lock = asyncio.Lock() + + async def generate_starboard_embed(self, message: discord.Message) -> discord.Embed: + star_count = [x for x in message.reactions if str(x.emoji) == "\N{white medium star}"] + if not star_count: + star_count = 0 + else: + star_count = star_count[0].count + # noinspection PyUnresolvedReferences + cap = (message.channel if "thread" in message.channel.type.name else message.guild).member_count * 0.1 + embed = discord.Embed(colour=discord.Colour.gold(), timestamp=message.created_at, description=message.content) + embed.set_author( + name=message.author.display_name, url=message.jump_url, icon_url=message.author.display_avatar.url + ) + + if star_count > 5: + stars = "\N{white medium star}x{:,}".format(star_count) + else: + stars = "\N{white medium star}" * star_count + stars = stars or "\N{no entry sign}" + + embed.add_field( + name="Info", + value=f"Star count: {stars}\n" + f"Channel: {message.channel.mention}\n" + f"Author: {message.author.mention}\n" + f"URL: [jump]({message.jump_url})\n" + f"Sent: {discord.utils.format_dt(message.created_at, 'R')}", + inline=False, + ) + if message.edited_at: + embed.fields[0].value += "\nLast edited: " + discord.utils.format_dt(message.edited_at, "R") + + if message.reference is not None: + try: + ref: discord.Message = await self.bot.get_channel(message.reference.channel_id).fetch_message( + message.reference.message_id + ) + except discord.HTTPException: + pass + else: + embed.add_field( + name="In reply to", + value=f"[Message by {ref.author.display_name}]({ref.jump_url}):\n>>> ", + inline=False, + ) + field = embed.fields[1] + if not ref.content: + embed.fields[1].value = field.value.replace(":\n>>> ", "") + else: + embed.fields[1].value += textwrap.shorten(ref.content, 1024 - len(field.value), placeholder="...") + + if message.attachments: + for file in message.attachments: + name = f"Attachment #{message.attachments.index(file)}" + spoiler = file.is_spoiler() + if not spoiler and file.url.lower().endswith(("png", "jpeg", "jpg", "gif", "webp")) and not embed.image: + embed.set_image(url=file.url) + elif spoiler: + embed.add_field(name=name, value=f"||[{file.filename}]({file.url})||", inline=False) + else: + embed.add_field(name=name, value=f"[{file.filename}]({file.url})", inline=False) + + embed.set_footer(text="Starboard threshold for this message was {:.2f}.".format(cap)) + return embed + + @commands.Cog.listener("on_raw_reaction_add") + @commands.Cog.listener("on_raw_reaction_remove") + async def on_star_add(self, payload: discord.RawReactionActionEvent): + async with self.lock: + if str(payload.emoji) != "\N{white medium star}": + return + message: discord.Message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + star_count = [x for x in message.reactions if str(x.emoji) == "\N{white medium star}"] + if not star_count: + star_count = 0 + else: + star_count = star_count[0].count + database: Tuple[StarBoardMessage, bool] = await StarBoardMessage.objects.get_or_create( + {"channel": payload.channel_id}, id=payload.message_id + ) + entry, created = database + if created: + # noinspection PyUnresolvedReferences + cap = message.channel if "thread" in message.channel.type.name else message.guild + if self.bot.intents.members and hasattr(cap, "members"): + cap = len([x for x in cap.members if not x.bot]) * 0.1 + else: + cap = cap.member_count * 0.1 + if star_count >= cap: + channel = discord.utils.get(message.guild.text_channels, name="starboard") + if channel and channel.can_send(): + msg = await channel.send(embed=await self.generate_starboard_embed(message)) + await entry.update(starboard_message=msg.id) + else: + await entry.delete() + return + else: + channel = discord.utils.get(message.guild.text_channels, name="starboard") + if channel and channel.can_send() and entry.starboard_message: + try: + msg = await channel.fetch_message(entry.starboard_message) + except discord.NotFound: + msg = await channel.send(embed=await self.generate_starboard_embed(message)) + await entry.update(starboard_message=msg.id) + except discord.HTTPException: + pass + else: + await msg.edit(embed=await self.generate_starboard_embed(message)) + + @commands.message_command(name="Starboard Info") + @discord.guild_only() + async def get_starboard_info(self, ctx: discord.ApplicationContext, message: discord.Message): + return await ctx.respond(embed=await self.generate_starboard_embed(message)) + + @commands.command(name="threshold") + @commands.guild_only() + async def get_threshold(self, ctx: commands.Context): + """Shows you the current starboard threshold""" + if self.bot.intents.members and hasattr(ctx.channel, "members"): + cap = len([x for x in ctx.channel.members if not x.bot]) * 0.1 + else: + cap = ctx.channel.member_count * 0.1 + return await ctx.reply(f"Messages currently need {c:.2f} stars in this channel to be posted to the starboard.") + + +def setup(bot): + bot.add_cog(StarBoardCog(bot)) diff --git a/main.py b/main.py index 089f16a..b811021 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,16 @@ bot = commands.Bot( ) bot.training_lock = Lock() -extensions = ["jishaku", "cogs.verify", "cogs.mod", "cogs.events", "cogs.assignments", "cogs.timetable", "cogs.other"] +extensions = [ + "jishaku", + "cogs.verify", + "cogs.mod", + "cogs.events", + "cogs.assignments", + "cogs.timetable", + "cogs.other", + "cogs.starboard", +] for ext in extensions: bot.load_extension(ext) console.log(f"Loaded extension [green]{ext}") diff --git a/utils/db.py b/utils/db.py index 2957a66..165675b 100644 --- a/utils/db.py +++ b/utils/db.py @@ -119,3 +119,20 @@ class Assignments(orm.Model): submitted: bool assignees: list[int] # description: Optional[str] + + +class StarBoardMessage(orm.Model): + tablename = "starboard" + registry = registry + fields = { + "entry_id": orm.UUID(primary_key=True, default=uuid.uuid4), + "id": orm.BigInteger(unique=True), + "channel": orm.BigInteger(), + "starboard_message": orm.BigInteger(default=None, allow_null=True), + } + + if TYPE_CHECKING: + entry_id: uuid.UUID + id: int + channel: int + starboard_message: int | None diff --git a/utils/views.py b/utils/views.py index 34be922..7ed321b 100644 --- a/utils/views.py +++ b/utils/views.py @@ -57,9 +57,7 @@ class VerifyView(View): f" (originally associated with account {ban.associated_account})" ) await Student.objects.create( - id=existing.student_id, - user_id=interaction.user.id, - name=existing.name + id=existing.student_id, user_id=interaction.user.id, name=existing.name ) await existing.delete() role = discord.utils.find(lambda r: r.name.lower() == "verified", interaction.guild.roles) @@ -67,10 +65,7 @@ class VerifyView(View): if role and role < interaction.guild.me.top_role: await member.add_roles(role, reason="Verified") try: - await member.edit( - nick=f"{existing.name}", - reason="Verified" - ) + await member.edit(nick=f"{existing.name}", reason="Verified") except discord.HTTPException: pass console.log(f"[green]{interaction.user} verified ({interaction.user.id}/{existing.student_id})") @@ -118,16 +113,15 @@ class VerifyView(View): return await interaction.followup.send( "\N{cross mark} Invalid student ID - Failed to verify with regex." " Please try again with a valid student ID. Make sure it is formatted as `BXXXXXX` " - "(e.g. `B{}`)".format(''.join(str(random.randint(0, 9)) for _ in range(6))), - delete_after=60 + "(e.g. `B{}`)".format("".join(str(random.randint(0, 9)) for _ in range(6))), + delete_after=60, ) ex = await get_or_none(Student, id=st) if ex: btn.disabled = False return await interaction.followup.send( - "\N{cross mark} Student ID is already associated.", - delete_after=60 + "\N{cross mark} Student ID is already associated.", delete_after=60 ) try: