mirror of
https://github.com/hencaric/SCNewsBot.git
synced 2026-08-16 22:46:03 +01:00
Sweeping updates
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# scnewsbot
|
# scnewsbot
|
||||||
config.toml
|
config.toml
|
||||||
|
rstarcitizen.py
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 mudkip
|
Copyright (c) 2026 hencaric
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ The production version of the SC News Bot used by the r/StarCitizen Discord serv
|
|||||||
- Join https://discord.gg/starcitizen for the best Star Citizen community on the internet
|
- Join https://discord.gg/starcitizen for the best Star Citizen community on the internet
|
||||||
|
|
||||||
## Version
|
## Version
|
||||||
1.0.4
|
3.0.2
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
- Ian (hencaric)
|
- Ian (hencaric)
|
||||||
https://github.com/hencaric
|
https://github.com/hencaric
|
||||||
- mudkip
|
|
||||||
https://github.com/mudkipdev
|
Existing code is inspired by the 1.0 version produced by mudkip
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
64
leaderboard.json
Normal file
64
leaderboard.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"288522211164160010": {
|
||||||
|
"count": 3,
|
||||||
|
"last_post": "2026-03-03T18:22:49.689738",
|
||||||
|
"posts": [
|
||||||
|
"2026-03-03T18:22:49.689738",
|
||||||
|
"2026-03-03T18:22:49.689738",
|
||||||
|
"2026-03-03T18:22:49.689738"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"251146366980456448": {
|
||||||
|
"count": 17,
|
||||||
|
"last_post": "2026-03-12T21:08:34.082167",
|
||||||
|
"posts": [
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167",
|
||||||
|
"2026-03-12T21:08:34.082167"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"295679494096814080": {
|
||||||
|
"count": 17,
|
||||||
|
"last_post": "2026-03-13T02:14:42.801894",
|
||||||
|
"posts": [
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894",
|
||||||
|
"2026-03-13T02:14:42.801894"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"138019022766604288": {
|
||||||
|
"count": 1,
|
||||||
|
"last_post": "2026-03-05T01:00:39.004903",
|
||||||
|
"posts": [
|
||||||
|
"2026-03-05T01:00:39.004903"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "scnewsbot"
|
name = "scnewsbot"
|
||||||
version = "1.0.3"
|
version = "3.0.1"
|
||||||
description = "A bot for the r/starcitizen Discord server."
|
description = "A bot for the r/starcitizen Discord server."
|
||||||
authors = ["mudkipdev <mudkip@mudkip.dev>"]
|
authors = ["hencaric"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [
|
packages = [
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import discord
|
|
||||||
import asyncio
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from utils import Config
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
class RStarCitizen(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
self._last_member = None
|
|
||||||
|
|
||||||
@commands.command(description='Modmail Close Message')
|
|
||||||
async def mmc(self, ctx):
|
|
||||||
await ctx.reply("```=aclose Situation resolved, please only reply if there is something else.```")
|
|
||||||
|
|
||||||
@tasks.loop(hours=1)
|
|
||||||
async def update_member_count(self):
|
|
||||||
guild = self.bot.get_guild(82210263440306176) # Replace YOUR_GUILD_ID with your actual guild ID
|
|
||||||
channel = guild.get_channel(1223778123472965755) # Replace YOUR_CHANNEL_ID with your actual channel ID
|
|
||||||
|
|
||||||
member_count = len(guild.members)
|
|
||||||
await channel.edit(name=f'Members: {member_count}')
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_ready(self):
|
|
||||||
print('MemberCount cog is ready.')
|
|
||||||
self.update_member_count.start()
|
|
||||||
|
|
||||||
async def setup(bot):
|
|
||||||
await bot.add_cog(RStarCitizen(bot))
|
|
||||||
@@ -1,637 +1,289 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Optional, Callable, Awaitable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ui import Button
|
|
||||||
import discord
|
import discord
|
||||||
from utils import can_publish_announcements
|
from discord.ext import commands
|
||||||
|
from datetime import datetime
|
||||||
|
from extensions import leaderboard
|
||||||
|
|
||||||
ANNOUNCEMENT_BUILDER_TIMEOUT = 1200
|
# CONFIG
|
||||||
ANNOUNCEMENT_EMOJI = "<:upvote:354233015842635776>"
|
DEFAULT_IMAGE_URL = "https://cdn.discordapp.com/attachments/611922107345141760/1348673800874754088/Polaris_over_Yela_bright.png"
|
||||||
DEFAULT_IMAGE_URL = "https://cdn.discordapp.com/attachments/611922107345141760/1348673800874754088/Polaris_over_Yela_bright.png?ex=67eff5db&is=67eea45b&hm=f851f8aa07e09ab2c506d6dc395ce91a2db623b0a0c6c090eb19360b0c464c7e&"
|
LOGGING_CHANNEL_IDS = [1091876261938343986]
|
||||||
INSTRUCTIONS = """\
|
|
||||||
1. Title should not use any formatting.
|
|
||||||
2. "Video" should only be used for YouTube or video links with pretty embeds.
|
|
||||||
3. URL should be used for any regular link such as a comm-link.
|
|
||||||
4. In the description box, use `-` and it will replace it with `➣`, use `+` and it will replace it with `✦` preceeded by three spaces.
|
|
||||||
5. Use the `&ids` commands to get the channel and role IDs.
|
|
||||||
6. Do not ping for every post if there are consecutive posts in the same channel, instead ping only on the final post and provide an overall preview.\n7. **ALWAYS** include a ping preview, you can find these using `&previews`.
|
|
||||||
8. Always select publish unless explicitly not needed (server only announcements).\
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
CHANNEL_OPTIONS = [
|
||||||
|
("Server News", 1113146864804573285),
|
||||||
|
("Community Feed", 1388237591845011516),
|
||||||
|
("SC News", 569635458183856149),
|
||||||
|
("General News", 803341100618219540),
|
||||||
|
("Patch Notes", 585952222853201941),
|
||||||
|
("Testing", 1062905729532571719),
|
||||||
|
]
|
||||||
|
|
||||||
async def get_follow_up_message(
|
PING_ROLE_OPTIONS = [
|
||||||
announcement: discord.Message, /, *, limit: int
|
("Server News", 1113152142300156004),
|
||||||
) -> Optional[discord.Message]:
|
("Community Feed", 1402452172595265596),
|
||||||
index = 0
|
("SC News", 620025828079697920),
|
||||||
|
("General News", 803343410794594385),
|
||||||
|
("Patch Notes", 620025894559547412),
|
||||||
|
("Evocati Patch Notes", 1305975151858552862),
|
||||||
|
("MOTD", 1310721100392435797),
|
||||||
|
("Testing", 1473004437034242260),
|
||||||
|
]
|
||||||
|
|
||||||
if limit == 0:
|
LEADERBOARD_CHANNEL_IDS = {
|
||||||
return announcement
|
1113146864804573285,
|
||||||
elif limit < 0:
|
1388237591845011516,
|
||||||
async for message in announcement.channel.history(
|
569635458183856149,
|
||||||
before=announcement.created_at
|
803341100618219540,
|
||||||
):
|
585952222853201941,
|
||||||
index -= 1
|
}
|
||||||
|
|
||||||
if message.author.id == announcement._state.user.id and index == limit:
|
|
||||||
return message
|
|
||||||
elif limit > 0:
|
|
||||||
async for message in announcement.channel.history(
|
|
||||||
after=announcement.created_at
|
|
||||||
):
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
if message.author.id == announcement._state.user.id and index == limit:
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
def is_announcement(announcement: discord.Message, /) -> bool:
|
|
||||||
return (
|
|
||||||
announcement
|
|
||||||
and announcement.author == announcement.guild.me
|
|
||||||
and len(announcement.embeds) == 1
|
|
||||||
and not announcement.content
|
|
||||||
and not announcement.components
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_video_message(announcement: discord.Message, /) -> bool:
|
|
||||||
return (
|
|
||||||
announcement
|
|
||||||
and announcement.author == announcement.guild.me
|
|
||||||
and len(announcement.embeds) == 1
|
|
||||||
and not announcement.mentions
|
|
||||||
and announcement.content
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reformat_description(description: str) -> str:
|
|
||||||
split_description = description.split("\n")
|
|
||||||
|
|
||||||
for index, line in enumerate(split_description):
|
|
||||||
if line.startswith("-"):
|
|
||||||
split_description[index] = "➣" + line[1:]
|
|
||||||
elif line.startswith("+"):
|
|
||||||
split_description[index] = "ㅤ✦" + line[1:]
|
|
||||||
|
|
||||||
return "\n".join(split_description)
|
|
||||||
|
|
||||||
|
|
||||||
class AnnouncementCog(commands.Cog, name="Announcements"):
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.check(can_publish_announcements)
|
|
||||||
@commands.command(brief="Gives you instructions for using the announcement system.")
|
|
||||||
async def instructions(self, ctx: commands.Context) -> None:
|
|
||||||
embed = discord.Embed(
|
|
||||||
color=self.bot.config.embed_color,
|
|
||||||
title="Instructions",
|
|
||||||
description=INSTRUCTIONS,
|
|
||||||
)
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
@commands.guild_only()
|
|
||||||
@commands.group(
|
|
||||||
brief="Commands relating to the r/starcitizen Discord news system.",
|
|
||||||
aliases=["announcement", "news", "embed"],
|
|
||||||
invoke_without_command=True,
|
|
||||||
)
|
|
||||||
async def announcements(self, ctx: commands.Context) -> None:
|
|
||||||
await ctx.send_help(ctx.command)
|
|
||||||
|
|
||||||
@commands.check(can_publish_announcements)
|
|
||||||
@announcements.command(
|
|
||||||
aliases=["post"], brief="Creates and sends a new announcement."
|
|
||||||
)
|
|
||||||
async def create(self, ctx: commands.Context) -> None:
|
|
||||||
announcement_builder = AnnouncementBuilder(owner=ctx.author)
|
|
||||||
embed = await announcement_builder.get_embed(bot=self.bot)
|
|
||||||
await ctx.send(
|
|
||||||
content=announcement_builder.announcement.video_url,
|
|
||||||
embed=embed,
|
|
||||||
view=announcement_builder.view,
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(can_publish_announcements)
|
|
||||||
@announcements.command(brief="Edits an existing announcement.")
|
|
||||||
async def edit(self, ctx: commands.Context, message: discord.Message) -> None:
|
|
||||||
announcement = await Announcement.from_message(message, bot=self.bot)
|
|
||||||
announcement_builder = AnnouncementBuilder(
|
|
||||||
edit_message=message, edit_announcement=announcement, owner=ctx.author
|
|
||||||
)
|
|
||||||
embed = await announcement_builder.get_embed(bot=self.bot)
|
|
||||||
await ctx.send(
|
|
||||||
content=announcement_builder.announcement.video_url,
|
|
||||||
embed=embed,
|
|
||||||
view=announcement_builder.view,
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.check(can_publish_announcements)
|
|
||||||
@announcements.command(brief="Deletes an announcement.")
|
|
||||||
async def delete(self, ctx: commands.Context, message: discord.Message) -> None:
|
|
||||||
if not is_announcement(message):
|
|
||||||
await ctx.reply("That is not an announcement!")
|
|
||||||
return
|
|
||||||
|
|
||||||
video_message = await get_follow_up_message(message, limit=-1)
|
|
||||||
ping_message = await get_follow_up_message(message, limit=1)
|
|
||||||
|
|
||||||
if is_video_message(video_message):
|
|
||||||
await video_message.delete()
|
|
||||||
if ping_message:
|
|
||||||
await ping_message.delete()
|
|
||||||
|
|
||||||
await message.delete()
|
|
||||||
await ctx.reply("Deleted that announcement. 👌")
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_command_error(
|
|
||||||
self, ctx: commands.Context, error: commands.CommandError
|
|
||||||
) -> None:
|
|
||||||
if isinstance(error, commands.MessageNotFound):
|
|
||||||
await ctx.send("Could not find that message.")
|
|
||||||
else:
|
|
||||||
raise error
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidAnnouncementException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Option:
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
directions: Optional[str] = None
|
|
||||||
row: int = 0
|
|
||||||
is_long: bool = False
|
|
||||||
|
|
||||||
|
EMBED_COLOR = discord.Color.blurple()
|
||||||
|
TIMEOUT = 900
|
||||||
|
|
||||||
|
# DATA MODEL
|
||||||
class Announcement:
|
class Announcement:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
title: str = "",
|
||||||
title: str = "Announcement",
|
description: str = "",
|
||||||
url: Optional[str] = None,
|
url: str | None = None,
|
||||||
description: Optional[str] = None,
|
image_url: str | None = None,
|
||||||
video_url: Optional[str] = None,
|
video_url: str | None = None,
|
||||||
image_url: Optional[str] = DEFAULT_IMAGE_URL,
|
channel: discord.TextChannel | None = None,
|
||||||
channel: Optional[discord.abc.Messageable] = None,
|
ping: discord.Role | None = None,
|
||||||
ping: Optional[discord.Role] = None,
|
ping_preview: str | None = None,
|
||||||
ping_preview: Optional[str] = None,
|
publish: bool = False,
|
||||||
author_id: Optional[int] = None,
|
):
|
||||||
is_anonymous: bool = False,
|
|
||||||
will_notify: bool = False,
|
|
||||||
) -> None:
|
|
||||||
self.title = title
|
self.title = title
|
||||||
self.url = url
|
|
||||||
self.description = description
|
self.description = description
|
||||||
|
self.url = url
|
||||||
|
self.image_url = image_url or DEFAULT_IMAGE_URL
|
||||||
self.video_url = video_url
|
self.video_url = video_url
|
||||||
self.image_url = image_url
|
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.ping = ping
|
self.ping = ping
|
||||||
self.ping_preview = ping_preview
|
self.ping_preview = ping_preview
|
||||||
|
self.publish = publish
|
||||||
self.author_id = author_id
|
|
||||||
self.is_anonymous = is_anonymous
|
|
||||||
self.will_notify = will_notify
|
|
||||||
|
|
||||||
async def set_option(
|
|
||||||
self, option: Option, value: Any, *, guild: discord.Guild
|
|
||||||
) -> bool:
|
|
||||||
if option is None:
|
|
||||||
setattr(self, option.id, converted_value)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if option.id == "description":
|
|
||||||
converted_value = reformat_description(value)
|
|
||||||
elif option.id == "channel":
|
|
||||||
if value.startswith("#"):
|
|
||||||
value = value[1:]
|
|
||||||
|
|
||||||
converted_value = discord.utils.find(
|
|
||||||
lambda channel: channel.name.lower() == value.lower(),
|
|
||||||
guild.text_channels,
|
|
||||||
)
|
|
||||||
if not converted_value and value.isnumeric():
|
|
||||||
converted_value = guild.get_channel(int(value))
|
|
||||||
elif option.id == "ping":
|
|
||||||
converted_value = discord.utils.find(
|
|
||||||
lambda role: role.name.lower() == value.lower(), guild.roles
|
|
||||||
)
|
|
||||||
if not converted_value and value.isnumeric():
|
|
||||||
converted_value = guild.get_role(int(value))
|
|
||||||
else:
|
|
||||||
if option.id == "video_url" and self.image_url == DEFAULT_IMAGE_URL:
|
|
||||||
self.image_url = None
|
|
||||||
|
|
||||||
converted_value = value
|
|
||||||
|
|
||||||
if converted_value is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
setattr(self, option.id, converted_value)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def get_embed(
|
|
||||||
self, *, bot: commands.Bot, show_author: bool = False
|
|
||||||
) -> discord.Embed:
|
|
||||||
embed = discord.Embed(
|
|
||||||
color=bot.config.embed_color,
|
|
||||||
title=self.title,
|
|
||||||
description=self.description,
|
|
||||||
)
|
|
||||||
embed.set_image(url=self.image_url)
|
|
||||||
|
|
||||||
if self.url and self.description:
|
|
||||||
embed.description = f"{self.url}\n\n" + embed.description
|
|
||||||
elif self.url:
|
|
||||||
embed.description = self.url
|
|
||||||
|
|
||||||
if not self.is_anonymous or show_author:
|
|
||||||
if self.author_id:
|
|
||||||
author = await bot.fetch_user(self.author_id)
|
|
||||||
embed.set_footer(text=f"This post was written by {author}")
|
|
||||||
else:
|
|
||||||
embed.set_footer(text="Unknown Author")
|
|
||||||
|
|
||||||
return embed
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_message(
|
def from_message(cls, message: discord.Message):
|
||||||
cls, message: discord.Message, /, *, bot: commands.Bot
|
|
||||||
) -> Any:
|
|
||||||
if not is_announcement(message):
|
|
||||||
raise InvalidAnnouncementException("That is not an announcement!")
|
|
||||||
return
|
|
||||||
|
|
||||||
embed = message.embeds[0]
|
embed = message.embeds[0]
|
||||||
author = None
|
|
||||||
|
|
||||||
if embed.footer.text:
|
|
||||||
parsed_footer = embed.footer.text.replace("This post was written by ", "")
|
|
||||||
author = discord.utils.find(
|
|
||||||
lambda member: str(member) == parsed_footer, message.guild.members
|
|
||||||
)
|
|
||||||
|
|
||||||
ping = None
|
|
||||||
ping_preview = None
|
|
||||||
ping_message = await get_follow_up_message(message, limit=1)
|
|
||||||
|
|
||||||
if ping_message:
|
|
||||||
role_converter = commands.RoleConverter()
|
|
||||||
split_message = ping_message.content.split(" - ")
|
|
||||||
ping = await role_converter.convert(
|
|
||||||
await bot.get_context(ping_message), split_message[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(split_message) == 2:
|
|
||||||
ping_preview = split_message[1]
|
|
||||||
|
|
||||||
url = ""
|
|
||||||
description = embed.description
|
|
||||||
video_message = await get_follow_up_message(message, limit=-1)
|
|
||||||
|
|
||||||
if not is_video_message(video_message):
|
|
||||||
video_message = None
|
|
||||||
|
|
||||||
if embed.description is not None:
|
|
||||||
content = embed.description
|
|
||||||
|
|
||||||
if "\n\n" in embed.description:
|
|
||||||
url = embed.description.split("\n\n")[0] + "\n\n"
|
|
||||||
content = embed.description.split("\n\n")[1:]
|
|
||||||
|
|
||||||
description = url + "\n".join(content)
|
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
title=embed.title,
|
title=embed.title or "",
|
||||||
url=url or None,
|
description=embed.description or "",
|
||||||
description=description,
|
url=embed.url,
|
||||||
video_url=video_message.content if video_message else None,
|
image_url=embed.image.url if embed.image else DEFAULT_IMAGE_URL,
|
||||||
image_url=embed.image.url,
|
|
||||||
channel=message.channel,
|
channel=message.channel,
|
||||||
ping=ping,
|
|
||||||
ping_preview=ping_preview,
|
|
||||||
author_id=author.id if author else None,
|
|
||||||
is_anonymous=embed.author is None,
|
|
||||||
will_notify=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def embed(self) -> discord.Embed:
|
||||||
class AnnouncementBuilder:
|
e = discord.Embed(
|
||||||
def __init__(
|
title=self.title or None,
|
||||||
self,
|
description=self.description or None,
|
||||||
*,
|
url=self.url,
|
||||||
edit_announcement: Optional[Announcement] = None,
|
color=EMBED_COLOR,
|
||||||
edit_message: Optional[discord.Message] = None,
|
|
||||||
owner: discord.User = None,
|
|
||||||
) -> None:
|
|
||||||
self.announcement = edit_announcement or Announcement(author_id=owner.id)
|
|
||||||
self.message = edit_message
|
|
||||||
self.edit = edit_announcement is not None
|
|
||||||
self.view = AnnouncementBuilderView(self)
|
|
||||||
self.owner = owner
|
|
||||||
|
|
||||||
_items = self.view.children
|
|
||||||
self.view.clear_items()
|
|
||||||
|
|
||||||
self.options: list[Option] = []
|
|
||||||
self.add_option(Option(id="title", name="Title"))
|
|
||||||
self.add_option(Option(id="url", name="URL"))
|
|
||||||
self.add_option(Option(id="description", name="Description", is_long=True))
|
|
||||||
self.add_option(Option(id="video_url", name="Video"))
|
|
||||||
self.add_option(Option(id="image_url", name="Image"))
|
|
||||||
self.add_option(Option(id="channel", name="Channel", row=1))
|
|
||||||
self.add_option(Option(id="ping", name="Ping", row=1))
|
|
||||||
self.add_option(Option(id="ping_preview", name="Ping Preview", row=1))
|
|
||||||
|
|
||||||
for item in _items:
|
|
||||||
if (
|
|
||||||
isinstance(item, discord.ui.Button)
|
|
||||||
and self.edit
|
|
||||||
and item.label == "Post"
|
|
||||||
):
|
|
||||||
item.label = "Edit"
|
|
||||||
|
|
||||||
self.view.add_item(item)
|
|
||||||
|
|
||||||
async def get_embed(self, bot: commands.Bot) -> discord.Embed:
|
|
||||||
return await self.announcement.get_embed(bot=bot)
|
|
||||||
|
|
||||||
def add_option(self, option: Option, /) -> None:
|
|
||||||
button = OptionButton(
|
|
||||||
self, custom_id=option.id, label=option.name, row=option.row
|
|
||||||
)
|
)
|
||||||
self.options.append(option)
|
if self.image_url:
|
||||||
self.view.add_item(button)
|
e.set_image(url=self.image_url)
|
||||||
|
return e
|
||||||
|
|
||||||
|
# MODAL
|
||||||
|
class TextModal(discord.ui.Modal):
|
||||||
|
def __init__(self, builder, field: str, label: str, long: bool = False):
|
||||||
|
super().__init__(title=f"Edit {label}")
|
||||||
|
self.builder = builder
|
||||||
|
self.field = field
|
||||||
|
default_value = getattr(builder.announcement, field) or ""
|
||||||
|
self.input = discord.ui.TextInput(
|
||||||
|
label=label,
|
||||||
|
default=default_value,
|
||||||
|
style=discord.TextStyle.long if long else discord.TextStyle.short,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
self.add_item(self.input)
|
||||||
|
|
||||||
class AnnouncementBuilderView(discord.ui.View):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
def __init__(self, announcement_builder: AnnouncementBuilder, /) -> None:
|
setattr(self.builder.announcement, self.field, self.input.value)
|
||||||
super().__init__(timeout=ANNOUNCEMENT_BUILDER_TIMEOUT)
|
self.builder.update_field_buttons()
|
||||||
self.announcement_builder = announcement_builder
|
|
||||||
|
|
||||||
def has_permission(self, user: discord.User) -> bool:
|
|
||||||
return user == self.announcement_builder.owner
|
|
||||||
|
|
||||||
async def update(self, interaction: discord.Interaction, /) -> None:
|
|
||||||
if (
|
|
||||||
self.announcement_builder.announcement.channel
|
|
||||||
and self.announcement_builder.announcement.channel.type
|
|
||||||
is discord.ChannelType.news
|
|
||||||
):
|
|
||||||
self.toggle_notification.disabled = False
|
|
||||||
else:
|
|
||||||
self.announcement_builder.announcement.will_notify = False
|
|
||||||
self.toggle_notification.style = discord.ButtonStyle.gray
|
|
||||||
self.toggle_notification.disabled = True
|
|
||||||
|
|
||||||
await interaction.response.edit_message(
|
await interaction.response.edit_message(
|
||||||
content=self.announcement_builder.announcement.video_url,
|
embed=self.builder.announcement.embed(),
|
||||||
embed=await self.announcement_builder.get_embed(bot=interaction.client),
|
view=self.builder.view,
|
||||||
view=self,
|
|
||||||
allowed_mentions=discord.AllowedMentions.none(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def button_callback(
|
# SELECTS
|
||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
class ChannelSelect(discord.ui.Select):
|
||||||
) -> None:
|
def __init__(self, builder):
|
||||||
if not self.has_permission(interaction.user):
|
self.builder = builder
|
||||||
await interaction.response.send_message(
|
options = [discord.SelectOption(label=name, value=str(cid)) for name, cid in CHANNEL_OPTIONS]
|
||||||
"You cannot use this menu.", ephemeral=True
|
super().__init__(placeholder="Select Channel", options=options, row=0)
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
cid = int(self.values[0])
|
||||||
|
self.builder.announcement.channel = interaction.guild.get_channel(cid)
|
||||||
|
self.placeholder = next((name for name, id in CHANNEL_OPTIONS if id == cid), "Select Channel")
|
||||||
|
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
|
||||||
|
|
||||||
|
class PingSelect(discord.ui.Select):
|
||||||
|
def __init__(self, builder):
|
||||||
|
self.builder = builder
|
||||||
|
options = [discord.SelectOption(label=name, value=str(rid)) for name, rid in PING_ROLE_OPTIONS]
|
||||||
|
super().__init__(placeholder="Select Ping Role", options=options, row=1)
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
rid = int(self.values[0])
|
||||||
|
self.builder.announcement.ping = interaction.guild.get_role(rid)
|
||||||
|
self.placeholder = next((name for name, id in PING_ROLE_OPTIONS if id == rid), "Select Ping Role")
|
||||||
|
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
|
||||||
|
|
||||||
|
# FIELD BUTTON
|
||||||
|
class FieldButton(discord.ui.Button):
|
||||||
|
def __init__(self, builder, field, label, row, style):
|
||||||
|
super().__init__(label=label, row=row, style=style)
|
||||||
|
self.builder = builder
|
||||||
|
self.field = field
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
await interaction.response.send_modal(
|
await interaction.response.send_modal(
|
||||||
ChangeOptionModal(
|
TextModal(self.builder, self.field, self.label, long=self.field=="description")
|
||||||
announcement_builder=self.announcement_builder,
|
|
||||||
option=discord.utils.get(
|
|
||||||
self.announcement_builder.options, id=button.custom_id
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@discord.ui.button(
|
# CANCEL / POST / PUBLISH BUTTONS
|
||||||
custom_id="cancel", label="Cancel", style=discord.ButtonStyle.danger, row=2
|
class PublishButton(discord.ui.Button):
|
||||||
)
|
def __init__(self, builder):
|
||||||
async def cancel(
|
label = "Published: ✅" if builder.announcement.publish else "Published: ❌"
|
||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
super().__init__(label=label, style=discord.ButtonStyle.secondary, row=3)
|
||||||
) -> None:
|
self.builder = builder
|
||||||
if not self.has_permission(interaction.user):
|
|
||||||
await interaction.response.send_message(
|
async def callback(self, interaction: discord.Interaction):
|
||||||
"You cannot use this menu.", ephemeral=True
|
self.builder.announcement.publish = not self.builder.announcement.publish
|
||||||
)
|
self.label = "Published: ✅" if self.builder.announcement.publish else "Published: ❌"
|
||||||
|
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
|
||||||
|
|
||||||
|
class CancelButton(discord.ui.Button):
|
||||||
|
def __init__(self, builder, editing=False):
|
||||||
|
super().__init__(label="Cancel", style=discord.ButtonStyle.danger, row=4)
|
||||||
|
self.builder = builder
|
||||||
|
self.editing = editing
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
await interaction.message.delete()
|
||||||
|
msg_text = "Announcement editing cancelled." if self.editing else "Announcement creation cancelled."
|
||||||
|
await interaction.response.send_message(msg_text, ephemeral=True)
|
||||||
|
self.view.stop()
|
||||||
|
|
||||||
|
class PostButton(discord.ui.Button):
|
||||||
|
def __init__(self, builder, editing=False):
|
||||||
|
label = "Edit" if editing else "Post"
|
||||||
|
super().__init__(label=label, style=discord.ButtonStyle.blurple, row=4)
|
||||||
|
self.builder = builder
|
||||||
|
self.editing = editing
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
ann = self.builder.announcement
|
||||||
|
if not ann.channel:
|
||||||
|
await interaction.response.send_message("Select a channel first.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.stop()
|
if self.editing and self.builder.target:
|
||||||
|
msg = self.builder.target
|
||||||
|
await msg.edit(embed=ann.embed())
|
||||||
|
else:
|
||||||
|
msg = await ann.channel.send(embed=ann.embed())
|
||||||
|
|
||||||
|
if ann.publish and isinstance(ann.channel, discord.TextChannel) and ann.channel.is_news():
|
||||||
|
await msg.publish()
|
||||||
|
|
||||||
|
if ann.video_url:
|
||||||
|
await ann.channel.send(ann.video_url)
|
||||||
|
|
||||||
|
ping_msg = ""
|
||||||
|
if ann.ping:
|
||||||
|
ping_msg += f"{ann.ping.mention}"
|
||||||
|
if ann.ping_preview:
|
||||||
|
ping_msg += f" - {ann.ping_preview}"
|
||||||
|
if ping_msg:
|
||||||
|
await ann.channel.send(ping_msg)
|
||||||
|
|
||||||
|
for cid in LOGGING_CHANNEL_IDS:
|
||||||
|
ch = self.builder.ctx.guild.get_channel(cid)
|
||||||
|
if ch:
|
||||||
|
await ch.send(embed=ann.embed())
|
||||||
|
|
||||||
|
if ann.channel.id in LEADERBOARD_CHANNEL_IDS:
|
||||||
|
leaderboard.record_announcement_post(interaction.user.id)
|
||||||
|
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"Cancelled editing this announcement."
|
"The announcement has been posted! \nhttps://i.postimg.cc/J48Vk8my/meme-8-1.gif"
|
||||||
if self.announcement_builder.edit
|
if not self.editing else "Announcement edited!",
|
||||||
else "Cancelled posting this announcement."
|
ephemeral=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@discord.ui.button(custom_id="anonymous", label="Anonymous?", row=2)
|
self.view.stop()
|
||||||
async def toggle_anonymous(
|
|
||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
# VIEW
|
||||||
) -> None:
|
class BuilderView(discord.ui.View):
|
||||||
if not self.has_permission(interaction.user):
|
def __init__(self, builder, editing=False):
|
||||||
await interaction.response.send_message(
|
super().__init__(timeout=TIMEOUT)
|
||||||
"You cannot use this menu.", ephemeral=True
|
self.builder = builder
|
||||||
)
|
self.editing = editing
|
||||||
|
|
||||||
|
self.add_item(ChannelSelect(builder))
|
||||||
|
self.add_item(PingSelect(builder))
|
||||||
|
self.update_field_buttons()
|
||||||
|
self.add_item(PublishButton(builder))
|
||||||
|
self.add_item(CancelButton(builder, editing=editing))
|
||||||
|
self.add_item(PostButton(builder, editing=editing))
|
||||||
|
|
||||||
|
def update_field_buttons(self):
|
||||||
|
field_buttons = [item for item in self.children if isinstance(item, FieldButton)]
|
||||||
|
for btn in field_buttons:
|
||||||
|
self.remove_item(btn)
|
||||||
|
|
||||||
|
for field, label, row, long in [
|
||||||
|
("title", "Title", 2, False),
|
||||||
|
("description", "Description", 2, True),
|
||||||
|
("url", "URL", 2, False),
|
||||||
|
("image_url", "Image", 3, False),
|
||||||
|
("video_url", "Video", 3, False),
|
||||||
|
("ping_preview", "Ping Preview", 3, False),
|
||||||
|
]:
|
||||||
|
style = discord.ButtonStyle.success if getattr(self.builder.announcement, field) else discord.ButtonStyle.gray
|
||||||
|
self.add_item(FieldButton(self.builder, field, label, row, style))
|
||||||
|
|
||||||
|
# BUILDER
|
||||||
|
class Builder:
|
||||||
|
def __init__(self, ctx, announcement, target=None, editing=False):
|
||||||
|
self.ctx = ctx
|
||||||
|
self.announcement = announcement
|
||||||
|
self.target = target
|
||||||
|
self.editing = editing
|
||||||
|
self.view = BuilderView(self, editing=editing)
|
||||||
|
|
||||||
|
def update_field_buttons(self):
|
||||||
|
self.view.update_field_buttons()
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
await self.ctx.send(embed=self.announcement.embed(), view=self.view)
|
||||||
|
|
||||||
|
# COG
|
||||||
|
class Announcements(commands.Cog):
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.group(invoke_without_command=True)
|
||||||
|
async def embed(self, ctx):
|
||||||
|
await ctx.send_help(ctx.command)
|
||||||
|
|
||||||
|
@embed.command()
|
||||||
|
async def create(self, ctx):
|
||||||
|
await Builder(ctx, Announcement(channel=ctx.channel)).start()
|
||||||
|
|
||||||
|
@embed.command()
|
||||||
|
async def edit(self, ctx, message: discord.Message):
|
||||||
|
if not message.embeds:
|
||||||
|
await ctx.reply("That message has no embed.")
|
||||||
return
|
return
|
||||||
|
await Builder(ctx, Announcement.from_message(message), target=message, editing=True).start()
|
||||||
|
|
||||||
self.announcement_builder.announcement.is_anonymous = (
|
# SETUP
|
||||||
not self.announcement_builder.announcement.is_anonymous
|
async def setup(bot: commands.Bot):
|
||||||
)
|
await bot.add_cog(Announcements(bot))
|
||||||
if self.announcement_builder.announcement.is_anonymous:
|
|
||||||
button.style = discord.ButtonStyle.green
|
|
||||||
else:
|
|
||||||
button.style = discord.ButtonStyle.gray
|
|
||||||
|
|
||||||
await self.update(interaction)
|
|
||||||
|
|
||||||
@discord.ui.button(
|
|
||||||
custom_id="notification", label="Published?", row=2, disabled=True
|
|
||||||
)
|
|
||||||
async def toggle_notification(
|
|
||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
||||||
) -> None:
|
|
||||||
if not self.has_permission(interaction.user):
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"You cannot use this menu.", ephemeral=True
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.announcement_builder.announcement.will_notify = (
|
|
||||||
not self.announcement_builder.announcement.will_notify
|
|
||||||
)
|
|
||||||
if self.announcement_builder.announcement.will_notify:
|
|
||||||
button.style = discord.ButtonStyle.green
|
|
||||||
else:
|
|
||||||
button.style = discord.ButtonStyle.gray
|
|
||||||
|
|
||||||
await self.update(interaction)
|
|
||||||
|
|
||||||
@discord.ui.button(
|
|
||||||
custom_id="publish", label="Post", style=discord.ButtonStyle.blurple, row=2
|
|
||||||
)
|
|
||||||
async def publish(
|
|
||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
||||||
) -> None:
|
|
||||||
if not self.has_permission(interaction.user):
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"You cannot use this menu.", ephemeral=True
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
announcement = self.announcement_builder.announcement
|
|
||||||
role = announcement.ping
|
|
||||||
allowed_mentions = (
|
|
||||||
discord.AllowedMentions(everyone=False, users=False, roles=[role])
|
|
||||||
if role
|
|
||||||
else discord.AllowedMentions.none()
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.announcement_builder.edit:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
f"Your announcement was edited! 🎉"
|
|
||||||
+ f" ({self.announcement_builder.message.jump_url})"
|
|
||||||
)
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
embed = await self.announcement_builder.get_embed(bot=interaction.client)
|
|
||||||
embed.remove_footer()
|
|
||||||
|
|
||||||
await self.announcement_builder.message.edit(embed=embed)
|
|
||||||
video_message = await get_follow_up_message(
|
|
||||||
self.announcement_builder.message, limit=-1
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_video_message(video_message):
|
|
||||||
await video_message.edit(
|
|
||||||
content=self.announcement_builder.announcement.video_url
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
if not announcement.channel:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"You must have a channel selected!", ephemeral=True
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if announcement.ping_preview and not announcement.ping:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"You cannot have a ping preview selected without a ping!",
|
|
||||||
ephemeral=True,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
bot = interaction.client
|
|
||||||
video_message = None
|
|
||||||
if announcement.video_url:
|
|
||||||
video_message = await announcement.channel.send(announcement.video_url)
|
|
||||||
|
|
||||||
message = await announcement.channel.send(
|
|
||||||
embed=await announcement.get_embed(bot=bot),
|
|
||||||
)
|
|
||||||
await message.add_reaction(ANNOUNCEMENT_EMOJI)
|
|
||||||
|
|
||||||
await interaction.response.send_message(
|
|
||||||
f"Your announcement was posted! 🎉 ({message.jump_url})"
|
|
||||||
+ "\nhttps://i.postimg.cc/J48Vk8my/meme-8-1.gif"
|
|
||||||
+ "\n<@288522211164160010> tweet this shit brotha"
|
|
||||||
)
|
|
||||||
|
|
||||||
for channel_id in bot.config.repost_channels:
|
|
||||||
repost_channel = await bot.fetch_channel(channel_id)
|
|
||||||
|
|
||||||
if announcement.video_url:
|
|
||||||
await repost_channel.send(announcement.video_url)
|
|
||||||
|
|
||||||
await repost_channel.send(
|
|
||||||
embed=await announcement.get_embed(bot=bot, show_author=True)
|
|
||||||
)
|
|
||||||
await repost_channel.send(message.jump_url)
|
|
||||||
|
|
||||||
if announcement.ping and announcement.ping_preview:
|
|
||||||
await announcement.channel.send(
|
|
||||||
f"{announcement.ping.mention} - {announcement.ping_preview}",
|
|
||||||
allowed_mentions=allowed_mentions,
|
|
||||||
)
|
|
||||||
elif announcement.ping:
|
|
||||||
await announcement.channel.send(
|
|
||||||
announcement.ping.mention, allowed_mentions=allowed_mentions
|
|
||||||
)
|
|
||||||
|
|
||||||
if announcement.will_notify:
|
|
||||||
try:
|
|
||||||
if video_message:
|
|
||||||
await video_message.publish()
|
|
||||||
|
|
||||||
await message.publish()
|
|
||||||
except discord.Forbidden:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeOptionModal(discord.ui.Modal):
|
|
||||||
def __init__(
|
|
||||||
self, announcement_builder: AnnouncementBuilder, option: Option
|
|
||||||
) -> None:
|
|
||||||
super().__init__(title=option.name)
|
|
||||||
self.announcement_builder = announcement_builder
|
|
||||||
self.option = option
|
|
||||||
|
|
||||||
self.option_input = discord.ui.TextInput(
|
|
||||||
custom_id=self.option.id,
|
|
||||||
label=self.option.name,
|
|
||||||
required=self.option.id in ("title", "channel"),
|
|
||||||
placeholder=self.option.directions,
|
|
||||||
style=(
|
|
||||||
discord.TextStyle.long
|
|
||||||
if self.option.is_long
|
|
||||||
else discord.TextStyle.short
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.add_item(self.option_input)
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction, /) -> None:
|
|
||||||
option_value = self.option_input.value
|
|
||||||
conversion_success = await self.announcement_builder.announcement.set_option(
|
|
||||||
self.option, option_value, guild=interaction.guild
|
|
||||||
)
|
|
||||||
item = discord.utils.get(
|
|
||||||
self.announcement_builder.view.children, custom_id=self.option.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if conversion_success and not option_value:
|
|
||||||
item.style = discord.ButtonStyle.gray
|
|
||||||
await self.announcement_builder.view.update(interaction)
|
|
||||||
elif conversion_success:
|
|
||||||
item.style = discord.ButtonStyle.green
|
|
||||||
await self.announcement_builder.view.update(interaction)
|
|
||||||
else:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
"Could not find that role or channel.", ephemeral=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionButton(discord.ui.Button):
|
|
||||||
def __init__(
|
|
||||||
self, announcement_builder: AnnouncementBuilder, *args, **kwargs
|
|
||||||
) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.announcement_builder = announcement_builder
|
|
||||||
|
|
||||||
async def callback(self, interaction: discord.Interaction, /) -> None:
|
|
||||||
await self.announcement_builder.view.button_callback(interaction, self)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
|
||||||
await bot.add_cog(AnnouncementCog(bot))
|
|
||||||
|
|||||||
248
scnewsbot/extensions/leaderboard.py
Normal file
248
scnewsbot/extensions/leaderboard.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
# CONFIG
|
||||||
|
LEADERBOARD_FILE = "leaderboard.json"
|
||||||
|
EMBED_COLOR = discord.Color.gold()
|
||||||
|
TOP_EMOJIS = ["🥇", "🥈", "🥉"]
|
||||||
|
USERS_PER_PAGE = 5
|
||||||
|
|
||||||
|
# DATA
|
||||||
|
class LeaderboardData:
|
||||||
|
|
||||||
|
def __init__(self, filepath=LEADERBOARD_FILE):
|
||||||
|
self.filepath = filepath
|
||||||
|
self.data = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
if os.path.exists(self.filepath):
|
||||||
|
try:
|
||||||
|
with open(self.filepath, "r", encoding="utf-8") as f:
|
||||||
|
self.data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
self.data = {}
|
||||||
|
else:
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
with open(self.filepath, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.data, f, indent=4)
|
||||||
|
|
||||||
|
# RECORD POST
|
||||||
|
def record_post(self, user_id: int):
|
||||||
|
|
||||||
|
uid = str(user_id)
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
if uid not in self.data:
|
||||||
|
self.data[uid] = {
|
||||||
|
"count": 0,
|
||||||
|
"last_post": now,
|
||||||
|
"posts": []
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data[uid]["count"] += 1
|
||||||
|
self.data[uid]["last_post"] = now
|
||||||
|
self.data[uid]["posts"].append(now)
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
# SORT
|
||||||
|
def get_sorted(self):
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
self.data.items(),
|
||||||
|
key=lambda item: (-item[1]["count"], item[1]["last_post"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# USER STATS
|
||||||
|
def user_30_days(self, posts):
|
||||||
|
|
||||||
|
cutoff = datetime.utcnow() - timedelta(days=30)
|
||||||
|
|
||||||
|
return sum(
|
||||||
|
1 for p in posts
|
||||||
|
if datetime.fromisoformat(p) >= cutoff
|
||||||
|
)
|
||||||
|
|
||||||
|
def user_year(self, posts):
|
||||||
|
|
||||||
|
year = datetime.utcnow().year
|
||||||
|
|
||||||
|
return sum(
|
||||||
|
1 for p in posts
|
||||||
|
if datetime.fromisoformat(p).year == year
|
||||||
|
)
|
||||||
|
|
||||||
|
# GLOBAL STATS
|
||||||
|
def global_stats(self):
|
||||||
|
|
||||||
|
cutoff = datetime.utcnow() - timedelta(days=30)
|
||||||
|
year = datetime.utcnow().year
|
||||||
|
|
||||||
|
total30 = 0
|
||||||
|
totalYear = 0
|
||||||
|
totalAll = 0
|
||||||
|
|
||||||
|
for user in self.data.values():
|
||||||
|
|
||||||
|
posts = user.get("posts", [])
|
||||||
|
|
||||||
|
totalAll += user.get("count", 0)
|
||||||
|
|
||||||
|
for p in posts:
|
||||||
|
|
||||||
|
dt = datetime.fromisoformat(p)
|
||||||
|
|
||||||
|
if dt >= cutoff:
|
||||||
|
total30 += 1
|
||||||
|
|
||||||
|
if dt.year == year:
|
||||||
|
totalYear += 1
|
||||||
|
|
||||||
|
return total30, totalYear, totalAll
|
||||||
|
|
||||||
|
|
||||||
|
leaderboard_data = LeaderboardData()
|
||||||
|
|
||||||
|
# VIEW
|
||||||
|
class LeaderboardView(discord.ui.View):
|
||||||
|
|
||||||
|
def __init__(self, ctx, users):
|
||||||
|
|
||||||
|
super().__init__(timeout=180)
|
||||||
|
|
||||||
|
self.ctx = ctx
|
||||||
|
self.users = users
|
||||||
|
self.page = 0
|
||||||
|
self.max_page = (len(users) - 1) // USERS_PER_PAGE
|
||||||
|
|
||||||
|
def build_embed(self):
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📊 Announcement Leaderboard",
|
||||||
|
color=EMBED_COLOR,
|
||||||
|
timestamp=datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
start = self.page * USERS_PER_PAGE
|
||||||
|
end = start + USERS_PER_PAGE
|
||||||
|
|
||||||
|
page_users = self.users[start:end]
|
||||||
|
|
||||||
|
for i, (uid, stats) in enumerate(page_users, start=start+1):
|
||||||
|
|
||||||
|
member = self.ctx.guild.get_member(int(uid))
|
||||||
|
name = member.display_name if member else f"User {uid}"
|
||||||
|
|
||||||
|
posts = stats.get("posts", [])
|
||||||
|
|
||||||
|
alltime = stats["count"]
|
||||||
|
last30 = leaderboard_data.user_30_days(posts)
|
||||||
|
year = leaderboard_data.user_year(posts)
|
||||||
|
|
||||||
|
last_post = datetime.fromisoformat(stats["last_post"]).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
|
||||||
|
if i <= 3:
|
||||||
|
title = f"{TOP_EMOJIS[i-1]} {name}"
|
||||||
|
else:
|
||||||
|
title = name
|
||||||
|
|
||||||
|
value = (
|
||||||
|
f"**All Time:** {alltime}\n"
|
||||||
|
f"**30 Days:** {last30}\n"
|
||||||
|
f"**Year:** {year}\n"
|
||||||
|
f"**Last:** {last_post}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if int(uid) == self.ctx.author.id:
|
||||||
|
value += " 👈 You"
|
||||||
|
|
||||||
|
embed.add_field(name=title, value=value, inline=False)
|
||||||
|
|
||||||
|
# GLOBAL STATS
|
||||||
|
total30, totalYear, totalAll = leaderboard_data.global_stats()
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="📈 Server Announcement Stats",
|
||||||
|
value=(
|
||||||
|
f"**Last 30 Days:** {total30}\n"
|
||||||
|
f"**This Year:** {totalYear}\n"
|
||||||
|
f"**All Time:** {totalAll}"
|
||||||
|
),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_footer(text=f"Page {self.page+1}/{self.max_page+1}")
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
# BUTTONS
|
||||||
|
@discord.ui.button(label="⬅ Previous", style=discord.ButtonStyle.secondary)
|
||||||
|
async def previous(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
|
||||||
|
if self.page > 0:
|
||||||
|
self.page -= 1
|
||||||
|
|
||||||
|
await interaction.response.edit_message(embed=self.build_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="🧑 My Rank", style=discord.ButtonStyle.primary)
|
||||||
|
async def my_rank(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
|
||||||
|
uid = str(interaction.user.id)
|
||||||
|
|
||||||
|
for index, (user_id, _) in enumerate(self.users):
|
||||||
|
|
||||||
|
if user_id == uid:
|
||||||
|
|
||||||
|
self.page = index // USERS_PER_PAGE
|
||||||
|
break
|
||||||
|
|
||||||
|
await interaction.response.edit_message(embed=self.build_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="Next ➡", style=discord.ButtonStyle.secondary)
|
||||||
|
async def next(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
|
||||||
|
if self.page < self.max_page:
|
||||||
|
self.page += 1
|
||||||
|
|
||||||
|
await interaction.response.edit_message(embed=self.build_embed(), view=self)
|
||||||
|
|
||||||
|
|
||||||
|
# COG
|
||||||
|
class Leaderboard(commands.Cog):
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def leaderboard(self, ctx):
|
||||||
|
|
||||||
|
users = leaderboard_data.get_sorted()
|
||||||
|
|
||||||
|
if not users:
|
||||||
|
await ctx.send("No announcements have been posted yet.")
|
||||||
|
return
|
||||||
|
|
||||||
|
view = LeaderboardView(ctx, users)
|
||||||
|
|
||||||
|
await ctx.send(embed=view.build_embed(), view=view)
|
||||||
|
|
||||||
|
|
||||||
|
# HELPER
|
||||||
|
def record_announcement_post(user_id: int):
|
||||||
|
|
||||||
|
leaderboard_data.record_post(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
# SETUP
|
||||||
|
async def setup(bot):
|
||||||
|
|
||||||
|
await bot.add_cog(Leaderboard(bot))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,20 +3,18 @@ import discord
|
|||||||
from extensions.announcements import Announcement
|
from extensions.announcements import Announcement
|
||||||
from utils import can_publish_announcements
|
from utils import can_publish_announcements
|
||||||
|
|
||||||
|
# TEMPLATES
|
||||||
TEMPLATES: dict[str, Announcement] = {
|
TEMPLATES: dict[str, Announcement] = {
|
||||||
"isc": Announcement(title="Inside Star Citizen | [topic] - [subtopic]"),
|
"isc": Announcement(title="Inside Star Citizen | [topic] - [subtopic]"),
|
||||||
"scl": Announcement(title="Star Citizen Live | [topic] - [subtopic]"),
|
"scl": Announcement(title="Star Citizen Live | [topic] - [subtopic]"),
|
||||||
"tracker": Announcement(title="Progress Tracker Update | [date]"),
|
"tracker": Announcement(title="Progress Tracker Update | [date]"),
|
||||||
"roundup": Announcement(title="Roadmap Roundup | [date]"),
|
"roundup": Announcement(title="Roadmap Roundup | [date]"),
|
||||||
"patchnotes": Announcement(
|
"patchnotes": Announcement(title="Star Citizen Alpha X.XX.X XPTU.XXXXXXX Patch Notes"),
|
||||||
title="Star Citizen Alpha X.XX.X XPTU.XXXXXXX Patch Notes"
|
|
||||||
),
|
|
||||||
"galactapedia": Announcement(title="Weekly Sneak Peek | [date]"),
|
"galactapedia": Announcement(title="Weekly Sneak Peek | [date]"),
|
||||||
"devreply": Announcement(title="Dev Reply | Topic"),
|
"devreply": Announcement(title="Dev Reply | Topic"),
|
||||||
"twisc": Announcement(
|
"twisc": Announcement(title="This Week in Star Citizen | Week of [date]"),
|
||||||
title="This Week in Star Citizen | Week of [date]",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PING_PREVIEWS = """\
|
PING_PREVIEWS = """\
|
||||||
**Patch Notes**
|
**Patch Notes**
|
||||||
- New Wave: `3.XX Wave X Release`
|
- New Wave: `3.XX Wave X Release`
|
||||||
@@ -40,6 +38,7 @@ PING_PREVIEWS = """\
|
|||||||
- Subscriber Items: `Month Subscriber Promotions`
|
- Subscriber Items: `Month Subscriber Promotions`
|
||||||
- JP: `Jump Point`
|
- JP: `Jump Point`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IDS = """\
|
IDS = """\
|
||||||
__**Server News:**__
|
__**Server News:**__
|
||||||
Channel - `1113146864804573285`
|
Channel - `1113146864804573285`
|
||||||
@@ -67,6 +66,7 @@ __**Posting Locations:**__
|
|||||||
[Check here](https://discord.com/channels/82210263440306176/611922107345141760/1113905662217441330) for a guide on what post types go where.
|
[Check here](https://discord.com/channels/82210263440306176/611922107345141760/1113905662217441330) for a guide on what post types go where.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# COG
|
||||||
class TemplatesCog(commands.Cog, name="Templates"):
|
class TemplatesCog(commands.Cog, name="Templates"):
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -97,8 +97,9 @@ class TemplatesCog(commands.Cog, name="Templates"):
|
|||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
f"Could not find that template. Use `{ctx.prefix}templates list` to list all available templates."
|
f"Could not find that template. Use `{ctx.prefix}templates list` to list all available templates."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
embed = await template.get_embed(bot=self.bot)
|
embed = template.embed()
|
||||||
embed.remove_author()
|
embed.remove_author()
|
||||||
await ctx.reply(embed=embed)
|
await ctx.reply(embed=embed)
|
||||||
|
|
||||||
@@ -114,9 +115,7 @@ class TemplatesCog(commands.Cog, name="Templates"):
|
|||||||
@commands.command(name="previews", brief="Shows all the possible ping previews.")
|
@commands.command(name="previews", brief="Shows all the possible ping previews.")
|
||||||
async def ping_previews(self, ctx: commands.Context) -> None:
|
async def ping_previews(self, ctx: commands.Context) -> None:
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
embed=discord.Embed(
|
embed=discord.Embed(color=self.bot.config.embed_color, description=PING_PREVIEWS),
|
||||||
color=self.bot.config.embed_color, description=PING_PREVIEWS
|
|
||||||
),
|
|
||||||
allowed_mentions=discord.AllowedMentions.none(),
|
allowed_mentions=discord.AllowedMentions.none(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,6 +126,6 @@ class TemplatesCog(commands.Cog, name="Templates"):
|
|||||||
embed=discord.Embed(color=self.bot.config.embed_color, description=IDS)
|
embed=discord.Embed(color=self.bot.config.embed_color, description=IDS)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# SETUP
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: commands.Bot) -> None:
|
||||||
await bot.add_cog(TemplatesCog(bot))
|
await bot.add_cog(TemplatesCog(bot))
|
||||||
Reference in New Issue
Block a user