Add starboard to bot
All checks were successful
Build and Publish Jimmy.2 / build_and_publish (push) Successful in 33s

This commit is contained in:
Nexus 2024-05-05 02:27:41 +01:00
parent 808d621fb6
commit da7d122f14
Signed by: nex
GPG key ID: 0FA334385D0B689F
4 changed files with 227 additions and 5 deletions

View file

@ -62,7 +62,8 @@ foo = "bar"
# Redis configuration. If you are using the docker-compose setup, then you do not need to configure this.
host = "redis" # the host[name or IP] of the redis server
port = 6379 # self-explanatory
no_ping = false # disables the startup health-check. Not recommended to set to true.
# the no_ping option was removed after g+808D621F. All options in this section are now passed directly to redis.Redis().
# See the python redis docs for more info.
[responder]
# Configures the auto-responder.

View file

@ -265,10 +265,7 @@ class ChatHistory:
def __init__(self):
self._internal = {}
self.log = logging.getLogger("jimmy.cogs.ollama.history")
no_ping = CONFIG["redis"].pop("no_ping", False)
self.redis = redis.Redis(**CONFIG["redis"])
if no_ping is False:
assert self.redis.ping(), "Redis appears to be offline."
self.redis = redis.Redis(**CONFIG["redis"], db="jimmy-v2-ollama")
def load_thread(self, thread_id: str):
value: str = self.redis.get("threads:" + thread_id)

216
src/cogs/starboard.py Normal file
View file

@ -0,0 +1,216 @@
import logging
import discord
import json
from discord.ext import commands
from redis.asyncio import Redis
from conf import CONFIG
class Starboard(commands.Cog):
DOWNVOTE_EMOJI = discord.PartialEmoji.from_str("\N{collision symbol}")
def __init__(self, bot):
self.bot: commands.Bot = bot
self.config = CONFIG["starboard"]
self.emoji = discord.PartialEmoji.from_str(self.config.get("emoji", "\N{white medium star}"))
self.log = logging.getLogger("jimmy.cogs.starboard")
self.redis = Redis(**CONFIG["redis"], db="jimmy-v2-starboard")
async def generate_starboard_embed(self, message: discord.Message) -> tuple[discord.Embed, int]:
"""
Generates an embed ready for a starboard message.
:param message: The message to base off of.
:return: The created embed
"""
reactions: list[discord.Reaction] = [x for x in message.reactions if x.emoji == self.emoji]
downvote_reactions = [x for x in message.reactions if x.emoji == self.DOWNVOTE_EMOJI]
if not reactions:
# Nobody has added the star reaction.
star_count = 0
else:
# Count the number of reactions
star_count = sum([x.count for x in reactions])
if downvote_reactions:
star_count -= sum([x.count for x in downvote_reactions])
if star_count >= 0:
star_emoji_count = (str(self.emoji) * star_count)[:10]
else:
star_emoji_count = str(self.DOWNVOTE_EMOJI) * star_count
embed = discord.Embed(
colour=discord.Colour.gold(),
url=message.jump_url,
timestamp=message.created_at,
author=discord.EmbedAuthor(
message.author.display_name,
message.author.jump_url,
message.author.display_avatar.url
),
fields=[
discord.EmbedField(
name="Info",
value=f"[Stars: {star_emoji_count} ({star_count:,})]({message.jump_url})"
)
]
)
if message.content:
embed.description = message.content
elif message.embeds:
for message_embed in message.embeds:
if message_embed.type != "rich":
if message_embed.type == "image":
if message_embed.thumbnail and message_embed.thumbnail.proxy_url:
embed.set_image(url=message_embed.thumbnail.proxy_url)
continue
if message_embed.description:
embed.description = message_embed.description
if not message.attachments and not embed.description:
raise ValueError("Message does not appear to contain any text, embeds, or attachments.")
if message.attachments:
new_fields = []
for n, attachment in reversed(tuple(enumerate(message.attachments, start=1))):
attachment: discord.Attachment
if attachment.size >= 1024 * 1024:
size = f"{attachment.size / 1024 / 1024:,.1f}MiB"
elif attachment.size >= 1024:
size = f"{attachment.size / 1024:,.1f}KiB"
else:
size = f"{attachment.size:,} bytes"
new_fields.append(
{
"name": "Attachment #%d:" % n,
"value": f"[{attachment.filename} ({size})]({attachment.url})",
"inline": True
}
)
if attachment.content_type.startswith("image/"):
embed.set_image(url=attachment.url)
new_fields.reverse()
for field in new_fields:
embed.add_field(**field)
# This whacky reverse -> perform -> reverse basically just means we can set the first image/*
# attachment as the image.
return embed, star_count
async def get_or_fetch_message(self, channel_id: int, message_id: int) -> discord.Message:
"""
Fetches a message from cache where possible, falling back to the API.
"""
message: discord.Message | None = discord.utils.get(self.bot.cached_messages, id=message_id)
if not message:
message: discord.Message = await self.bot.get_channel(channel_id).fetch_message(message_id)
return message
@commands.Cog.listener("on_raw_reaction_add")
@commands.Cog.listener("on_raw_reaction_remove")
async def handle_raw_reaction(self, payload: discord.RawReactionActionEvent):
if self.config.get("enabled", True) is not True:
self.log.debug("Starboard is disabled.")
return
if not payload.guild_id:
# A direct message - has no starboard
return
elif payload.emoji != self.emoji:
# Was not a star emoji, ignore.
return
if not await self.redis.ping():
# Could not contact redis, not much point trying anything else without the database
return self.log.critical("Failed to contact redis (ping failed)!")
guild: discord.Guild = self.bot.get_guild(payload.guild_id)
starboard_channel: discord.TextChannel | None = discord.utils.get(
guild.text_channels,
name="starboard"
)
if not starboard_channel:
self.log.warning("Could not find starboard channel in %s (%d)", guild, guild.id)
return
message = await self.get_or_fetch_message(payload.channel_id, payload.message_id)
if payload.user_id == message.author.id:
self.log.info("%s tried to star their own message.", message.author)
return await message.reply(
"You can't star your own message you pretentious dick. Go outside, %s." % message.author.mention,
delete_after=30
)
embed, star_count = await self.generate_starboard_embed(message)
data = await self.redis.get(str(message.id))
if data is None:
if star_count <= 0:
self.log.info("%s has no stars, not processing.", message.jump_url)
return
data = {
"source_guild_id": payload.guild_id,
"source_channel_id": payload.channel_id,
"source_message_id": payload.message_id,
"history": [],
"starboard_channel_id": starboard_channel,
"starboard_message_id": None
}
if not starboard_channel.can_send(embed):
self.log.warning(
"Cannot send starboard messages in %d, %d (#%s @ %s)",
starboard_channel.id,
payload.guild_id,
starboard_channel.name,
guild.name
)
return
starboard_message = await starboard_channel.send(
embed=embed,
silent=True
)
data["starboard_message_id"] = starboard_message.id
await self.redis.set(str(message.id), json.dumps(data))
else:
try:
starboard_message = await self.get_or_fetch_message(
data["starboard_channel_id"],
data["starboard_message_id"]
)
except discord.NotFound:
if star_count <= 0:
return
starboard_message = await starboard_channel.send(
embed=embed,
silent=True
)
data["starboard_message_id"] = starboard_message.id
data["starboard_channel_id"] = starboard_message.channel.id
await self.redis.set(str(message.id), json.dumps(data))
else:
if star_count <= 0:
await starboard_message.delete(delay=0.1, reason="Lost all stars.")
self.log.info(
"Deleted message %s in %s, %s - lost all stars",
starboard_message.id,
starboard_message.channel.name,
starboard_message.guild.name
)
elif starboard_message.embeds[0] != embed:
await starboard_message.edit(embed=embed)
@commands.message_command(name="Preview Starboard Message")
async def preview_starboard_message(self, ctx: discord.ApplicationContext, message: discord.Message):
embed, stars = await self.generate_starboard_embed(message)
data = await self.redis.get(str(message.id))
return await ctx.respond(
f"```json\n{json.dumps(data, indent=4)}\n```\nStars: {stars:,}",
embed=embed
)
def setup(bot):
bot.add_cog(Starboard(bot))

View file

@ -44,6 +44,14 @@ CONFIG.setdefault("network", {})
CONFIG.setdefault("quote_a", {"channel": None})
CONFIG.setdefault("redis", {"host": "redis", "port": 6379, "decode_responses": True})
if CONFIG["redis"].pop("db", None) is not None:
log.warning("`redis.db` cannot be manually specified, each cog that uses redis has its own db value! Value ignored")
if CONFIG["redis"].pop("no_ping", None) is not None:
log.warning("`redis.no_ping` was deprecated after 808D621F. Ping is now always mandatory.")
CONFIG["redis"]["decode_responses"] = True
if _t := os.getenv("JIMMY_TOKEN"):
log.info("Overriding config with token from $JIMMY_TOKEN.")
CONFIG["jimmy"]["token"] = _t