Add starboard to bot
All checks were successful
Build and Publish Jimmy.2 / build_and_publish (push) Successful in 33s
All checks were successful
Build and Publish Jimmy.2 / build_and_publish (push) Successful in 33s
This commit is contained in:
parent
808d621fb6
commit
da7d122f14
4 changed files with 227 additions and 5 deletions
|
@ -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.
|
||||
|
|
|
@ -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
216
src/cogs/starboard.py
Normal 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))
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue