feat(shard): 🎉 Initialize the rewrite.
This commit is contained in:
+55
-365
@@ -1,54 +1,44 @@
|
||||
from typing import Literal
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import sys
|
||||
|
||||
import discord
|
||||
from discord import abc
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import errors
|
||||
|
||||
from tortoise import expressions, transactions, exceptions as tortoise_exc
|
||||
import redis.asyncio as redis
|
||||
|
||||
from redis import Redis
|
||||
|
||||
import asyncpg.exceptions
|
||||
|
||||
from payphone.db import models
|
||||
from payphone.core import (
|
||||
constants,
|
||||
exceptions,
|
||||
formatting,
|
||||
help_command,
|
||||
config,
|
||||
logging,
|
||||
)
|
||||
from payphone.db import setup, models
|
||||
from payphone.core import constants, logging
|
||||
|
||||
|
||||
class Payphone(commands.AutoShardedBot):
|
||||
log = logging.logger
|
||||
|
||||
redis: Redis = Redis(
|
||||
url="redis://localhost" if config.IS_DEBUG else "redis://redis",
|
||||
redis = redis.Redis(
|
||||
host=constants.REDIS_HOST, port=constants.REDIS_PORT, decode_responses=True
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self):
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
intents.guilds = True
|
||||
intents.guild_messages = True
|
||||
intents.guild_reactions = True
|
||||
intents.message_content = True
|
||||
|
||||
if config.IS_DEBUG:
|
||||
intents.members = True
|
||||
self.shard_ids = [constants.SHARD_ID]
|
||||
self.shard_count = constants.SHARD_COUNT
|
||||
|
||||
super().__init__(
|
||||
intents=intents,
|
||||
command_prefix=self.get_prefix,
|
||||
help_command=help_command.HelpCommand(),
|
||||
command_prefix="p.",
|
||||
strip_after_prefix=True,
|
||||
case_insensitive=True,
|
||||
)
|
||||
|
||||
for cog in ["calls", "admin"]:
|
||||
self.load_extension(f"payphone.cogs.{cog}.cog")
|
||||
self.log.info("Loaded cog %s.", cog)
|
||||
|
||||
from payphone.core import calls
|
||||
|
||||
self.call_handler = calls.CallHandler(self)
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
"""Runs when the bot has finished initializing."""
|
||||
|
||||
@@ -102,7 +92,7 @@ class Payphone(commands.AutoShardedBot):
|
||||
|
||||
async def get_or_fetch_channel(
|
||||
self, channel_id: int
|
||||
) -> abc.GuildChannel | abc.PrivateChannel | discord.Thread | None:
|
||||
) -> discord.abc.GuildChannel | discord.abc.PrivateChannel | discord.Thread | None:
|
||||
"""Get's a channel from the bot's cache, or fetches it from Discord if it's not available.
|
||||
|
||||
Args:
|
||||
@@ -119,365 +109,65 @@ class Payphone(commands.AutoShardedBot):
|
||||
|
||||
return channel
|
||||
|
||||
async def is_banned(
|
||||
self, user: discord.User | discord.Member | models.User | int
|
||||
) -> bool:
|
||||
"""Checks whether a user is banned.
|
||||
|
||||
Checks whether a user is banned, and caches the result in Redis. The cache expires in 5
|
||||
minutes.
|
||||
def clean_message(self, message: discord.Message, call) -> str:
|
||||
"""Cleans a message.
|
||||
|
||||
Args:
|
||||
user (discord.User | discord.Member | models.User | int): The user to check or their
|
||||
ID.
|
||||
message (discord.Message): The message to clean.
|
||||
call (Call): The call the message is in.
|
||||
|
||||
Returns:
|
||||
bool: Whether the user is banned or not.
|
||||
(str): The cleaned message.
|
||||
"""
|
||||
|
||||
user_id = user if isinstance(user, int) else user.id
|
||||
key = constants.BAN_TEMPLATE % user_id
|
||||
content = message.content
|
||||
|
||||
is_banned = await self.redis.get(key)
|
||||
content = content.replace(
|
||||
"@everyone", f"@{constants.INVISIBLE_CHARACTER}everyone"
|
||||
).replace("@here", f"@{constants.INVISIBLE_CHARACTER}here")
|
||||
|
||||
if is_banned is not None:
|
||||
return bool(int(is_banned))
|
||||
for mention in message.mentions + message.role_mentions:
|
||||
content = content.replace(
|
||||
f"<@{mention.id}>", f"@**{mention.display_name}**"
|
||||
)
|
||||
|
||||
is_banned = await models.Ban.filter(
|
||||
expressions.Q(user_id=user_id),
|
||||
expressions.Q(
|
||||
expires_at=None,
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
join_type="OR",
|
||||
),
|
||||
).exists()
|
||||
for mention in message.channel_mentions:
|
||||
content = content.replace(
|
||||
f"<@{mention.id}>", f"#**{mention.display_name}**"
|
||||
)
|
||||
|
||||
await self.redis.set(key, int(is_banned), ex=300)
|
||||
return content
|
||||
|
||||
return is_banned
|
||||
|
||||
@transactions.atomic()
|
||||
async def ban(
|
||||
self,
|
||||
user: discord.User | discord.Member | models.User | int,
|
||||
duration: timedelta | None,
|
||||
reason: str,
|
||||
moderator: discord.User | discord.Member | models.User | int | None = None,
|
||||
report_id: int | None = None,
|
||||
) -> models.Ban:
|
||||
"""Bans a user from Payphone.
|
||||
async def get_user_flag(self, user_id: int) -> str:
|
||||
"""Gets the flags of a user.
|
||||
|
||||
Args:
|
||||
user (discord.User | discord.Member | models.User | int): The user to ban or their ID.
|
||||
duration (timedelta | None): The expiry date, or None if it does not expire.
|
||||
reason (str): The reason of the ban.
|
||||
moderator (discord.User | discord.Member | models.User | int | None, optional): The
|
||||
moderator who performed the action.
|
||||
report_id (int | None, optional): The ID of the report the ban is related to.
|
||||
user_id (int): The ID of the user.
|
||||
|
||||
Returns:
|
||||
models.Ban: The newly created ban.
|
||||
(str): The flags of the user.
|
||||
"""
|
||||
|
||||
user_id = user if isinstance(user, int) else user.id
|
||||
key = constants.BAN_TEMPLATE % user_id
|
||||
flag = await self.redis.hget(f"user:{user_id}", "flags")
|
||||
|
||||
mod_id = (
|
||||
moderator.id
|
||||
if isinstance(moderator, (discord.User, discord.Member, models.User))
|
||||
else moderator
|
||||
)
|
||||
if flag is None:
|
||||
user, _ = await models.User.get_or_create(id=user_id)
|
||||
|
||||
until = datetime.now(timezone.utc) + duration if duration is not None else None
|
||||
flag = user.flags
|
||||
|
||||
if duration is not None:
|
||||
last_ban = (
|
||||
await models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at=None,
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
join_type="OR",
|
||||
),
|
||||
user_id=user_id,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
|
||||
if last_ban is not None:
|
||||
until = (
|
||||
duration + last_ban.expires_at
|
||||
if last_ban.expires_at is not None
|
||||
else None
|
||||
)
|
||||
|
||||
row = await models.Ban.create(
|
||||
user_id=user_id,
|
||||
reason=reason,
|
||||
expires_at=until,
|
||||
banned_by_id=mod_id,
|
||||
report_id=report_id,
|
||||
)
|
||||
|
||||
await self.redis.set(key, "1", ex=300)
|
||||
|
||||
return row
|
||||
|
||||
@transactions.atomic()
|
||||
async def unban(
|
||||
self,
|
||||
user: discord.User | discord.Member | models.User | int,
|
||||
unbanned_by: discord.User | discord.Member | models.User | int | None = None,
|
||||
) -> None:
|
||||
"""Unbans a user from Payphone.
|
||||
|
||||
Args:
|
||||
user (discord.User | discord.Member | models.User | int): The user to unban or their
|
||||
ID.
|
||||
unbanned_by (discord.User | discord.Member | models.User | int | None, optional): The
|
||||
moderator who unbanned the user or their ID.
|
||||
"""
|
||||
|
||||
user_id = user if isinstance(user, int) else user.id
|
||||
unbanned_by_id = unbanned_by if isinstance(unbanned_by, int) else unbanned_by.id
|
||||
|
||||
key = constants.BAN_TEMPLATE % user_id
|
||||
|
||||
await models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at=None,
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
join_type="OR",
|
||||
),
|
||||
user_id=user_id,
|
||||
).update(expires_at=datetime.now(timezone.utc), unbanned_by_id=unbanned_by_id)
|
||||
|
||||
await self.redis.set(key, "0", ex=300)
|
||||
|
||||
async def get_prefix(
|
||||
self, message: discord.Message | None = None, server_id: int | None = None
|
||||
) -> str | list[str]:
|
||||
"""Returns the prefix for the message.
|
||||
|
||||
The prefix is case-insensitive, meaning that it'll adapt to the message to be valid not
|
||||
depending of the case. In case that the prefix is invalid, the default or set prefix in its
|
||||
original case will be returned.
|
||||
|
||||
This method can also be used on a server's raw ID. Either `message` or `server_id` must be
|
||||
set, but not both.
|
||||
|
||||
Args:
|
||||
message (discord.Message | None): The message to check the prefix for, or None.
|
||||
server_id (int | None): The server's ID to get the prefix from.
|
||||
|
||||
Returns:
|
||||
str: The prefix for the message.
|
||||
|
||||
Raises:
|
||||
ValueError: Either `message` or `server_id` must be set, but not both.
|
||||
"""
|
||||
|
||||
default_prefixes = ["p.", "P.", self.user.mention]
|
||||
|
||||
if message is not None and server_id is not None:
|
||||
raise ValueError(
|
||||
"Either `message` or `server_id` must be set, but not both."
|
||||
)
|
||||
|
||||
if message is None and server_id is None:
|
||||
raise ValueError("Either `message` or `server_id` must be set.")
|
||||
|
||||
if message is not None and message.guild is None:
|
||||
return default_prefixes
|
||||
|
||||
if message is not None and message.content is None:
|
||||
return default_prefixes
|
||||
|
||||
if (
|
||||
message is not None
|
||||
and message.content is not None
|
||||
and message.content.startswith(self.user.mention)
|
||||
):
|
||||
return default_prefixes
|
||||
|
||||
server_id = server_id if server_id is not None else message.guild.id
|
||||
|
||||
key = constants.PREFIX_TEMPLATE % server_id
|
||||
|
||||
prefix: str | None = await self.redis.get(key)
|
||||
|
||||
if prefix is None:
|
||||
server, _ = await models.Server.update_or_create(
|
||||
(
|
||||
{"name": message.guild.name}
|
||||
if message is not None and message.guild is not None
|
||||
else None
|
||||
),
|
||||
id=server_id,
|
||||
)
|
||||
|
||||
await self.redis.set(key, server.prefix, ex=60 * 60)
|
||||
|
||||
prefix = server.prefix
|
||||
|
||||
if message is not None and message.content.lower().startswith(prefix.lower()):
|
||||
return message.content[: len(prefix)]
|
||||
|
||||
return prefix
|
||||
|
||||
async def set_prefix(
|
||||
self, server: discord.Guild | models.Server | int, prefix: str = "p."
|
||||
) -> None:
|
||||
"""Update a server's prefix.
|
||||
|
||||
Args:
|
||||
server (discord.Guild | models.Server | int): The server to update or it's ID.
|
||||
prefix (str, optional): The new prefix. It cannot be longer than 5 characters. Defaults
|
||||
to "p.".
|
||||
|
||||
Raises:
|
||||
ValueError: The prefix provided is invalid.
|
||||
"""
|
||||
|
||||
if len(prefix) > 5 or len(prefix) == 0:
|
||||
raise ValueError(
|
||||
"The prefix must not be longer than 5 characters and must not be an empty string."
|
||||
)
|
||||
|
||||
server_id = server if isinstance(server, int) else server.id
|
||||
|
||||
await models.Server.filter(id=server_id).update(prefix=prefix)
|
||||
|
||||
await self.redis.set(constants.PREFIX_TEMPLATE % server_id, prefix, ex=60 * 60)
|
||||
|
||||
async def on_command_error(
|
||||
self, context: commands.Context, exception: commands.CommandError
|
||||
) -> None:
|
||||
"""Handles each of the errors raised by commands.
|
||||
|
||||
Args:
|
||||
context (commands.Context): The context of the command.
|
||||
exception (commands.CommandError): The exception raised.
|
||||
"""
|
||||
|
||||
if isinstance(exception, exceptions.ChannelDisabledError):
|
||||
await context.send(
|
||||
"❌ You cannot run commands in this channel.", delete_after=20.0
|
||||
)
|
||||
|
||||
elif isinstance(exception, exceptions.BannedError):
|
||||
await context.send("❌ You have been banned from Payphone.")
|
||||
|
||||
elif isinstance(exception, exceptions.NotOnCallError):
|
||||
await context.send("❌ You need to be on a call to run this command.")
|
||||
|
||||
elif isinstance(exception, exceptions.NotAModeratorError):
|
||||
await context.send("❌ You're not a bot moderator.")
|
||||
|
||||
elif isinstance(exception, errors.MissingRequiredArgument):
|
||||
await context.send(
|
||||
f"❌ `{exception.param.name}` is missing. Use `{context.clean_prefix}help {context.command.name}` to learn how to use it."
|
||||
)
|
||||
|
||||
elif isinstance(exception, errors.BotMissingPermissions):
|
||||
await context.send(
|
||||
f"❌ Payphone's missing {formatting.text_join([f"`{perm.replace("_", " ").title()}`" for perm in exception.missing_permissions])} permissions to run this command."
|
||||
)
|
||||
|
||||
elif isinstance(exception, errors.NoPrivateMessage):
|
||||
await context.send("❌ This command can only be run in a server.")
|
||||
|
||||
elif isinstance(exception, errors.MissingPermissions):
|
||||
await context.send("❌ You don't have permission to run this command.")
|
||||
|
||||
elif isinstance(exception, errors.CommandOnCooldown):
|
||||
await context.send(
|
||||
f"❌ You're going too fast! Wait `{int(exception.retry_after)}` seconds before trying again."
|
||||
)
|
||||
|
||||
elif isinstance(exception, errors.NotOwner):
|
||||
await context.send("❌ You're not the owner of this bot.")
|
||||
|
||||
elif isinstance(exception, errors.CommandNotFound):
|
||||
return
|
||||
|
||||
elif isinstance(exception, tortoise_exc.TransactionManagementError):
|
||||
# Tortoise ORM bug. This may need to be logged for debugging, but for now they just
|
||||
# clutter the logs.
|
||||
return
|
||||
await self.redis.hset(f"user:{user_id}", "flags", int(flag))
|
||||
|
||||
else:
|
||||
return await super().on_command_error(context, exception)
|
||||
flag = models.UserFlag(int(flag))
|
||||
|
||||
async def on_error(
|
||||
self, event_method: str, *args: discord.Any, **kwargs: discord.Any
|
||||
) -> None:
|
||||
_, exception, _ = sys.exc_info()
|
||||
|
||||
if isinstance(
|
||||
exception,
|
||||
(asyncpg.exceptions.InterfaceError, asyncpg.exceptions.PostgresError),
|
||||
):
|
||||
# IMPORTANT! These are just spam at the moment, but may cause trouble in the future as
|
||||
# the bot grows.
|
||||
return
|
||||
|
||||
return await super().on_error(event_method, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def embed_author(self) -> discord.EmbedAuthor:
|
||||
"""The embed author for embeds sent by Payphone.
|
||||
|
||||
Returns:
|
||||
discord.EmbedAuthor: The embed author object.
|
||||
"""
|
||||
|
||||
return discord.EmbedAuthor(
|
||||
name=self.user.display_name, icon_url=self.user.display_avatar.url
|
||||
)
|
||||
return flag
|
||||
|
||||
|
||||
bot = Payphone()
|
||||
def run():
|
||||
"""Runs the bot."""
|
||||
|
||||
bot.load_extension("payphone.cogs.admin.cog")
|
||||
bot.load_extension("payphone.cogs.calls.cog")
|
||||
bot.load_extension("payphone.cogs.easter.cog")
|
||||
bot.load_extension("payphone.cogs.mods.cog")
|
||||
bot.load_extension("payphone.cogs.users.cog")
|
||||
bot.load_extension("payphone.cogs.utility.cog")
|
||||
bot = Payphone()
|
||||
|
||||
bot.loop.run_until_complete(setup.init())
|
||||
|
||||
@bot.check
|
||||
async def is_channel_enabled(ctx: commands.Context) -> Literal[True]:
|
||||
"""Checks whether the current channel is enabled to run a command or not.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The current context.
|
||||
|
||||
Raises:
|
||||
exceptions.ChannelDisabledError: The current channel is disabled.
|
||||
"""
|
||||
|
||||
if ctx.guild is None:
|
||||
return True
|
||||
|
||||
if ctx.command.qualified_name in ["config enable", "config disable"]:
|
||||
return True
|
||||
|
||||
server_id = ctx.guild.id
|
||||
channel_id = ctx.channel.id
|
||||
|
||||
await models.Server.get_or_create(id=server_id)
|
||||
channel, _ = await models.Channel.get_or_create(id=channel_id, server_id=server_id)
|
||||
|
||||
if channel.enabled:
|
||||
return True
|
||||
|
||||
if channel.enabled is False:
|
||||
raise exceptions.ChannelDisabledError()
|
||||
|
||||
if await models.Channel.filter(server_id=server_id, enabled=True).exists():
|
||||
raise exceptions.ChannelDisabledError()
|
||||
|
||||
return True
|
||||
bot.run(constants.TOKEN)
|
||||
|
||||
+43
-187
@@ -1,209 +1,65 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import discord
|
||||
from discord import errors
|
||||
from discord.ext import commands, tasks
|
||||
from discord.ext import commands
|
||||
|
||||
from tortoise import transactions, functions, exceptions
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import utils as dbutils, models
|
||||
from payphone.cogs import base
|
||||
from payphone.core import utils
|
||||
from payphone.db import models
|
||||
from payphone.bot import Payphone
|
||||
|
||||
|
||||
class Admin(base.Cog):
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
class Admin(commands.Cog):
|
||||
"""Administration in Payphone."""
|
||||
|
||||
self.update_leaderboard.add_exception_type(exceptions.ConfigurationError)
|
||||
self.update_leaderboard.start()
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
self.update_leaderboard.cancel()
|
||||
|
||||
@tasks.loop(minutes=15)
|
||||
async def update_leaderboard(self) -> None:
|
||||
"""Updates the leaderboard."""
|
||||
|
||||
calls = (
|
||||
await models.Call.annotate(messages_count=functions.Count("messages"))
|
||||
.filter(
|
||||
started_at__month=datetime.now(timezone.utc).month,
|
||||
started_at__year=datetime.now(timezone.utc).year,
|
||||
messages_count__gte=25,
|
||||
)
|
||||
.prefetch_related(
|
||||
"call_rss", "call_rss__channel", "call_rss__channel__server"
|
||||
)
|
||||
)
|
||||
|
||||
# ID: Count
|
||||
servers_map: dict[int, int] = {}
|
||||
servers: dict[int, models.Server] = {}
|
||||
|
||||
for call in calls:
|
||||
for rs in utils.deduplicate(call.call_rss, lambda i: i.channel.server_id):
|
||||
server_id = rs.channel.server_id
|
||||
servers[server_id] = rs.channel.server
|
||||
|
||||
if server_id in servers_map:
|
||||
servers_map[server_id] += 1
|
||||
|
||||
else:
|
||||
servers_map[server_id] = 1
|
||||
|
||||
server_counts = sorted(servers_map.items(), key=lambda i: i[1], reverse=True)
|
||||
|
||||
for pos, (server_id, calls_count) in enumerate(server_counts):
|
||||
servers[server_id].leaderboard_calls_count = calls_count
|
||||
servers[server_id].leaderboard_position = pos + 1
|
||||
|
||||
await models.Server.bulk_update(
|
||||
servers.values(),
|
||||
["leaderboard_calls_count", "leaderboard_position"],
|
||||
batch_size=500,
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_update(
|
||||
self, _before: discord.Guild, after: discord.Guild
|
||||
) -> None:
|
||||
"""Updates a server.
|
||||
def __init__(self, bot: Payphone) -> None:
|
||||
"""Initializes the Calls cog.
|
||||
|
||||
Args:
|
||||
_before (discord.Guild): The server before it being updated.
|
||||
after (discord.Guild): The server after it being updated.
|
||||
bot (Payphone): The bot instance.
|
||||
"""
|
||||
|
||||
vanity_code = None
|
||||
self.bot = bot
|
||||
|
||||
try:
|
||||
vanity = await after.vanity_invite()
|
||||
@commands.command(aliases=["mod"])
|
||||
@commands.is_owner()
|
||||
async def moderator(
|
||||
self, ctx: commands.Context, user: discord.User, toggle: bool
|
||||
) -> None:
|
||||
"""Gives the user the Moderator role."""
|
||||
|
||||
if vanity is not None:
|
||||
vanity_code = vanity.code
|
||||
|
||||
except errors.DiscordException:
|
||||
pass
|
||||
|
||||
await models.Server.update_or_create(
|
||||
{"name": after.name, "vanity_code": vanity_code}, id=after.id
|
||||
await models.User.update_or_create(
|
||||
id=user.id, defaults={"is_moderator": toggle}
|
||||
)
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
@commands.has_guild_permissions(manage_guild=True)
|
||||
async def config(
|
||||
self, ctx: commands.Context, setting: str = "", value: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Set up Payphone for the current server.
|
||||
|
||||
Settings:
|
||||
- `prefix`: Set a custom prefix.
|
||||
- `enable`: Enable a (or all) channel(s) to run commands in it. Non-enabled channels will pass to be disabled.
|
||||
- `disable`: Disables a channel to run commands in it. Non-disabled channels will stay enabled by default.
|
||||
|
||||
You can use `{prefix}help config <setting>` to get help about a specific configuration.
|
||||
"""
|
||||
|
||||
await ctx.send_help("config")
|
||||
|
||||
@config.command(extras={"help_name": "Prefix"})
|
||||
@commands.guild_only()
|
||||
@commands.has_guild_permissions(manage_guild=True)
|
||||
@transactions.atomic()
|
||||
async def prefix(self, ctx: commands.Context, prefix: str = "p.") -> None:
|
||||
"""
|
||||
Set a custom bot prefix.
|
||||
|
||||
Options:
|
||||
- `<prefix>`: The new custom prefix.
|
||||
|
||||
Permissions:
|
||||
- Manage Server
|
||||
"""
|
||||
|
||||
try:
|
||||
await self.bot.set_prefix(ctx.guild, prefix)
|
||||
except ValueError:
|
||||
await ctx.send("❌ The prefix must not be longer than 5 characters.")
|
||||
return
|
||||
|
||||
await ctx.send(f"✅ The prefix was set to `{prefix}`!")
|
||||
|
||||
@config.command(extras={"help_name": "Enable Channel"})
|
||||
@commands.guild_only()
|
||||
@commands.has_guild_permissions(manage_channels=True)
|
||||
@transactions.atomic()
|
||||
async def enable(
|
||||
self, ctx: commands.Context, channel: discord.TextChannel | str | None
|
||||
) -> None:
|
||||
"""
|
||||
Enable a channel to run commands in it. Non-enabled channels will pass to be disabled.
|
||||
|
||||
Options:
|
||||
- `[<channel>]`: The channel to enable or "all". If no channel is specified, the current channel will be enabled.
|
||||
|
||||
Permissions:
|
||||
- Manage Channels
|
||||
"""
|
||||
|
||||
server_id = ctx.guild.id
|
||||
channel = channel or ctx.channel
|
||||
|
||||
if isinstance(channel, str) and channel.lower() == "all":
|
||||
await models.Channel.filter(server_id=server_id).update(enabled=None)
|
||||
|
||||
await ctx.send(
|
||||
"✅ All channels were enabled! You can now run commands in any channel."
|
||||
)
|
||||
|
||||
else:
|
||||
row = await dbutils.get_channel(channel)
|
||||
|
||||
row.enabled = True
|
||||
await row.save()
|
||||
|
||||
await ctx.send(
|
||||
f"✅ {channel.mention} was enabled! Other non-enabled channels were disabled."
|
||||
)
|
||||
|
||||
@config.command(extras={"help_name": "Disable Channel"})
|
||||
@commands.guild_only()
|
||||
@commands.has_guild_permissions(manage_channels=True)
|
||||
@transactions.atomic()
|
||||
async def disable(
|
||||
self, ctx: commands.Context, channel: discord.TextChannel | None
|
||||
) -> None:
|
||||
"""
|
||||
Disables a channel to run commands in it. Non-disabled channels will stay enabled by default.
|
||||
|
||||
Options:
|
||||
- `[<channel>]`: The channel to disable. If no channel is specified, the current channel will be disabled.
|
||||
|
||||
Permissions:
|
||||
- Manage Channels
|
||||
"""
|
||||
|
||||
channel = channel or ctx.channel
|
||||
|
||||
row = await dbutils.get_channel(channel)
|
||||
|
||||
row.enabled = False
|
||||
await row.save()
|
||||
|
||||
await ctx.send(
|
||||
f"✅ {channel.mention} was disabled. Commands cannot be ran in this channel anymore."
|
||||
f"Toggled the moderator role {'on' if toggle else 'off'} for {user.mention}."
|
||||
)
|
||||
|
||||
@commands.command(aliases=["sup"])
|
||||
@commands.is_owner()
|
||||
async def supporter(
|
||||
self, ctx: commands.Context, user: discord.User, toggle: bool = True
|
||||
) -> None:
|
||||
"""Gives the user the Supporter role."""
|
||||
|
||||
await models.User.update_or_create(
|
||||
id=user.id,
|
||||
defaults={
|
||||
"is_supporter_until": (
|
||||
datetime.now(timezone.utc) + timedelta(days=30) if toggle else None
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
await ctx.send(
|
||||
f"Gave the Supporter role to {ctx.author.mention} until in one month."
|
||||
)
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
def setup(bot: Payphone) -> None:
|
||||
"""Loads the Admin cog into the bot.
|
||||
|
||||
Args:
|
||||
b (bot.Payphone): The bot instance to load the cog into.
|
||||
bot (Payphone): The bot instance.
|
||||
"""
|
||||
|
||||
b.add_cog(Admin(b))
|
||||
bot.add_cog(Admin(bot))
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
class Cog(commands.Cog):
|
||||
"""Base cog with Payphone-specific properties."""
|
||||
|
||||
show_in_help: bool = True
|
||||
+283
-1048
File diff suppressed because it is too large
Load Diff
@@ -1,104 +0,0 @@
|
||||
import discord
|
||||
|
||||
import orjson
|
||||
|
||||
from redis import Redis
|
||||
|
||||
from payphone.db import models
|
||||
from payphone.core import constants, formatting
|
||||
|
||||
|
||||
async def ReplyEmbed(
|
||||
redis: Redis, reference: discord.MessageReference, call_id: int
|
||||
) -> tuple[discord.Embed | None, models.Message | None]:
|
||||
"""Generates the embed sent on calls to represent replies.
|
||||
|
||||
Args:
|
||||
redis (redis.Redis): An instance of a Redis connection handler.
|
||||
reference (discord.MessageReference): The message reference.
|
||||
call_id (int): The ID of the call the message was sent in.
|
||||
|
||||
Returns:
|
||||
tuple[discord.Embed | None, models.Message | None]: The generated embed and the replied
|
||||
message.
|
||||
"""
|
||||
|
||||
message = None
|
||||
|
||||
unflushed_message = await redis.hget(
|
||||
constants.CALL_MESSAGES_TEMPLATE % call_id, reference.message_id
|
||||
)
|
||||
|
||||
if unflushed_message is None:
|
||||
message_id = await redis.hget(
|
||||
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call_id,
|
||||
reference.message_id,
|
||||
)
|
||||
|
||||
if message_id is not None:
|
||||
unflushed_message = await redis.hget(
|
||||
constants.CALL_MESSAGES_TEMPLATE % call_id, message_id
|
||||
)
|
||||
|
||||
if unflushed_message is None:
|
||||
return (None, None)
|
||||
|
||||
message = models.Message(**orjson.loads(unflushed_message))
|
||||
|
||||
unflushed_message_content_iteration = int(
|
||||
await redis.hget(
|
||||
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % call_id, message.id
|
||||
)
|
||||
)
|
||||
unflushed_message_content = await redis.hget(
|
||||
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call_id,
|
||||
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE
|
||||
% (message.id, unflushed_message_content_iteration),
|
||||
)
|
||||
|
||||
last_content = models.MessageContent(**orjson.loads(unflushed_message_content))
|
||||
|
||||
message.channel = await models.Channel.get(id=message.channel_id)
|
||||
message.profile = await models.Profile.get(id=message.profile_id)
|
||||
message.propagation_channel = await models.Channel.get(
|
||||
id=message.propagation_channel_id
|
||||
)
|
||||
|
||||
if message is None:
|
||||
return None, None
|
||||
|
||||
is_this_channel = message.id == reference.message_id
|
||||
|
||||
if last_content is None:
|
||||
for content in message.contents:
|
||||
if last_content is None:
|
||||
last_content = content
|
||||
continue
|
||||
|
||||
if last_content.message_id < content.message_id:
|
||||
last_content = content
|
||||
|
||||
author_name = message.display_name if is_this_channel else message.profile.name
|
||||
author_avatar_url = (
|
||||
message.display_avatar if is_this_channel else message.profile.avatar_url
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=(
|
||||
constants.EMBED_COLOR
|
||||
if not is_this_channel and message.mask is not None
|
||||
else message.profile.color or constants.EMBED_COLOR
|
||||
),
|
||||
description=(
|
||||
formatting.trim(last_content.content)
|
||||
if last_content.was_propagated or is_this_channel
|
||||
else "*This message was censored.*"
|
||||
),
|
||||
author=discord.EmbedAuthor(
|
||||
name=f"Replied to {author_name}",
|
||||
icon_url=author_avatar_url,
|
||||
url=message.jump_url(is_this_channel),
|
||||
),
|
||||
)
|
||||
|
||||
return embed, message
|
||||
@@ -1,10 +0,0 @@
|
||||
import enum
|
||||
|
||||
|
||||
class EasterEggCommand(enum.IntFlag):
|
||||
"""Flags representing the easter eggs.
|
||||
|
||||
This is used to keep track of which easter eggs did the user find."""
|
||||
|
||||
CATMASK = 1
|
||||
USERPHONE = 2
|
||||
@@ -1,35 +0,0 @@
|
||||
from discord.ext import commands
|
||||
|
||||
from payphone.db import utils
|
||||
from payphone.cogs.easter import EasterEggCommand
|
||||
|
||||
|
||||
def easter_egg(command: EasterEggCommand):
|
||||
"""Send rewards to the user when they find an easter egg."""
|
||||
|
||||
async def predicate(ctx: commands.Context) -> bool:
|
||||
if ctx.author.bot:
|
||||
return False
|
||||
|
||||
user, _ = await utils.get_user(ctx.author)
|
||||
|
||||
if command not in user.found_easter_eggs:
|
||||
user.found_easter_eggs |= command
|
||||
|
||||
remaining_easter_eggs = len(list(EasterEggCommand)) - len(
|
||||
user.found_easter_eggs.name.split("|")
|
||||
)
|
||||
await ctx.send(
|
||||
"✨ You found an easter egg! "
|
||||
+ (
|
||||
f"There are {remaining_easter_eggs} easter eggs remaining."
|
||||
if remaining_easter_eggs > 0
|
||||
else "There are no easter eggs left, congratulations! 🎉"
|
||||
)
|
||||
)
|
||||
|
||||
await user.save()
|
||||
|
||||
return True
|
||||
|
||||
return commands.check(predicate)
|
||||
@@ -1,66 +0,0 @@
|
||||
import secrets
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from tortoise import transactions
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models
|
||||
from payphone.cogs import base
|
||||
from payphone.core import constants
|
||||
from payphone.cogs.easter import EasterEggCommand, checks
|
||||
|
||||
|
||||
class Easter(base.Cog):
|
||||
show_in_help = False
|
||||
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
|
||||
@commands.command()
|
||||
@checks.easter_egg(EasterEggCommand.CATMASK)
|
||||
@transactions.atomic()
|
||||
async def catmask(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
This is a secret command!
|
||||
"""
|
||||
|
||||
await ctx.message.delete()
|
||||
|
||||
new_mask = secrets.choice(constants.CAT_MASKS_LIST)
|
||||
|
||||
channel_rs = (
|
||||
await models.CallUserChannelRelationship.filter(
|
||||
channel_id=ctx.channel.id, user_id=ctx.author.id
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.limit(1)
|
||||
.select_related("call")
|
||||
.first()
|
||||
)
|
||||
|
||||
if channel_rs is None or channel_rs.call.ended_at is not None:
|
||||
return
|
||||
|
||||
channel_rs.mask = new_mask.value
|
||||
channel_rs.is_masked = True
|
||||
await channel_rs.save()
|
||||
|
||||
@commands.command(aliases=["speakerphone"])
|
||||
@checks.easter_egg(EasterEggCommand.USERPHONE)
|
||||
async def userphone(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
The second easter egg! You found it.
|
||||
"""
|
||||
|
||||
await ctx.send("☎️ Please insert 25 cents to complete this call.")
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
|
||||
Args:
|
||||
b (bot.Payphone): The bot instance to load the cog into.
|
||||
"""
|
||||
|
||||
b.add_cog(Easter(b))
|
||||
@@ -1,443 +0,0 @@
|
||||
from typing import Awaitable
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord import errors
|
||||
from discord.ext import commands
|
||||
|
||||
from tortoise import transactions, expressions, functions
|
||||
|
||||
from procs import flush_messages
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import utils, models
|
||||
from payphone.cogs import base
|
||||
from payphone.cogs.mods import embeds, views
|
||||
from payphone.core import checks, converters, constants, formatting as fmt
|
||||
|
||||
|
||||
class Mods(base.Cog):
|
||||
show_in_help = False
|
||||
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
|
||||
@commands.command(extras={"reply_argument": "user"})
|
||||
@checks.is_moderator()
|
||||
@transactions.atomic()
|
||||
async def ban(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
user: converters.DiscordUserID,
|
||||
duration: converters.Timedelta | None = None,
|
||||
*,
|
||||
reason: str | None = "",
|
||||
) -> None:
|
||||
"""
|
||||
Ban a user by their ID, pinging them, or replying to their message.
|
||||
|
||||
Options:
|
||||
- `<user>`: The user to ban or their ID.
|
||||
- `[<duration>]`: The amount of time to ban the user for. For example, `1d 5h`.
|
||||
- `[<reason>]`: The reason of their ban.
|
||||
"""
|
||||
|
||||
user_id = user
|
||||
|
||||
profile = await models.Profile.filter(user_id=user_id).limit(1).first()
|
||||
|
||||
row = await self.bot.ban(
|
||||
user_id,
|
||||
duration,
|
||||
reason,
|
||||
ctx.author,
|
||||
)
|
||||
|
||||
message = f"✅ {f"**{profile.name}**" if profile is not None else f"`ID: {user_id}`"} was banned"
|
||||
|
||||
if row.expires_at:
|
||||
message += f"{f" for `{fmt.time_to_text(duration)}`" if duration is not None else ""} until <t:{int(row.expires_at.timestamp())}:F> (<t:{int(row.expires_at.timestamp())}:R>)"
|
||||
|
||||
else:
|
||||
message += " permanently"
|
||||
|
||||
message += "."
|
||||
|
||||
await ctx.send(message)
|
||||
|
||||
try:
|
||||
notification_message = await (
|
||||
await self.bot.get_or_fetch_user(user_id)
|
||||
).send(
|
||||
embed=embeds.BanNotificationEmbed(self.bot, row),
|
||||
)
|
||||
|
||||
row.notification_message_id = notification_message.id
|
||||
await row.save()
|
||||
|
||||
except errors.DiscordException:
|
||||
pass
|
||||
|
||||
@commands.command(extras={"reply_argument": "user"})
|
||||
@checks.is_moderator()
|
||||
@transactions.atomic()
|
||||
async def unban(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
user: converters.DiscordUserID,
|
||||
) -> None:
|
||||
"""
|
||||
Unbans a user by their ID, pinging them, or replying to their message. The user will be
|
||||
attempted to get notified of the unban.
|
||||
|
||||
Options:
|
||||
- `<user>`: The user to unban or their ID.
|
||||
"""
|
||||
|
||||
user_id = user
|
||||
|
||||
await self.bot.unban(user_id, ctx.author)
|
||||
|
||||
profile = await models.Profile.filter(user_id=user_id).limit(1).first()
|
||||
|
||||
await ctx.send(
|
||||
f"✅ {f"**{profile.name}**" if profile is not None else f"`ID: {user_id}`"} was unbanned."
|
||||
)
|
||||
|
||||
try:
|
||||
await (await self.bot.get_or_fetch_user(user_id)).send(
|
||||
"🔔 You have been unbanned from Payphone."
|
||||
)
|
||||
except errors.DiscordException:
|
||||
pass
|
||||
|
||||
@commands.command(extras={"reply_argument": "user"})
|
||||
@checks.is_moderator()
|
||||
@transactions.atomic()
|
||||
async def warn(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
user: converters.DiscordUserID,
|
||||
*,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""
|
||||
Warn a user by their ID or mention.
|
||||
|
||||
Options:
|
||||
- `<user>`: The user's mention or their ID.
|
||||
- `<reason>`: The reason of the warn.
|
||||
"""
|
||||
|
||||
user_id = user
|
||||
|
||||
user = None
|
||||
|
||||
try:
|
||||
user = await self.bot.get_or_fetch_user(user_id)
|
||||
except errors.DiscordError:
|
||||
await ctx.send(
|
||||
"❌ Could not find this user. It may be because the user has left all servers with Payphone in them."
|
||||
)
|
||||
return
|
||||
|
||||
embed = discord.Embed(
|
||||
color=constants.EMBED_COLOR,
|
||||
title="You've been warned",
|
||||
description=reason,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
author=self.bot.embed_author,
|
||||
footer=discord.EmbedFooter(
|
||||
text="Ignoring this warning will result in a ban."
|
||||
),
|
||||
thumbnail=self.bot.user.display_avatar.url,
|
||||
)
|
||||
|
||||
profile = await models.Profile.filter(user_id=user_id).limit(1).first()
|
||||
|
||||
try:
|
||||
await user.send(embed=embed)
|
||||
except errors.DiscordException:
|
||||
await ctx.send(
|
||||
f"❌ Could not send the warning to {f"**{profile.name}**" if profile is not None else "the user"}, probably because they have their DMs closed."
|
||||
)
|
||||
return
|
||||
|
||||
await ctx.send(
|
||||
f"✅ {f"**{profile.name}**" if profile is not None else "the user"} has been warned."
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@checks.is_moderator()
|
||||
@transactions.atomic()
|
||||
async def reports(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
Shows the next report to review.
|
||||
"""
|
||||
|
||||
report = (
|
||||
await models.Report.filter(status=models.ReportStatus.ON_REVIEW)
|
||||
.order_by("-priority")
|
||||
.limit(1)
|
||||
.first()
|
||||
.select_related("call")
|
||||
.prefetch_related("call__call_rss", "call__call_rss__profile")
|
||||
)
|
||||
total = await models.Report.filter(status=models.ReportStatus.ON_REVIEW).count()
|
||||
|
||||
if report is None:
|
||||
await ctx.send("✅ No reports left, have this cookie 🍪 !!")
|
||||
return
|
||||
|
||||
await ctx.send(
|
||||
embed=embeds.ReportEmbed(self.bot, report),
|
||||
view=views.ReportView(self.bot, report, total),
|
||||
)
|
||||
|
||||
@commands.command(aliases=["mod"])
|
||||
@commands.is_owner()
|
||||
@transactions.atomic()
|
||||
async def moderator(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
user: discord.User | discord.Member,
|
||||
toggle: bool | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Toggles a user's moderator status. It can only be ran by Payphone's owner.
|
||||
|
||||
Options:
|
||||
- `<user>`: The user to toggle the moderation status for.
|
||||
- `[<toggle>]`: The moderator status to set to the user.
|
||||
"""
|
||||
|
||||
user_row, profile_row = await utils.get_user(user)
|
||||
|
||||
if toggle is not None:
|
||||
user_row.is_moderator = toggle
|
||||
|
||||
else:
|
||||
user_row.is_moderator = not user_row.is_moderator
|
||||
|
||||
await user_row.save()
|
||||
|
||||
await ctx.send(
|
||||
f"✅ **{profile_row.name}**'s moderator status was set to `{"on" if user_row.is_moderator else "off"}`."
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@commands.is_owner()
|
||||
async def stats(
|
||||
self, ctx: commands.Context, iterations: int = 4, wait: int = 5
|
||||
) -> None:
|
||||
"""
|
||||
Shows all Payphone's stats.
|
||||
|
||||
Options:
|
||||
- `<iterations>`: The amount of iterations (to display progress). Defaults to 5.
|
||||
- `<wait>`: The amount of seconds to wait between iterations. Defaults to 5.
|
||||
"""
|
||||
|
||||
async def count_banned_users() -> int:
|
||||
return (
|
||||
await models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at=None,
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
join_type="OR",
|
||||
)
|
||||
)
|
||||
.annotate(unique_users_count=functions.Count("user_id", distinct=True))
|
||||
.values("unique_users_count")
|
||||
)[0]["unique_users_count"]
|
||||
|
||||
stats = [
|
||||
(
|
||||
"On-call Channels",
|
||||
lambda: self.bot.redis.scard(constants.CHANNELS_ONCALL_SET),
|
||||
),
|
||||
(
|
||||
"Searching Channels",
|
||||
lambda: self.bot.redis.scard(constants.CHANNELS_SEARCH_QUEUE),
|
||||
),
|
||||
(
|
||||
"Messages",
|
||||
lambda: models.Message.all().count(),
|
||||
),
|
||||
(
|
||||
"Message Contents",
|
||||
lambda: models.MessageContent.all().count(),
|
||||
),
|
||||
("Calls", lambda: models.Call.all().count()),
|
||||
(
|
||||
"Ongoing Calls",
|
||||
lambda: models.Call.filter(ended_at=None).count(),
|
||||
),
|
||||
("Bans", lambda: models.Ban.all().count()),
|
||||
(
|
||||
"Ongoing Bans",
|
||||
lambda: models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at=None,
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
join_type="OR",
|
||||
)
|
||||
).count(),
|
||||
),
|
||||
(
|
||||
"Banned Users",
|
||||
count_banned_users,
|
||||
),
|
||||
("Reports", lambda: models.Report.all().count()),
|
||||
(
|
||||
"On-review Reports",
|
||||
lambda: models.Report.filter(
|
||||
status=models.ReportStatus.ON_REVIEW
|
||||
).count(),
|
||||
),
|
||||
("Users", lambda: models.User.all().count()),
|
||||
(
|
||||
"Profiles",
|
||||
lambda: models.Profile.all().count(),
|
||||
),
|
||||
("Servers", lambda: models.Server.all().count()),
|
||||
("Shards", lambda: self.bot.shard_count),
|
||||
("Current Shard", lambda: ctx.guild.shard_id + 1),
|
||||
]
|
||||
|
||||
rows = [[stat[0]] for stat in stats]
|
||||
rows_with_diff = [[stat[0]] for stat in stats]
|
||||
|
||||
i = 0
|
||||
message = None
|
||||
|
||||
while i < iterations:
|
||||
i += 1
|
||||
|
||||
for stat_i, stat in enumerate(stats):
|
||||
value = stat[1]()
|
||||
|
||||
if isinstance(value, Awaitable):
|
||||
value = await value
|
||||
|
||||
rows[stat_i].append(value)
|
||||
prev_value = rows[stat_i][-2]
|
||||
|
||||
if isinstance(prev_value, str):
|
||||
rows_with_diff[stat_i].append(f"{value:,}")
|
||||
|
||||
else:
|
||||
diff = value - prev_value
|
||||
|
||||
rows_with_diff[stat_i].append(
|
||||
f"{value:,}"
|
||||
if diff == 0
|
||||
else (f"{value:,}", f"({"+" if diff > 0 else ""}{diff})")
|
||||
)
|
||||
|
||||
table = fmt.monospace_table(
|
||||
rows_with_diff,
|
||||
[""] + [f"{wait * j}s" for j in range(i)],
|
||||
padding=2,
|
||||
)
|
||||
|
||||
if message is None:
|
||||
message = await ctx.send(f"```\n{table}\n```")
|
||||
|
||||
else:
|
||||
message = await message.edit(f"```\n{table}\n```")
|
||||
|
||||
if i < iterations:
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
@commands.command()
|
||||
@checks.is_moderator()
|
||||
async def user(self, ctx: commands.Context, user: converters.DiscordUserID) -> None:
|
||||
"""
|
||||
Shows all the information about a user.
|
||||
|
||||
Options:
|
||||
- `[<user>]`: The user, their ID, or `.` or `reply` replying to their message.
|
||||
"""
|
||||
|
||||
user_id = user
|
||||
|
||||
user = await models.User.get_or_none(id=user_id).prefetch_related("profiles")
|
||||
profile: models.Profile | None = user.profiles[0] if user is not None else None
|
||||
|
||||
bans = await models.Ban.filter(
|
||||
user_id=user_id,
|
||||
).order_by("-created_at")
|
||||
|
||||
ongoing_bans = [
|
||||
ban
|
||||
for ban in bans
|
||||
if ban.expires_at is None or ban.expires_at > datetime.now(timezone.utc)
|
||||
]
|
||||
expired_bans = [
|
||||
ban
|
||||
for ban in bans
|
||||
if ban.expires_at is not None
|
||||
and ban.expires_at < datetime.now(timezone.utc)
|
||||
]
|
||||
|
||||
embed = discord.Embed(
|
||||
color=(
|
||||
(profile.color or constants.EMBED_COLOR)
|
||||
if profile is not None
|
||||
else constants.EMBED_COLOR
|
||||
),
|
||||
title="Profile Information",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Ongoing Bans",
|
||||
value=fmt.trim(
|
||||
"\n".join(
|
||||
[
|
||||
f"- `ID: {ban.id}`{f" (by <@{ban.banned_by_id}>)" if ban.banned_by_id else ""}: {ban.reason if ban.reason else "*No reason*"}. Banned {f"until <t:{int(ban.expires_at.timestamp())}:R>" if ban.expires_at is not None else "permanently"}."
|
||||
for ban in ongoing_bans
|
||||
]
|
||||
),
|
||||
1024,
|
||||
)
|
||||
or "*No ongoing bans.*",
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Past Bans",
|
||||
value=fmt.trim(
|
||||
"\n".join(
|
||||
[
|
||||
f"- `ID: {ban.id}`{f" (by <@{ban.banned_by_id}>)" if ban.banned_by_id else None}: {ban.reason if ban.reason else "*No reason*"}. {"Expired" if ban.unbanned_by_id is None else f"Unbanned by <@{ban.unbanned_by_id}>"} <t:{int(ban.expires_at.timestamp())}:R>."
|
||||
for ban in expired_bans
|
||||
]
|
||||
),
|
||||
1024,
|
||||
)
|
||||
or "*No expired bans.*",
|
||||
),
|
||||
],
|
||||
author=(
|
||||
profile.embed_author if profile is not None else self.bot.embed_author
|
||||
),
|
||||
thumbnail=(
|
||||
profile.avatar_url
|
||||
if profile is not None
|
||||
else self.bot.user.display_avatar.url
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
|
||||
Args:
|
||||
b (bot.Payphone): The bot instance to load the cog into.
|
||||
"""
|
||||
|
||||
b.add_cog(Mods(b))
|
||||
@@ -1,84 +0,0 @@
|
||||
import discord
|
||||
from discord.utils import find
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models
|
||||
from payphone.core import constants, formatting
|
||||
|
||||
|
||||
class BanNotificationEmbed(discord.Embed):
|
||||
"""Notification embed sent to users when they get banned."""
|
||||
|
||||
def __init__(self, bot: bot.Payphone, ban: models.Ban) -> None:
|
||||
"""Initializes the embed.
|
||||
|
||||
Args:
|
||||
bot (bot.Payphone): The instance of the bot.
|
||||
ban (models.Ban): The ban.
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
color=constants.EMBED_COLOR,
|
||||
title="You've Been Banned from Payphone",
|
||||
description="You have been banned from using Payphone.",
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Reason",
|
||||
value=ban.reason if ban.reason else "*No reason.*",
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Expires At",
|
||||
value=(
|
||||
f"<t:{int(ban.expires_at.timestamp())}:F> (<t:{int(ban.expires_at.timestamp())}:R>)"
|
||||
if ban.expires_at
|
||||
else "*Does not expire.*"
|
||||
),
|
||||
),
|
||||
],
|
||||
author=bot.embed_author,
|
||||
footer=discord.EmbedFooter(
|
||||
text="If the ban was automatically set, it may be removed after a moderator checks it."
|
||||
),
|
||||
thumbnail=bot.user.display_avatar.url,
|
||||
)
|
||||
|
||||
|
||||
class ReportEmbed(discord.Embed):
|
||||
"""The report embed shown to moderators."""
|
||||
|
||||
def __init__(self, bot: bot.Payphone, report: models.Report) -> None:
|
||||
"""Initializes the embed.
|
||||
|
||||
Args:
|
||||
bot (bot.Payphone): The instance of the bot.
|
||||
report (models.Report): The report.
|
||||
"""
|
||||
|
||||
reported_by_profile: models.Profile = find(
|
||||
lambda rs: rs.user_id == report.reported_by_id, report.call.call_rss
|
||||
).profile
|
||||
|
||||
super().__init__(
|
||||
color=constants.EMBED_COLOR,
|
||||
title="Report",
|
||||
description=f"{reported_by_profile.link} reported a call.",
|
||||
fields=[
|
||||
discord.EmbedField(name="Reason", value=report.reason),
|
||||
discord.EmbedField(
|
||||
name="Participants",
|
||||
value=(
|
||||
f"Channel 1: {", ".join(map(lambda rs: rs.profile.link, filter(lambda rs: rs.channel_id == report.call.call_rss[0].channel_id, report.call.call_rss)))}\n"
|
||||
f"Channel 2: {", ".join(map(lambda rs: rs.profile.link, filter(lambda rs: rs.channel_id != report.call.call_rss[0].channel_id, report.call.call_rss)))}"
|
||||
),
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Duration",
|
||||
value=f"{formatting.time_to_text(report.call.ended_at - report.call.started_at)}, from <t:{int(report.call.started_at.timestamp())}:F> to <t:{int(report.call.ended_at.timestamp())}:F>.",
|
||||
),
|
||||
],
|
||||
author=bot.embed_author,
|
||||
footer=discord.EmbedFooter(
|
||||
text=f"Call ID: {report.call.id} | Report ID: {report.id}"
|
||||
),
|
||||
thumbnail=bot.user.display_avatar.url,
|
||||
)
|
||||
@@ -1,90 +0,0 @@
|
||||
import discord
|
||||
from discord import errors
|
||||
|
||||
from tortoise import transactions
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models
|
||||
from payphone.core import formatting
|
||||
from payphone.cogs.mods import embeds
|
||||
|
||||
|
||||
class BanUserModal(discord.ui.Modal):
|
||||
"""Modal sent to ban a user."""
|
||||
|
||||
def __init__(
|
||||
self, bot: bot.Payphone, profile: models.Profile, report_id: int | None = None
|
||||
) -> None:
|
||||
"""Initializes the modal.
|
||||
|
||||
Args:
|
||||
bot (bot.Bot): The bot.
|
||||
report_id (int, optional): The ID of the report, if any.
|
||||
profile (models.Profile): The profile of the user to ban.
|
||||
"""
|
||||
|
||||
self.bot = bot
|
||||
self.profile = profile
|
||||
self.report_id = report_id
|
||||
|
||||
super().__init__(
|
||||
discord.ui.InputText(
|
||||
style=discord.InputTextStyle.long,
|
||||
label="Reason",
|
||||
max_length=500,
|
||||
required=False,
|
||||
),
|
||||
discord.ui.InputText(
|
||||
label="Duration", placeholder="30d", value="30d", required=False
|
||||
),
|
||||
title=f"Ban {profile.name}",
|
||||
)
|
||||
|
||||
@transactions.atomic()
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
await interaction.response.defer()
|
||||
|
||||
duration = None
|
||||
|
||||
if self.children[1].value:
|
||||
try:
|
||||
duration = formatting.parse_timedelta(self.children[1].value)
|
||||
|
||||
except ValueError:
|
||||
await interaction.response.send_message(
|
||||
f"❌ The ban's duration (`{self.children[1].value}`) is not a valid time. Time must be formatted like `28d 3h 5s`."
|
||||
)
|
||||
return
|
||||
|
||||
ban = await self.bot.ban(
|
||||
self.profile.user_id,
|
||||
duration,
|
||||
self.children[0].value,
|
||||
interaction.user.id,
|
||||
self.report_id
|
||||
)
|
||||
|
||||
try:
|
||||
notification_message = await (
|
||||
await self.bot.get_or_fetch_user(self.profile.user_id)
|
||||
).send(
|
||||
embed=embeds.BanNotificationEmbed(self.bot, ban)
|
||||
)
|
||||
|
||||
ban.notification_message_id = notification_message.id
|
||||
await ban.save()
|
||||
|
||||
except errors.DiscordException:
|
||||
pass
|
||||
|
||||
message = f"✅ **{self.profile.name}** (User ID: {self.profile.user_id}) was banned by {interaction.user.mention}"
|
||||
|
||||
if ban.expires_at is not None:
|
||||
message += f"{f" for `{formatting.time_to_text(duration)}`" if duration is not None else ""} until <t:{int(ban.expires_at.timestamp())}:F> (<t:{int(ban.expires_at.timestamp())}:R>)"
|
||||
|
||||
else:
|
||||
message += " permanently"
|
||||
|
||||
message += "."
|
||||
|
||||
await interaction.channel.send(message)
|
||||
@@ -1,233 +0,0 @@
|
||||
import discord
|
||||
from discord import errors
|
||||
from discord.ext import pages
|
||||
from discord.utils import find
|
||||
|
||||
from tortoise import transactions
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models
|
||||
from payphone.core import constants, formatting as fmt
|
||||
from payphone.cogs.mods import modals, views, embeds
|
||||
|
||||
|
||||
class ReportView(discord.ui.View):
|
||||
"""The view sent together with the report embed."""
|
||||
|
||||
def __init__(
|
||||
self, bot: bot.Payphone, report: models.Report, remaining_reports: int
|
||||
) -> None:
|
||||
"""Initializes the view.
|
||||
|
||||
Args:
|
||||
bot (bot.Payphone): The bot.
|
||||
report (models.Report): The report.
|
||||
remaining_reports (int): The amount of reports that need to be reviewed remaining.
|
||||
"""
|
||||
|
||||
super().__init__(timeout=900, disable_on_timeout=True)
|
||||
|
||||
self.bot = bot
|
||||
self.report = report
|
||||
|
||||
option_labels = set(
|
||||
f"Ban {rs.profile.name} [ID: {rs.user_id}]" for rs in report.call.call_rss
|
||||
)
|
||||
|
||||
actions = discord.ui.Select(
|
||||
placeholder="Actions",
|
||||
options=[discord.SelectOption(label=label) for label in option_labels],
|
||||
)
|
||||
|
||||
async def actions_callback(interaction: discord.Interaction) -> None:
|
||||
# interaction.data = {'values': ['Ban Nyeki (JPP)'], 'custom_id': 'staff_call_report:actions', 'component_type': 3}
|
||||
value = interaction.data["values"][0]
|
||||
|
||||
user_id = int(value.rsplit("[ID: ", 1)[1][:-1])
|
||||
|
||||
await interaction.response.send_modal(
|
||||
modals.BanUserModal(
|
||||
bot,
|
||||
find(
|
||||
lambda rs: rs.user_id == user_id, report.call.call_rss
|
||||
).profile,
|
||||
report.id,
|
||||
)
|
||||
)
|
||||
|
||||
actions.callback = actions_callback
|
||||
|
||||
self.add_item(actions)
|
||||
self.add_item(
|
||||
discord.ui.Button(
|
||||
label=f"Remaining: {remaining_reports}", disabled=True, row=2
|
||||
)
|
||||
)
|
||||
|
||||
@discord.ui.button(label="Message Logs", style=discord.ButtonStyle.gray, row=2)
|
||||
async def message_logs(
|
||||
self, _button: discord.ui.Button, interaction: discord.Interaction
|
||||
) -> None:
|
||||
report = self.report
|
||||
|
||||
messages = (
|
||||
await models.Message.filter(call=report.call)
|
||||
.order_by("id")
|
||||
.prefetch_related("contents")
|
||||
)
|
||||
|
||||
logs = []
|
||||
|
||||
reporter_channel_id = find(
|
||||
lambda rs: rs.user_id == report.reported_by_id, report.call.call_rss
|
||||
).channel_id
|
||||
|
||||
profiles_to_relationship_mapping = {
|
||||
rs.profile_id: rs for rs in report.call.call_rss
|
||||
}
|
||||
|
||||
last_user_id = None
|
||||
last_channel_id = None
|
||||
|
||||
current_block = ""
|
||||
|
||||
for message in messages:
|
||||
is_new_block = (
|
||||
message.user_id != last_user_id or last_channel_id != message.channel_id
|
||||
)
|
||||
|
||||
if is_new_block:
|
||||
logs.append(current_block)
|
||||
current_block = ""
|
||||
|
||||
current_block += f"{"+" if message.channel_id == reporter_channel_id else "-"} {f"{profiles_to_relationship_mapping[message.profile_id].profile.name}{f" (Masked as {profiles_to_relationship_mapping[message.profile_id].mask_name})" if profiles_to_relationship_mapping[message.profile_id].is_masked else ""}" if message.profile_id in profiles_to_relationship_mapping else "Unknown"}:"
|
||||
|
||||
for content in message.contents:
|
||||
current_block += (
|
||||
f"\n{"!" if not content.was_propagated else "X" if message.was_deleted else " "} {content.content}"
|
||||
)
|
||||
|
||||
last_user_id = message.user_id
|
||||
last_channel_id = message.channel_id
|
||||
|
||||
log_pages = []
|
||||
|
||||
for current_block in logs:
|
||||
if len(log_pages) > 0 and len(log_pages[-1]) + len(current_block) < 1000:
|
||||
log_pages[-1] += "\n\n" + current_block.replace(
|
||||
"```",
|
||||
f"`{constants.INVISIBLE_CHARACTER}`{constants.INVISIBLE_CHARACTER}`",
|
||||
)
|
||||
|
||||
else:
|
||||
log_pages.append(
|
||||
current_block.replace(
|
||||
"```",
|
||||
f"`{constants.INVISIBLE_CHARACTER}`{constants.INVISIBLE_CHARACTER}`",
|
||||
)
|
||||
)
|
||||
|
||||
await pages.Paginator(
|
||||
[f"```diff\n{page}\n```" for page in log_pages],
|
||||
timeout=300,
|
||||
).respond(interaction, ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="Accept", style=discord.ButtonStyle.green, row=2)
|
||||
@transactions.atomic()
|
||||
async def accept(
|
||||
self, _button: discord.ui.Button, interaction: discord.Interaction
|
||||
) -> None:
|
||||
report = self.report
|
||||
|
||||
report.status = models.ReportStatus.ACCEPTED
|
||||
report.reviewed_by_id = interaction.user.id
|
||||
await report.save()
|
||||
|
||||
next_report = (
|
||||
await models.Report.filter(status=models.ReportStatus.ON_REVIEW)
|
||||
.order_by("-priority")
|
||||
.limit(1)
|
||||
.first()
|
||||
.select_related("call")
|
||||
.prefetch_related("call__call_rss", "call__call_rss__profile")
|
||||
)
|
||||
total = await models.Report.filter(status=models.ReportStatus.ON_REVIEW).count()
|
||||
|
||||
banned_user_ids = (
|
||||
await models.Ban.filter(report=report)
|
||||
.distinct()
|
||||
.values_list("user_id", flat=True)
|
||||
)
|
||||
|
||||
if next_report is None:
|
||||
await interaction.response.edit_message(
|
||||
content="✅ No reports left, have this cookie 🍪 !!",
|
||||
embed=None,
|
||||
view=None,
|
||||
)
|
||||
|
||||
else:
|
||||
await interaction.response.edit_message(
|
||||
embed=embeds.ReportEmbed(self.bot, next_report),
|
||||
view=views.ReportView(self.bot, next_report, total),
|
||||
)
|
||||
|
||||
if len(banned_user_ids) > 0:
|
||||
profiles = [
|
||||
rs.profile
|
||||
for rs in report.call.call_rss
|
||||
if rs.user_id in banned_user_ids
|
||||
]
|
||||
|
||||
try:
|
||||
await (await self.bot.get_or_fetch_user(report.reported_by_id)).send(
|
||||
f"🔔 Your report for call `{report.call_id}` helped us ban "
|
||||
+ fmt.text_join(
|
||||
[
|
||||
(
|
||||
f"`{p.name}`"
|
||||
if p.user_id != report.reported_by_id
|
||||
else "`you`"
|
||||
)
|
||||
for p in profiles
|
||||
]
|
||||
)
|
||||
+ ". Thank you for your report!"
|
||||
)
|
||||
|
||||
except errors.DiscordException:
|
||||
pass
|
||||
|
||||
@discord.ui.button(label="Decline", style=discord.ButtonStyle.red, row=2)
|
||||
@transactions.atomic()
|
||||
async def decline(
|
||||
self, _button: discord.ui.Button, interaction: discord.Interaction
|
||||
) -> None:
|
||||
report = self.report
|
||||
|
||||
report.status = models.ReportStatus.DECLINED
|
||||
report.reviewed_by_id = interaction.user.id
|
||||
await report.save()
|
||||
|
||||
report = (
|
||||
await models.Report.filter(status=models.ReportStatus.ON_REVIEW)
|
||||
.order_by("-priority")
|
||||
.limit(1)
|
||||
.first()
|
||||
.select_related("call")
|
||||
.prefetch_related("call__call_rss", "call__call_rss__profile")
|
||||
)
|
||||
total = await models.Report.filter(status=models.ReportStatus.ON_REVIEW).count()
|
||||
|
||||
if report is None:
|
||||
await interaction.response.edit_message(
|
||||
content="✅ No reports left, have this cookie 🍪 !!",
|
||||
embed=None,
|
||||
view=None,
|
||||
)
|
||||
return
|
||||
|
||||
await interaction.response.edit_message(
|
||||
embed=embeds.ReportEmbed(self.bot, report),
|
||||
view=views.ReportView(self.bot, report, total),
|
||||
)
|
||||
@@ -1,592 +0,0 @@
|
||||
import io
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, pages, tasks
|
||||
from discord.utils import find
|
||||
|
||||
from tortoise import transactions, functions, expressions, exceptions
|
||||
|
||||
import colorthief
|
||||
|
||||
import httpx
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models, utils
|
||||
from payphone.core import constants
|
||||
from payphone.cogs import base
|
||||
from payphone.cogs.users import views
|
||||
|
||||
|
||||
class Users(base.Cog):
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
|
||||
self.update_leaderboard.add_exception_type(exceptions.ConfigurationError)
|
||||
self.update_leaderboard.start()
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
self.update_leaderboard.cancel()
|
||||
|
||||
@tasks.loop(minutes=15)
|
||||
async def update_leaderboard(self) -> None:
|
||||
"""Updates the leaderboard."""
|
||||
|
||||
calls = (
|
||||
await models.Call.annotate(messages_count=functions.Count("messages"))
|
||||
.filter(
|
||||
started_at__month=datetime.now(timezone.utc).month,
|
||||
started_at__year=datetime.now(timezone.utc).year,
|
||||
messages_count__gte=25,
|
||||
)
|
||||
.prefetch_related("call_rss", "call_rss__user")
|
||||
)
|
||||
|
||||
# ID to count
|
||||
users_count_map: dict[int, int] = {}
|
||||
|
||||
users_map: dict[int, models.User] = {
|
||||
rs.user.id: rs.user for call in calls for rs in call.call_rss
|
||||
}
|
||||
|
||||
for call in calls:
|
||||
for rs in call.call_rss:
|
||||
if rs.user_id in users_count_map:
|
||||
users_count_map[rs.user_id] += 1
|
||||
|
||||
else:
|
||||
users_count_map[rs.user_id] = 0
|
||||
|
||||
user_counts = list(users_count_map.items())
|
||||
|
||||
user_counts.sort(key=lambda i: i[1], reverse=True)
|
||||
|
||||
for i, user_map in enumerate(user_counts):
|
||||
users_map[user_map[0]].leaderboard_position = i + 1
|
||||
users_map[user_map[0]].leaderboard_calls_count = user_map[1]
|
||||
|
||||
await models.User.bulk_update(
|
||||
users_map.values(), ["leaderboard_position", "leaderboard_calls_count"], 500
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
@transactions.atomic()
|
||||
async def on_user_update(self, _before: discord.User, after: discord.User) -> None:
|
||||
"""Updates a user's Profile.
|
||||
|
||||
Args:
|
||||
_before (discord.User): The user before the change.
|
||||
after (discord.User): The user after the change.
|
||||
"""
|
||||
|
||||
profile = await models.Profile.get_or_none(user_id=after.id, server_id=None)
|
||||
|
||||
is_updated = False
|
||||
|
||||
if profile is None:
|
||||
return
|
||||
|
||||
if profile.avatar_url != after.display_avatar.url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(after.display_avatar.url)
|
||||
|
||||
if response.status_code not in range(200, 300):
|
||||
return
|
||||
|
||||
f = io.BytesIO(response.content)
|
||||
|
||||
(r, g, b) = colorthief.ColorThief(f).get_color(quality=1)
|
||||
|
||||
profile.color = (r << 16) | (g << 8) | b
|
||||
|
||||
is_updated = True
|
||||
|
||||
if profile.name != after.display_name:
|
||||
profile.name = after.display_name
|
||||
is_updated = True
|
||||
|
||||
if is_updated:
|
||||
await profile.save()
|
||||
|
||||
@commands.Cog.listener()
|
||||
@transactions.atomic()
|
||||
async def on_member_update(
|
||||
self, _before: discord.Member, after: discord.Member
|
||||
) -> None:
|
||||
"""Updates a member's profile.
|
||||
|
||||
Args:
|
||||
_before (discord.Member): The member's profile before the update.
|
||||
after (discord.Member): The member's profile after the update.
|
||||
"""
|
||||
|
||||
profile = await models.Profile.get_or_none(
|
||||
user_id=after.id, server_id=after.guild.id
|
||||
)
|
||||
|
||||
is_updated = False
|
||||
|
||||
if profile is None:
|
||||
return
|
||||
|
||||
if profile.avatar_url != after.display_avatar.url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(after.display_avatar.url)
|
||||
|
||||
if response.status_code not in range(200, 300):
|
||||
return
|
||||
|
||||
f = io.BytesIO(response.content)
|
||||
|
||||
(r, g, b) = colorthief.ColorThief(f).get_color(quality=1)
|
||||
|
||||
profile.color = (r << 16) | (g << 8) | b
|
||||
|
||||
is_updated = True
|
||||
|
||||
if profile.name != after.display_name:
|
||||
profile.name = after.display_name
|
||||
is_updated = True
|
||||
|
||||
if is_updated:
|
||||
await profile.save()
|
||||
|
||||
@commands.command()
|
||||
@transactions.atomic()
|
||||
async def mask(self, ctx: commands.Context, toggle: bool | None = None) -> None:
|
||||
"""
|
||||
Hide your identity during calls with a mask.
|
||||
|
||||
Options:
|
||||
- `[<toggle>]`: On or off. If not set, your current status and mask will be displayed.
|
||||
"""
|
||||
|
||||
user, _ = await utils.get_user(ctx.author)
|
||||
|
||||
channel_id = ctx.channel.id
|
||||
|
||||
if toggle is None:
|
||||
if user.is_masked:
|
||||
channel = await models.Channel.get_or_none(
|
||||
id=channel_id
|
||||
).select_related("call")
|
||||
call_rs = (
|
||||
await models.CallUserChannelRelationship.get_or_none(
|
||||
channel=channel, user=user, call=channel.call
|
||||
)
|
||||
if channel is not None
|
||||
else None
|
||||
)
|
||||
|
||||
message = "🕵️ You're masked!"
|
||||
|
||||
if call_rs is not None:
|
||||
message += f" You're currently being displayed as **{call_rs.mask_emoji} {call_rs.display_name}**."
|
||||
|
||||
else:
|
||||
message += " Your identity will be hidden during calls."
|
||||
|
||||
message += f" Use `{ctx.clean_prefix}mask off` to turn it off."
|
||||
|
||||
await ctx.send(message)
|
||||
|
||||
else:
|
||||
await ctx.send(
|
||||
f"🕵️ You're not masked! Use `{ctx.clean_prefix}mask on` to mask yourself."
|
||||
)
|
||||
|
||||
else:
|
||||
user.is_masked = toggle
|
||||
await user.save()
|
||||
|
||||
past_user_call_rss = (
|
||||
await models.CallUserChannelRelationship.filter(user=user)
|
||||
.order_by("-id")
|
||||
.limit(5)
|
||||
.select_related("call")
|
||||
)
|
||||
|
||||
def update(
|
||||
rs: models.CallUserChannelRelationship,
|
||||
) -> models.CallUserChannelRelationship:
|
||||
rs.is_masked = user.is_masked
|
||||
return rs
|
||||
|
||||
ongoing_call_user_rss = list(
|
||||
map(
|
||||
update,
|
||||
filter(lambda rs: rs.call.ended_at is None, past_user_call_rss),
|
||||
)
|
||||
)
|
||||
|
||||
if len(ongoing_call_user_rss) > 0:
|
||||
await models.CallUserChannelRelationship.bulk_update(
|
||||
ongoing_call_user_rss, ["is_masked"]
|
||||
)
|
||||
|
||||
message = (
|
||||
"🕵️ From now on your profile will be "
|
||||
+ ("hidden" if toggle else "displayed")
|
||||
+ "."
|
||||
)
|
||||
|
||||
current_channel_ongoing_call_user_rs = find(
|
||||
lambda rs: rs.channel_id == channel_id, ongoing_call_user_rss
|
||||
)
|
||||
|
||||
if current_channel_ongoing_call_user_rs and toggle:
|
||||
message += f" Your profile will be displayed as **{current_channel_ongoing_call_user_rs.mask_emoji} {current_channel_ongoing_call_user_rs.display_name}**."
|
||||
|
||||
message += (
|
||||
f" Use `{ctx.clean_prefix}mask` during a call to see your mask."
|
||||
)
|
||||
|
||||
else:
|
||||
if toggle:
|
||||
message += f" Use `{ctx.clean_prefix}mask off` to turn it off."
|
||||
|
||||
else:
|
||||
message += f" Use `{ctx.clean_prefix}mask on` to turn it on."
|
||||
|
||||
await ctx.send(message)
|
||||
|
||||
@commands.command()
|
||||
@transactions.atomic()
|
||||
async def history(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
See your calls history. Links will take you to the beginning of the call.
|
||||
"""
|
||||
|
||||
user_id = ctx.author.id
|
||||
|
||||
call_rss = (
|
||||
await models.CallUserChannelRelationship.filter(user_id=user_id)
|
||||
.order_by("-id")
|
||||
.select_related("profile")
|
||||
.limit(30)
|
||||
)
|
||||
|
||||
if len(call_rss) == 0:
|
||||
await ctx.send(
|
||||
f"☎️ You haven't participated in a call yet! Use `{ctx.clean_prefix}call` to start one."
|
||||
)
|
||||
return
|
||||
|
||||
call_ids = [rs.call_id for rs in call_rss]
|
||||
|
||||
profile = call_rss[0].profile
|
||||
|
||||
call_rss = (
|
||||
await models.CallUserChannelRelationship.filter(call_id__in=call_ids)
|
||||
.select_related("profile")
|
||||
.order_by("-id")
|
||||
.all()
|
||||
)
|
||||
calls_rss_map = {}
|
||||
|
||||
for call_rs in call_rss:
|
||||
if call_rs.call_id not in calls_rss_map:
|
||||
calls_rss_map[call_rs.call_id] = {
|
||||
"user_rs": None,
|
||||
"others_rs": [],
|
||||
"first_message": None,
|
||||
}
|
||||
|
||||
if call_rs.user_id == user_id:
|
||||
calls_rss_map[call_rs.call_id]["user_rs"] = call_rs
|
||||
|
||||
else:
|
||||
calls_rss_map[call_rs.call_id]["others_rs"].append(call_rs)
|
||||
|
||||
min_ids = await (
|
||||
models.Message.filter(call_id__in=call_ids)
|
||||
.exclude(propagation_id=None)
|
||||
.annotate(min_id=functions.Min("id"))
|
||||
.group_by("call_id")
|
||||
.values_list("min_id", flat=True)
|
||||
)
|
||||
|
||||
# Step 2: Fetch the actual messages using these IDs
|
||||
first_messages = (
|
||||
await models.Message.filter(id__in=min_ids)
|
||||
.select_related("propagation_channel", "channel")
|
||||
.all()
|
||||
)
|
||||
|
||||
for msg in first_messages:
|
||||
calls_rss_map[msg.call_id]["first_message"] = msg
|
||||
|
||||
embeds = []
|
||||
|
||||
logs = ""
|
||||
logs_count = 0
|
||||
|
||||
for call_id, data in calls_rss_map.items():
|
||||
logs += f"- `ID: {call_id}`: "
|
||||
|
||||
participant_names = ", ".join(
|
||||
(
|
||||
call_rs.display_name
|
||||
if call_rs.channel_id != data["user_rs"].channel_id
|
||||
else call_rs.profile.name
|
||||
)
|
||||
for call_rs in data["others_rs"] + [data["user_rs"]]
|
||||
)
|
||||
|
||||
link = None
|
||||
|
||||
first_message: models.Message | None = data["first_message"]
|
||||
|
||||
if first_message is not None:
|
||||
if first_message.channel_id == data["user_rs"].channel_id:
|
||||
link = f"https://discord.com/channels/{first_message.channel.server_id}/{first_message.channel_id}/{first_message.id}"
|
||||
|
||||
else:
|
||||
link = f"https://discord.com/channels/{first_message.propagation_channel.server_id}/{first_message.propagation_channel.id}/{first_message.propagation_id}"
|
||||
|
||||
if link is not None:
|
||||
logs += f"[{participant_names}]({link})"
|
||||
|
||||
else:
|
||||
logs += participant_names
|
||||
|
||||
logs += "\n"
|
||||
logs_count += 1
|
||||
|
||||
if logs.count("\n") == 10:
|
||||
embeds.append(
|
||||
discord.Embed(
|
||||
title="Call History",
|
||||
description="Last calls:\n" + logs,
|
||||
thumbnail=profile.avatar_url,
|
||||
author=discord.EmbedAuthor(
|
||||
name=profile.name, icon_url=profile.avatar_url
|
||||
),
|
||||
footer=discord.EmbedFooter(text=f"Page {len(embeds) + 1} of 3"),
|
||||
)
|
||||
)
|
||||
|
||||
logs = ""
|
||||
|
||||
if logs != "":
|
||||
embeds.append(
|
||||
discord.Embed(
|
||||
color=profile.color,
|
||||
title="Call History",
|
||||
description="Last calls:\n" + logs,
|
||||
thumbnail=profile.avatar_url,
|
||||
author=discord.EmbedAuthor(
|
||||
name=profile.name, icon_url=profile.avatar_url
|
||||
),
|
||||
footer=discord.EmbedFooter(text=f"Page {len(embeds) + 1} of 3"),
|
||||
)
|
||||
)
|
||||
|
||||
paginator = pages.Paginator(
|
||||
embeds,
|
||||
show_indicator=False,
|
||||
use_default_buttons=False,
|
||||
custom_buttons=[
|
||||
pages.PaginatorButton(
|
||||
button_type="prev", label="⬅️", style=discord.ButtonStyle.blurple
|
||||
),
|
||||
pages.PaginatorButton(
|
||||
button_type="next", label="➡️", style=discord.ButtonStyle.blurple
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
await paginator.send(ctx)
|
||||
|
||||
@commands.command()
|
||||
@transactions.atomic()
|
||||
async def me(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
Shows your user statistics.
|
||||
"""
|
||||
|
||||
user, profile = await utils.get_user(ctx.author)
|
||||
|
||||
messages_count = await models.Message.filter(user=user).count()
|
||||
|
||||
channel_ids = (
|
||||
await models.CallUserChannelRelationship.filter(user=user)
|
||||
.distinct()
|
||||
.values_list("channel_id", flat=True)
|
||||
)
|
||||
call_ids = (
|
||||
await models.CallUserChannelRelationship.filter(user=user)
|
||||
.distinct()
|
||||
.values_list("call_id", flat=True)
|
||||
)
|
||||
|
||||
people_met_count = await models.CallUserChannelRelationship.filter(
|
||||
call_id__in=call_ids, channel_id__not_in=channel_ids
|
||||
).count()
|
||||
ban = (
|
||||
await models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
expires_at=None,
|
||||
join_type="OR",
|
||||
),
|
||||
user_id=user.id,
|
||||
)
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
|
||||
reviewed_reports = (
|
||||
(await models.Report.filter(reviewed_by_id=user.id).count())
|
||||
if user.is_moderator
|
||||
else None
|
||||
)
|
||||
bans_made = (
|
||||
(await models.Ban.filter(banned_by_id=user.id).count())
|
||||
if user.is_moderator
|
||||
else None
|
||||
)
|
||||
bans_unmade = (
|
||||
(await models.Ban.filter(unbanned_by_id=user.id).count())
|
||||
if user.is_moderator
|
||||
else None
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=profile.color or constants.EMBED_COLOR,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Messages", value=str(messages_count), inline=True
|
||||
),
|
||||
discord.EmbedField(name="Calls", value=len(call_ids), inline=True),
|
||||
discord.EmbedField(
|
||||
name="People Met", value=people_met_count, inline=True
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Joined At",
|
||||
value=f"<t:{int(user.joined_at.timestamp())}:d> <t:{int(user.joined_at.timestamp())}:T>",
|
||||
inline=True,
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Leaderboard",
|
||||
value=(
|
||||
f"#{user.leaderboard_position} (Monthly)"
|
||||
if user.leaderboard_position is not None
|
||||
else "*No calls yet.*"
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Banned",
|
||||
value=(
|
||||
"*Not banned.*"
|
||||
if ban is None
|
||||
else (
|
||||
"*Permanent.*"
|
||||
if ban.expires_at is None
|
||||
else f"Until <t:{int(ban.expires_at.timestamp())}:F> (<t:{int(ban.expires_at.timestamp())}:R>)"
|
||||
)
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
]
|
||||
+ (
|
||||
[
|
||||
discord.EmbedField(
|
||||
name="Reviewed Reports", value=reviewed_reports, inline=True
|
||||
),
|
||||
discord.EmbedField(name="Bans Made", value=bans_made, inline=True),
|
||||
discord.EmbedField(
|
||||
name="Bans Unmade", value=bans_unmade, inline=True
|
||||
),
|
||||
]
|
||||
if user.is_moderator
|
||||
else []
|
||||
),
|
||||
author=discord.EmbedAuthor(name=profile.name, icon_url=profile.avatar_url),
|
||||
footer=discord.EmbedFooter(
|
||||
text="Thank you for using Payphone! Leaderboard updates every 15 minutes."
|
||||
),
|
||||
thumbnail=profile.avatar_url,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed, view=views.MeView(user))
|
||||
|
||||
@commands.command(aliases=["l", "leader", "ranking"])
|
||||
async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None:
|
||||
"""
|
||||
Shows the leaderboard for the month.
|
||||
|
||||
Options:
|
||||
- `[<board>]`: The board to show (users/servers). Defaults to users.
|
||||
"""
|
||||
|
||||
users_board_options = ["u", "user", "users"]
|
||||
server_board_options = ["s", "server", "servers", "g", "guild", "guilds"]
|
||||
|
||||
board = board.lower()
|
||||
|
||||
if board not in users_board_options + server_board_options:
|
||||
await ctx.send(
|
||||
f"❌ This leaderboard does not exist. The leaderboards are `servers` and `users`, for example `{ctx.clean_prefix}leaderboard users`."
|
||||
)
|
||||
return
|
||||
|
||||
table = ""
|
||||
table_name = "users" if board in users_board_options else "servers"
|
||||
|
||||
if board in users_board_options:
|
||||
top_users = (
|
||||
await models.User.filter(leaderboard_position__range=[1, 5])
|
||||
.prefetch_related("profiles")
|
||||
.order_by("leaderboard_position")
|
||||
.limit(5)
|
||||
)
|
||||
user_rows = [
|
||||
f"**#{pos + 1} - {user.profiles[0].name}**{" (You!)" if user.id == ctx.author.id else ""}\nParticipated in {user.leaderboard_calls_count} calls."
|
||||
for pos, user in enumerate(top_users)
|
||||
]
|
||||
|
||||
table = "__**Top Users:**__\n\n" + "\n\n".join(user_rows)
|
||||
|
||||
else:
|
||||
top_servers = (
|
||||
await models.Server.filter(leaderboard_position__range=[1, 5])
|
||||
.order_by("leaderboard_position")
|
||||
.limit(5)
|
||||
)
|
||||
server_rows = [
|
||||
f"**#{pos + 1} - {server.name or "Updating..."}{f" · .gg/{server.vanity_code}" if server.vanity_code else ""}**{" (Here!)" if ctx.guild is not None and server.id == ctx.guild.id else ""}\nParticipated in {server.leaderboard_calls_count} calls."
|
||||
for pos, server in enumerate(top_servers)
|
||||
]
|
||||
|
||||
table = "__**Top Servers:**__\n\n" + "\n\n".join(server_rows)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=constants.EMBED_COLOR,
|
||||
title=f"Monthly Leaderboard ({datetime.now(timezone.utc).strftime("%B")})",
|
||||
description=(
|
||||
f"Based on the amount of calls {table_name} have participated in with more than 25 "
|
||||
"messages sent (to prevent spam).\n\n"
|
||||
)
|
||||
+ table,
|
||||
author=self.bot.embed_author,
|
||||
footer=discord.EmbedFooter(
|
||||
text="The leaderboard is updated every 15 minutes."
|
||||
),
|
||||
thumbnail=self.bot.user.display_avatar.url,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
|
||||
Args:
|
||||
b (bot.Payphone): The bot instance to load the cog into.
|
||||
"""
|
||||
|
||||
b.add_cog(Users(b))
|
||||
@@ -1,69 +0,0 @@
|
||||
import discord
|
||||
|
||||
from payphone.db import models
|
||||
|
||||
|
||||
class MeView(discord.ui.View):
|
||||
status: int = 0
|
||||
|
||||
def __init__(self, user: models.User) -> None:
|
||||
"""Initializes the view.
|
||||
|
||||
Args:
|
||||
user (models.user): The user to show the view for.
|
||||
"""
|
||||
|
||||
super().__init__(disable_on_timeout=True)
|
||||
|
||||
async def callback(interaction: discord.Interaction) -> None:
|
||||
"""Handles the interaction of the button."""
|
||||
|
||||
if interaction.user.id != user.id:
|
||||
await interaction.response.send_message(
|
||||
"❌ You cannot delete other person's account.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
if self.status == 0:
|
||||
self.children[0].label = "Confirm?"
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
self.status += 1
|
||||
return
|
||||
|
||||
user.is_deleting = True
|
||||
await user.save()
|
||||
|
||||
self.disable_all_items()
|
||||
self.children[0].label = "Your account will be deleted"
|
||||
await interaction.response.send_message(
|
||||
(
|
||||
"👋 Your account was scheduled for deletion. It may take up to 1 hour for "
|
||||
"your account information to be deleted.\n\n"
|
||||
"What will be deleted:\n"
|
||||
"- Your messages\n"
|
||||
"- Your calls\n"
|
||||
"- Your profiles and user information\n"
|
||||
"- Your stats\n"
|
||||
"- Your reports\n\n"
|
||||
"What won't be deleted:\n"
|
||||
"- Your bans and ban history"
|
||||
),
|
||||
ephemeral=True,
|
||||
)
|
||||
await interaction.message.edit(view=self)
|
||||
|
||||
if not user.is_deleting:
|
||||
button = discord.ui.Button(
|
||||
style=discord.ButtonStyle.danger, label="Delete My Activity Data"
|
||||
)
|
||||
button.callback = callback
|
||||
|
||||
else:
|
||||
button = discord.ui.Button(
|
||||
style=discord.ButtonStyle.danger,
|
||||
label="Your account is being deleted",
|
||||
disabled=True,
|
||||
)
|
||||
|
||||
self.add_item(button)
|
||||
@@ -1,49 +0,0 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
from payphone import bot
|
||||
from payphone.cogs import base
|
||||
from payphone.core import config, constants, checks
|
||||
|
||||
|
||||
class Utility(base.Cog):
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 60, commands.BucketType.user)
|
||||
@checks.is_unbanned()
|
||||
async def suggest(self, ctx: commands.Context, *, suggestion: str) -> None:
|
||||
"""
|
||||
Make a suggestion to improve Payphone.
|
||||
"""
|
||||
|
||||
embed = discord.Embed(
|
||||
color=constants.EMBED_COLOR,
|
||||
title="Suggestion",
|
||||
description=suggestion,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
author=discord.EmbedAuthor(
|
||||
name=ctx.author.display_name, icon_url=ctx.author.display_avatar.url
|
||||
),
|
||||
footer=discord.EmbedFooter(text=ctx.author.name),
|
||||
thumbnail=self.bot.user.display_avatar.url,
|
||||
)
|
||||
|
||||
await self.bot.get_partial_messageable(config.SUGGESTIONS_CHANNEL_ID).send(
|
||||
embed=embed
|
||||
)
|
||||
|
||||
await ctx.send("✅ Thank you for your suggestion!")
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
|
||||
Args:
|
||||
b (bot.Payphone): The bot instance to load the cog into.
|
||||
"""
|
||||
|
||||
b.add_cog(Utility(b))
|
||||
@@ -0,0 +1,494 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import orjson
|
||||
|
||||
from payphone.bot import Payphone
|
||||
|
||||
|
||||
@dataclass
|
||||
class Participant:
|
||||
"""A participant in Payphone."""
|
||||
|
||||
_bot: Payphone
|
||||
|
||||
id: int
|
||||
name: str
|
||||
avatar_url: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Call:
|
||||
"""A call in Payphone."""
|
||||
|
||||
_bot: Payphone
|
||||
|
||||
id: int
|
||||
channels: tuple[int, int]
|
||||
|
||||
async def set_message(
|
||||
self, id: int, channel_id: int, propagation_id: int, author_id: int
|
||||
) -> None:
|
||||
"""Sets a message as part of a call.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the message to set.
|
||||
channel_id (int): The ID of the channel to set the message in.
|
||||
propagation_id (int): The ID of the propagation message.
|
||||
author_id (int): The ID of the author of the message.
|
||||
"""
|
||||
|
||||
pipe = self._bot.redis.pipeline()
|
||||
|
||||
pipe.hset("calls:messages", id, self.id)
|
||||
pipe.hset("calls:messages:channels", id, channel_id)
|
||||
pipe.hset("calls:messages:propagations", id, propagation_id)
|
||||
pipe.hset("calls:messages:reversed", propagation_id, id)
|
||||
pipe.hset("calls:messages:authors", id, author_id)
|
||||
pipe.hset("calls:last-message", self.id, "")
|
||||
pipe.hexpire("calls:messages", 15 * 60, id)
|
||||
pipe.hexpire("calls:messages:channels", 15 * 60, id)
|
||||
pipe.hexpire("calls:messages:propagations", 15 * 60, id)
|
||||
pipe.hexpire("calls:messages:reversed", 15 * 60, propagation_id)
|
||||
pipe.hexpire("calls:messages:authors", 15 * 60, id)
|
||||
pipe.hexpire("calls:last-message", 15 * 60, self.id)
|
||||
|
||||
await pipe.execute()
|
||||
|
||||
async def is_message_from_call(self, id: int) -> bool:
|
||||
"""Checks if a message is from a call.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the message to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the message is from the call, False otherwise.
|
||||
"""
|
||||
|
||||
return bool(await self._bot.redis.hexists("calls:messages", id)) or bool(
|
||||
await self._bot.redis.hexists("calls:messages:reversed", id)
|
||||
)
|
||||
|
||||
async def get_message_propagation(self, id: int) -> tuple[int, int] | None:
|
||||
"""Gets a message as part of a call.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the message to get.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: The channel and propagation ID of the message.
|
||||
"""
|
||||
|
||||
pipe = self._bot.redis.pipeline()
|
||||
|
||||
pipe.hget("calls:messages:channels", id)
|
||||
pipe.hget("calls:messages:propagations", id)
|
||||
|
||||
channel_id, propagation_id = await pipe.execute()
|
||||
|
||||
if channel_id is None or propagation_id is None:
|
||||
return
|
||||
|
||||
propagation_channel_id = (
|
||||
self.channels[0]
|
||||
if int(channel_id) != self.channels[0]
|
||||
else self.channels[1]
|
||||
)
|
||||
|
||||
return int(propagation_channel_id), int(propagation_id)
|
||||
|
||||
async def set_participant(
|
||||
self, user_id: int, user_name: str, user_avatar_url: str
|
||||
) -> Participant:
|
||||
"""Sets a participant in a call.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user to set.
|
||||
user_name (str): The name of the user to set.
|
||||
user_avatar_url (str): The avatar URL of the user to set.
|
||||
|
||||
Returns:
|
||||
Participant: The participant that was set.
|
||||
"""
|
||||
|
||||
data = orjson.dumps({"name": user_name, "avatar_url": user_avatar_url})
|
||||
|
||||
await self._bot.redis.hset(f"calls:{self.id}:participants", user_id, data)
|
||||
await self._bot.redis.hexpire(f"calls:{self.id}:participants", 3600, user_id)
|
||||
|
||||
return Participant(
|
||||
_bot=self._bot, id=user_id, name=user_name, avatar_url=user_avatar_url
|
||||
)
|
||||
|
||||
async def get_participant(self, user_id: int) -> Participant | None:
|
||||
"""Gets a participant in a call.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user to get.
|
||||
|
||||
Returns:
|
||||
Participant | None: The participant that was set.
|
||||
"""
|
||||
|
||||
data = await self._bot.redis.hget(f"calls:{self.id}:participants", user_id)
|
||||
|
||||
if data is None:
|
||||
return
|
||||
|
||||
data = orjson.loads(data)
|
||||
|
||||
return Participant(
|
||||
_bot=self._bot, id=user_id, name=data["name"], avatar_url=data["avatar_url"]
|
||||
)
|
||||
|
||||
async def get_participants(self) -> list["Participant"]:
|
||||
"""Gets all participants in a call.
|
||||
|
||||
Returns:
|
||||
list[Participant]: The participants in the call.
|
||||
"""
|
||||
|
||||
participants = []
|
||||
|
||||
data = await self._bot.redis.hgetall(f"calls:{self.id}:participants")
|
||||
|
||||
for user_id, data in data.items():
|
||||
data = orjson.loads(data)
|
||||
|
||||
participants.append(
|
||||
Participant(
|
||||
_bot=self._bot,
|
||||
id=int(user_id),
|
||||
name=data["name"],
|
||||
avatar_url=data["avatar_url"],
|
||||
)
|
||||
)
|
||||
|
||||
return participants
|
||||
|
||||
|
||||
class CallHandler:
|
||||
"""Handles calls in Payphone."""
|
||||
|
||||
def __init__(self, bot: Payphone) -> None:
|
||||
"""Initializes the CallHandler.
|
||||
|
||||
Args:
|
||||
bot (Payphone): The bot instance.
|
||||
"""
|
||||
|
||||
self.bot = bot
|
||||
|
||||
async def set_searching(self, channel_id: int, status: bool) -> None:
|
||||
"""Sets the channel as searching for a call.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to set as searching.
|
||||
status (bool): The status to set the channel to.
|
||||
"""
|
||||
|
||||
if status:
|
||||
await self.bot.redis.sadd("calls:channels:searching", channel_id)
|
||||
|
||||
else:
|
||||
await self.bot.redis.srem("calls:channels:searching", channel_id)
|
||||
|
||||
async def connect(self) -> list[tuple[int, int]]:
|
||||
"""Connects two channels together.
|
||||
|
||||
Returns:
|
||||
list[tuple[int, int]]: A list of channel pairs.
|
||||
"""
|
||||
|
||||
pairs = []
|
||||
|
||||
while True:
|
||||
channels = await self.bot.redis.spop("calls:channels:searching", 2)
|
||||
|
||||
if len(channels) < 2:
|
||||
if len(channels) == 1:
|
||||
await self.set_searching(channels[0], True)
|
||||
|
||||
break
|
||||
|
||||
last_calls = await self.bot.redis.hmget("last-calls", *channels)
|
||||
|
||||
break_outer = False
|
||||
|
||||
for channel in channels:
|
||||
if channel in last_calls:
|
||||
next_channel = await self.bot.redis.spop("calls:channels:searching")
|
||||
|
||||
if next_channel is not None:
|
||||
other_channel_id = channels[abs(channels.index(channel) - 1)]
|
||||
await self.set_searching(other_channel_id, True)
|
||||
|
||||
channels[channels.index(channel)] = next_channel
|
||||
|
||||
else:
|
||||
for channel in channels:
|
||||
await self.set_searching(channel, True)
|
||||
|
||||
break_outer = True
|
||||
break
|
||||
|
||||
if break_outer:
|
||||
break
|
||||
|
||||
pairs.append(tuple(channels))
|
||||
|
||||
mapping = {}
|
||||
reverse_mapping = {}
|
||||
|
||||
for pair in pairs:
|
||||
last_call_id = await self.bot.redis.incr("calls:calls:count")
|
||||
|
||||
mapping[pair[0]] = last_call_id
|
||||
mapping[pair[1]] = last_call_id
|
||||
|
||||
reverse_mapping[last_call_id] = f"{pair[0]}:{pair[1]}"
|
||||
|
||||
pipe = self.bot.redis.pipeline(transaction=True)
|
||||
|
||||
if mapping:
|
||||
pipe.hset("calls:channels:active", mapping=mapping)
|
||||
pipe.hset("calls:calls:active", mapping=reverse_mapping)
|
||||
|
||||
for pair in pairs:
|
||||
pipe.hmset("last-calls", {pair[0]: pair[1], pair[1]: pair[0]})
|
||||
pipe.hset("calls:last-message", mapping[pair[0]], "")
|
||||
pipe.hexpire("calls:last-message", 60, mapping[pair[0]])
|
||||
|
||||
await pipe.execute()
|
||||
|
||||
return pairs
|
||||
|
||||
async def expire(self) -> list[tuple[int, int]]:
|
||||
"""
|
||||
Ends all calls that have been inactive for more than 15 seconds.
|
||||
"""
|
||||
|
||||
active_calls = await self.bot.redis.hgetall("calls:calls:active")
|
||||
|
||||
pairs = []
|
||||
|
||||
pipe = self.bot.redis.pipeline(transaction=True)
|
||||
|
||||
for call_id, channels in active_calls.items():
|
||||
channels = [int(c) for c in channels.split(":")]
|
||||
|
||||
last_message = await self.bot.redis.hget("calls:last-message", call_id)
|
||||
|
||||
if last_message is not None:
|
||||
continue
|
||||
|
||||
pipe.hdel("calls:channels:active", *channels)
|
||||
pipe.hdel("calls:calls:active", call_id)
|
||||
pipe.srem("calls:channels:searching", *channels)
|
||||
|
||||
pairs.append(tuple(channels))
|
||||
|
||||
await pipe.execute()
|
||||
|
||||
return pairs
|
||||
|
||||
async def is_searching(self, channel_id: int) -> bool:
|
||||
"""Checks if a channel is searching for a call.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the channel is searching for a call, False otherwise.
|
||||
"""
|
||||
|
||||
return bool(
|
||||
await self.bot.redis.sismember("calls:channels:searching", channel_id)
|
||||
)
|
||||
|
||||
async def is_on_call(self, channel_id: int) -> bool:
|
||||
"""Checks if a channel is on a call.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the channel is on a call, False otherwise.
|
||||
"""
|
||||
|
||||
return await self.bot.redis.hexists("calls:channels:active", channel_id)
|
||||
|
||||
async def get_call(self, channel_id: int) -> Call | None:
|
||||
"""Gets the call that a channel is in.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to get the call for.
|
||||
|
||||
Returns:
|
||||
Call | None: The pair of channels that are connected.
|
||||
"""
|
||||
|
||||
call_id = await self.bot.redis.hget("calls:channels:active", channel_id)
|
||||
|
||||
if call_id is None:
|
||||
return
|
||||
|
||||
channels = [
|
||||
int(c)
|
||||
for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split(
|
||||
":"
|
||||
)
|
||||
]
|
||||
|
||||
return Call(_bot=self.bot, id=call_id, channels=channels)
|
||||
|
||||
async def hangup(self, channel_id: int) -> tuple[int, int] | tuple[int]:
|
||||
"""Ends a call in a channel.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to end the call in.
|
||||
|
||||
Returns:
|
||||
tuple[int, int] | tuple[int]: The pair of channels that were connected.
|
||||
"""
|
||||
|
||||
await self.set_searching(channel_id, False)
|
||||
|
||||
call_id = await self.bot.redis.hget("calls:channels:active", channel_id)
|
||||
|
||||
if call_id is None:
|
||||
# Channel is searching.
|
||||
await self.set_searching(channel_id, False)
|
||||
return (channel_id,)
|
||||
|
||||
channels = [
|
||||
int(c)
|
||||
for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split(
|
||||
":"
|
||||
)
|
||||
]
|
||||
|
||||
for channel in channels:
|
||||
await self.set_searching(channel, False)
|
||||
|
||||
await self.bot.redis.hdel("calls:channels:active", *channels)
|
||||
await self.bot.redis.hdel("calls:calls:active", call_id)
|
||||
|
||||
return tuple(channels)
|
||||
|
||||
async def skip(self, channel_id: int) -> tuple[int, int]:
|
||||
"""Skips a channel in the call queue.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to skip.
|
||||
"""
|
||||
|
||||
await self.set_searching(channel_id, True)
|
||||
|
||||
call_id = await self.bot.redis.hget("calls:channels:active", channel_id)
|
||||
|
||||
if call_id is None:
|
||||
return
|
||||
|
||||
channels = [
|
||||
int(c)
|
||||
for c in (await self.bot.redis.hget("calls:calls:active", call_id)).split(
|
||||
":"
|
||||
)
|
||||
]
|
||||
|
||||
for channel in channels:
|
||||
if channel != channel_id:
|
||||
await self.set_searching(channel, False)
|
||||
|
||||
await self.bot.redis.hdel("calls:channels:active", *channels)
|
||||
|
||||
return tuple(channels)
|
||||
|
||||
async def get_channel_webhook_url(self, channel_id: int) -> str:
|
||||
"""Gets the webhook for a channel.
|
||||
|
||||
Args:
|
||||
channel_id (int): The ID of the channel to get the webhook for.
|
||||
|
||||
Returns:
|
||||
str: The URL of the webhook.
|
||||
"""
|
||||
|
||||
cached_url = await self.bot.redis.hget("calls:channels:webhooks", channel_id)
|
||||
|
||||
if cached_url:
|
||||
return cached_url
|
||||
|
||||
channel = await self.bot.get_or_fetch_channel(channel_id)
|
||||
|
||||
if channel is None:
|
||||
return
|
||||
|
||||
for webhook in await channel.webhooks():
|
||||
if webhook.name == "Payphone" and webhook.token is not None:
|
||||
await self.bot.redis.hset(
|
||||
"calls:channels:webhooks", channel_id, webhook.url
|
||||
)
|
||||
return webhook.url
|
||||
|
||||
webhook = await channel.create_webhook(name="Payphone")
|
||||
|
||||
await self.bot.redis.hset("calls:channels:webhooks", channel_id, webhook.url)
|
||||
|
||||
return webhook.url
|
||||
|
||||
async def get_message(self, message_id: int) -> None:
|
||||
"""Gets a message as part of a call.
|
||||
|
||||
Args:
|
||||
message_id (int): The ID of the message to get.
|
||||
|
||||
Returns:
|
||||
None: The channel and propagation ID of the message.
|
||||
"""
|
||||
|
||||
return await self.bot.redis.hget("calls:messages", message_id)
|
||||
|
||||
async def get_message_author_id(self, id: int) -> int | None:
|
||||
"""Gets the author of a message as part of a call.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the message to get.
|
||||
|
||||
Returns:
|
||||
int: The author ID of the message.
|
||||
"""
|
||||
|
||||
pipe = self.bot.redis.pipeline()
|
||||
|
||||
pipe.hget("calls:messages", id)
|
||||
pipe.hget("calls:messages:reversed", id)
|
||||
|
||||
call_id, message_id = await pipe.execute()
|
||||
|
||||
if call_id is None and message_id is None:
|
||||
return
|
||||
|
||||
message_id = int(message_id) if message_id else id
|
||||
|
||||
author_id = await self.bot.redis.hget("calls:messages:authors", id)
|
||||
|
||||
return int(author_id)
|
||||
|
||||
async def get_original_message_id(self, id: int) -> int | None:
|
||||
"""
|
||||
Get the original message ID of a message in a call.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the message to get.
|
||||
|
||||
Returns:
|
||||
int: The original message ID of the message.
|
||||
"""
|
||||
|
||||
message_id = await self.bot.redis.hget("calls:messages:reversed", id)
|
||||
|
||||
if message_id:
|
||||
return int(message_id)
|
||||
|
||||
return None
|
||||
+7
-516
@@ -1,532 +1,23 @@
|
||||
import re
|
||||
|
||||
|
||||
def create_pattern(text: str) -> str:
|
||||
"""Creates regex patterns for words or phrases with possible variations
|
||||
words = []
|
||||
|
||||
Args:
|
||||
text (str): The word or phrase
|
||||
with open("censored.txt", encoding="UTF-8") as f:
|
||||
for line in f:
|
||||
words.append(line.strip())
|
||||
|
||||
Returns:
|
||||
str: The created pattern.
|
||||
"""
|
||||
|
||||
replacements = [
|
||||
("a", "a|@|4"),
|
||||
("c", "c|ç"),
|
||||
("e", "e|3"),
|
||||
("h", "h|#"),
|
||||
("i", "i|1|!"),
|
||||
("o", "o|0"),
|
||||
("t", "t|7"),
|
||||
("u", "u|v|ü"),
|
||||
("v", "u|v|ü"),
|
||||
("x", "x|><"),
|
||||
]
|
||||
|
||||
text = "+".join(list(text)) + "+"
|
||||
|
||||
for r in replacements:
|
||||
text = text.replace(r[0], f"({r[1]})")
|
||||
|
||||
return f"(?:{text})"
|
||||
|
||||
|
||||
BLOCKED_WORDS = [
|
||||
create_pattern(word)
|
||||
for word in [
|
||||
"2g1c",
|
||||
"2 girls 1 cup",
|
||||
"acrotomophilia",
|
||||
"alabama hot pocket",
|
||||
"alaskan pipeline",
|
||||
"anal",
|
||||
"anilingus",
|
||||
"anus",
|
||||
"apeshit",
|
||||
"arsehole",
|
||||
# "ass",
|
||||
"asshole",
|
||||
"assmunch",
|
||||
"auto erotic",
|
||||
"autoerotic",
|
||||
"babeland",
|
||||
"baby batter",
|
||||
"baby juice",
|
||||
"ball gag",
|
||||
"ball gravy",
|
||||
"ball kicking",
|
||||
"ball licking",
|
||||
"ball sack",
|
||||
"ball sucking",
|
||||
"bangbros",
|
||||
"bangbus",
|
||||
"bareback",
|
||||
"barely legal",
|
||||
"barenaked",
|
||||
"bastard",
|
||||
"bastardo",
|
||||
"bastinado",
|
||||
"bbw",
|
||||
"bdsm",
|
||||
"beaner",
|
||||
"beaners",
|
||||
"beaver cleaver",
|
||||
"beaver lips",
|
||||
"beastiality",
|
||||
"bestiality",
|
||||
"big black",
|
||||
"big breasts",
|
||||
"big knockers",
|
||||
"big tits",
|
||||
"bimbos",
|
||||
"birdlock",
|
||||
"bitch",
|
||||
"bitches",
|
||||
"black cock",
|
||||
"blonde action",
|
||||
"blonde on blonde action",
|
||||
"blowjob",
|
||||
"blow job",
|
||||
"blow your load",
|
||||
"blue waffle",
|
||||
"blumpkin",
|
||||
"bollocks",
|
||||
"bondage",
|
||||
"boner",
|
||||
"boob",
|
||||
"boobs",
|
||||
"booty call",
|
||||
"brown showers",
|
||||
"brunette action",
|
||||
"bukkake",
|
||||
"bulldyke",
|
||||
"bullet vibe",
|
||||
"bullshit",
|
||||
"bung hole",
|
||||
"bunghole",
|
||||
"busty",
|
||||
"butt",
|
||||
"buttcheeks",
|
||||
"butthole",
|
||||
"camel toe",
|
||||
"camgirl",
|
||||
"camslut",
|
||||
"camwhore",
|
||||
"carpet muncher",
|
||||
"carpetmuncher",
|
||||
"chocolate rosebuds",
|
||||
"cialis",
|
||||
"circlejerk",
|
||||
"cleveland steamer",
|
||||
"clit",
|
||||
"clitoris",
|
||||
"clover clamps",
|
||||
"clusterfuck",
|
||||
"cock",
|
||||
"cocks",
|
||||
"coprolagnia",
|
||||
"coprophilia",
|
||||
"cornhole",
|
||||
"coon",
|
||||
"coons",
|
||||
"creampie",
|
||||
"cum",
|
||||
"cumming",
|
||||
"cumshot",
|
||||
"cumshots",
|
||||
"cunnilingus",
|
||||
"cunt",
|
||||
"darkie",
|
||||
"date rape",
|
||||
"daterape",
|
||||
"deep throat",
|
||||
"deepthroat",
|
||||
"dendrophilia",
|
||||
"dick",
|
||||
"dildo",
|
||||
"dingleberry",
|
||||
"dingleberries",
|
||||
"dirty pillows",
|
||||
"dirty sanchez",
|
||||
"doggie style",
|
||||
"doggiestyle",
|
||||
"doggy style",
|
||||
"doggystyle",
|
||||
"dog style",
|
||||
"dolcett",
|
||||
"domination",
|
||||
"dominatrix",
|
||||
"dommes",
|
||||
"donkey punch",
|
||||
"double dong",
|
||||
"double penetration",
|
||||
"dp action",
|
||||
"dry hump",
|
||||
"dvda",
|
||||
"eat my ass",
|
||||
"ecchi",
|
||||
"ejaculation",
|
||||
"erotic",
|
||||
"erotism",
|
||||
"escort",
|
||||
"eunuch",
|
||||
"fag",
|
||||
"faggot",
|
||||
"fecal",
|
||||
"felch",
|
||||
"fellatio",
|
||||
"feltch",
|
||||
"female squirting",
|
||||
"femdom",
|
||||
"figging",
|
||||
"fingerbang",
|
||||
"fingering",
|
||||
"fisting",
|
||||
"foot fetish",
|
||||
"footjob",
|
||||
"frotting",
|
||||
"fuck buttons",
|
||||
# "fuckin",
|
||||
# "fucking",
|
||||
"fucktards",
|
||||
"fudge packer",
|
||||
"fudgepacker",
|
||||
"futanari",
|
||||
"gangbang",
|
||||
"gang bang",
|
||||
"gay sex",
|
||||
"genitals",
|
||||
"giant cock",
|
||||
"girl on",
|
||||
"girl on top",
|
||||
"girls gone wild",
|
||||
"goatcx",
|
||||
"goatse",
|
||||
"god damn",
|
||||
"gokkun",
|
||||
"golden shower",
|
||||
"goodpoop",
|
||||
"goo girl",
|
||||
"goregasm",
|
||||
"grope",
|
||||
"group sex",
|
||||
"g-spot",
|
||||
"guro",
|
||||
"hand job",
|
||||
"handjob",
|
||||
"hard core",
|
||||
"hardcore",
|
||||
"hentai",
|
||||
"homoerotic",
|
||||
"honkey",
|
||||
"hooker",
|
||||
"horny",
|
||||
"hot carl",
|
||||
"hot chick",
|
||||
"how to kill",
|
||||
"how to murder",
|
||||
"huge fat",
|
||||
"humping",
|
||||
"incest",
|
||||
"intercourse",
|
||||
"jack off",
|
||||
"jail bait",
|
||||
"jailbait",
|
||||
"jelly donut",
|
||||
"jerk off",
|
||||
"jigaboo",
|
||||
"jiggaboo",
|
||||
"jiggerboo",
|
||||
"jizz",
|
||||
"juggs",
|
||||
"kike",
|
||||
"kinbaku",
|
||||
"kinkster",
|
||||
"kinky",
|
||||
"knobbing",
|
||||
"leather restraint",
|
||||
"leather straight jacket",
|
||||
"lemon party",
|
||||
"livesex",
|
||||
"lolita",
|
||||
"lovemaking",
|
||||
"make me come",
|
||||
"male squirting",
|
||||
"masturbate",
|
||||
"masturbating",
|
||||
"masturbation",
|
||||
"menage a trois",
|
||||
"milf",
|
||||
"missionary position",
|
||||
"mong",
|
||||
"motherfucker",
|
||||
"mound of venus",
|
||||
"mr hands",
|
||||
"muff diver",
|
||||
"muffdiving",
|
||||
"nambla",
|
||||
"nawashi",
|
||||
"negro",
|
||||
"neonazi",
|
||||
"nigga",
|
||||
"nigger",
|
||||
"nig nog",
|
||||
"nimphomania",
|
||||
"nipple",
|
||||
"nipples",
|
||||
# "nsfw",
|
||||
"nsfw images",
|
||||
"nude",
|
||||
"nudity",
|
||||
"nutten",
|
||||
"nympho",
|
||||
"nymphomania",
|
||||
"octopussy",
|
||||
"omorashi",
|
||||
"one cup two girls",
|
||||
"one guy one jar",
|
||||
"orgasm",
|
||||
"orgy",
|
||||
"paedophile",
|
||||
"paki",
|
||||
"panties",
|
||||
"panty",
|
||||
"pedobear",
|
||||
"pedophile",
|
||||
"pegging",
|
||||
"penis",
|
||||
"phone sex",
|
||||
"piece of shit",
|
||||
"pikey",
|
||||
"pissing",
|
||||
"piss pig",
|
||||
"pisspig",
|
||||
"playboy",
|
||||
"pleasure chest",
|
||||
"pole smoker",
|
||||
"ponyplay",
|
||||
"poof",
|
||||
"poon",
|
||||
"poontang",
|
||||
"punany",
|
||||
"poop chute",
|
||||
"poopchute",
|
||||
"porn",
|
||||
"porno",
|
||||
"pornography",
|
||||
"prince albert piercing",
|
||||
"pthc",
|
||||
"pubes",
|
||||
"pussy",
|
||||
"queaf",
|
||||
"queef",
|
||||
"quim",
|
||||
"raghead",
|
||||
"raging boner",
|
||||
"rape",
|
||||
"raping",
|
||||
"rapist",
|
||||
"rectum",
|
||||
"reverse cowgirl",
|
||||
"rimjob",
|
||||
"rimming",
|
||||
"rosy palm",
|
||||
"rosy palm and her 5 sisters",
|
||||
"rusty trombone",
|
||||
"sadism",
|
||||
"santorum",
|
||||
"scat",
|
||||
"schlong",
|
||||
"scissoring",
|
||||
"semen",
|
||||
"sex",
|
||||
"sexcam",
|
||||
"sexo",
|
||||
"sexy",
|
||||
"sexual",
|
||||
"sexually",
|
||||
"sexuality",
|
||||
"shaved beaver",
|
||||
"shaved pussy",
|
||||
"shemale",
|
||||
"shibari",
|
||||
"shitblimp",
|
||||
"shota",
|
||||
"shrimping",
|
||||
"skeet",
|
||||
"slanteye",
|
||||
"slut",
|
||||
"s&m",
|
||||
"smut",
|
||||
"snatch",
|
||||
"snowballing",
|
||||
"sodomize",
|
||||
"sodomy",
|
||||
"spastic",
|
||||
"spic",
|
||||
"splooge",
|
||||
"splooge moose",
|
||||
"spooge",
|
||||
"spread legs",
|
||||
"spunk",
|
||||
"strap on",
|
||||
"strapon",
|
||||
"strappado",
|
||||
"strip club",
|
||||
"style doggy",
|
||||
"suicide girls",
|
||||
"sultry women",
|
||||
"swastika",
|
||||
"swinger",
|
||||
"tainted love",
|
||||
"taste my",
|
||||
"tea bagging",
|
||||
"threesome",
|
||||
"throating",
|
||||
"thumbzilla",
|
||||
"tied up",
|
||||
"tight white",
|
||||
"spank",
|
||||
"sperm",
|
||||
"tit",
|
||||
"vagina",
|
||||
"vulva",
|
||||
"whore",
|
||||
"ballsack",
|
||||
"blowjob",
|
||||
"busty",
|
||||
"clitoris",
|
||||
"cockring",
|
||||
"cunt",
|
||||
"deepthroat",
|
||||
"dominatrix",
|
||||
"ejaculation",
|
||||
"fetish",
|
||||
"gangbang",
|
||||
"genital",
|
||||
"jizzed",
|
||||
"lube",
|
||||
"milf",
|
||||
"nudity",
|
||||
"pecker",
|
||||
"pussylick",
|
||||
"semen",
|
||||
"shemale",
|
||||
"slutty",
|
||||
"threesome",
|
||||
"titfuck",
|
||||
"tit",
|
||||
"tits",
|
||||
"titties",
|
||||
"titty",
|
||||
"tongue in a",
|
||||
"topless",
|
||||
"tosser",
|
||||
"towelhead",
|
||||
"tranny",
|
||||
"tribadism",
|
||||
"tub girl",
|
||||
"tubgirl",
|
||||
"tushy",
|
||||
"twat",
|
||||
"twink",
|
||||
"twinkle",
|
||||
"two girls one cup",
|
||||
"undressing",
|
||||
"upskirt",
|
||||
"urethra play",
|
||||
"urophilia",
|
||||
"vagina",
|
||||
"venus mound",
|
||||
"viagra",
|
||||
"vibrator",
|
||||
"violet wand",
|
||||
"vorarephilia",
|
||||
"voyeur",
|
||||
"voyeurweb",
|
||||
"voyuer",
|
||||
"vulva",
|
||||
"wank",
|
||||
"wetback",
|
||||
"wet dream",
|
||||
"white powder",
|
||||
"whore",
|
||||
"worldsex",
|
||||
"wrapping men",
|
||||
"wrinkled starfish",
|
||||
"xxx",
|
||||
# "yaoi",
|
||||
"yellow showers",
|
||||
"yiffy",
|
||||
"zoophilia",
|
||||
"🖕",
|
||||
# "cracker",
|
||||
"honky",
|
||||
"kike",
|
||||
"weed",
|
||||
"cocaine",
|
||||
"heroin",
|
||||
"meth",
|
||||
"ecstasy",
|
||||
"lsd",
|
||||
"marijuana",
|
||||
"opioid",
|
||||
# "crack",
|
||||
"self-harm",
|
||||
"self harm",
|
||||
"self-injury",
|
||||
"self injury",
|
||||
"self-mutilation",
|
||||
"self mutilation",
|
||||
"hurting myself",
|
||||
"abortion",
|
||||
"rape",
|
||||
"molest",
|
||||
"pedophile",
|
||||
"incest",
|
||||
"abuse",
|
||||
"trafficking",
|
||||
"domestic violence",
|
||||
"child abuse",
|
||||
"neglect",
|
||||
"victim",
|
||||
"heathen",
|
||||
"infidel",
|
||||
"heretic",
|
||||
"blasphemer",
|
||||
"pagan",
|
||||
"zealot",
|
||||
"cultist",
|
||||
"fanatic",
|
||||
"apostate",
|
||||
"idolater",
|
||||
"fatty",
|
||||
"lardass",
|
||||
"twig",
|
||||
"beanpole",
|
||||
"chubby",
|
||||
"porker",
|
||||
"skeletor",
|
||||
"chunk",
|
||||
"blubber",
|
||||
# "skinny",
|
||||
"niga",
|
||||
"niger",
|
||||
"nga",
|
||||
]
|
||||
]
|
||||
|
||||
WORDS = re.compile(r"\w+")
|
||||
PATTERN = re.compile(
|
||||
f"^({"|".join(BLOCKED_WORDS + [word + "s" for word in BLOCKED_WORDS])})$",
|
||||
f"^({"|".join(words + [word + "s" for word in words])})$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
URL_PATTERN = re.compile(r"https?://[^\s]+", re.IGNORECASE)
|
||||
INVITES_PATTERN = re.compile(r"discord.gg/[^\s]+", re.IGNORECASE)
|
||||
INVITES_PATTERN = re.compile(r"(discord)?.gg/[^\s]+", re.IGNORECASE)
|
||||
|
||||
|
||||
def moderate(message: str) -> str:
|
||||
def censor(message: str) -> str:
|
||||
"""Returns the first censored word or pattern found.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
from discord.ext import commands
|
||||
|
||||
from payphone.db import utils
|
||||
from payphone.core import exceptions, constants
|
||||
|
||||
|
||||
def is_moderator():
|
||||
"""Checks whether the user is a moderator or not."""
|
||||
|
||||
async def predicate(ctx: commands.Context) -> bool:
|
||||
if ctx.author.bot:
|
||||
return False
|
||||
|
||||
user, _ = await utils.get_user(ctx.author)
|
||||
|
||||
if user.is_moderator:
|
||||
return True
|
||||
|
||||
raise exceptions.NotAModeratorError()
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def is_unbanned():
|
||||
"""Checks that the user is not banned."""
|
||||
|
||||
async def predicate(ctx: commands.Context) -> bool:
|
||||
if await ctx.bot.is_banned(ctx.author):
|
||||
raise exceptions.BannedError()
|
||||
|
||||
return True
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def is_on_call():
|
||||
"""Checks that a call is ongoing in the channel."""
|
||||
|
||||
async def predicate(ctx: commands.Context) -> bool:
|
||||
channel_id = ctx.channel.id
|
||||
|
||||
is_on_call = await ctx.bot.redis.sismember(
|
||||
constants.CHANNELS_ONCALL_SET, str(channel_id)
|
||||
)
|
||||
|
||||
if not is_on_call:
|
||||
raise exceptions.NotOnCallError()
|
||||
|
||||
return True
|
||||
|
||||
return commands.check(predicate)
|
||||
@@ -1,40 +0,0 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
TOKEN = os.getenv("TOKEN")
|
||||
|
||||
SHARD_ID = int(os.getenv("SHARD_ID", "0"))
|
||||
SHARD_COUNT = int(os.getenv("SHARD_COUNT", "1"))
|
||||
|
||||
IS_DEBUG = os.environ.get("IS_DEBUG", "false") == "true"
|
||||
|
||||
TORTOISE_ORM = {
|
||||
"connections": {
|
||||
"default": {
|
||||
"engine": "tortoise.backends.asyncpg",
|
||||
"credentials": {
|
||||
"host": "localhost" if IS_DEBUG else "postgresql",
|
||||
"port": "5432",
|
||||
"user": "payphone",
|
||||
"password": "payphone",
|
||||
"database": "payphone",
|
||||
},
|
||||
"minsize": 5,
|
||||
"maxsize": 100,
|
||||
},
|
||||
},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["payphone.db.models", "aerich.models"],
|
||||
"default_connection": "default",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
SUGGESTIONS_CHANNEL_ID = int(os.getenv("SUGGESTIONS_CHANNEL_ID", "0"))
|
||||
|
||||
SERVER_INVITE = os.getenv("SERVER_INVITE")
|
||||
+21
-218
@@ -1,228 +1,31 @@
|
||||
import enum
|
||||
import os
|
||||
|
||||
from payphone.core import config
|
||||
try:
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
class Mask(enum.IntEnum):
|
||||
FERRET = 1278176917123760139 if config.IS_DEBUG else 1278173356423909448
|
||||
OCTOPUS = 1278176904410697880 if config.IS_DEBUG else 1278173331421397072
|
||||
CAT = 1278176891290783824 if config.IS_DEBUG else 1278173317034934282
|
||||
MONKEY = 1278176881195094049 if config.IS_DEBUG else 1278173297456185344
|
||||
PANDA = 1278176868767629455 if config.IS_DEBUG else 1278173272390762538
|
||||
SHEEP = 1278176856910073946 if config.IS_DEBUG else 1278173251914432563
|
||||
HORSE = 1278176842116894760 if config.IS_DEBUG else 1278173237582237708
|
||||
BEE = 1278176823846506577 if config.IS_DEBUG else 1278173216652787826
|
||||
WILD_BOAR = 1278176806498996235 if config.IS_DEBUG else 1278173196658413649
|
||||
DUCK = 1278176794591232102 if config.IS_DEBUG else 1278173181483679918
|
||||
CHICKEN = 1278176781790216233 if config.IS_DEBUG else 1278173167218589726
|
||||
ELEPHANT = 1278176771413512213 if config.IS_DEBUG else 1278173150063886346
|
||||
COW = 1278176761565413483 if config.IS_DEBUG else 1278173125024153733
|
||||
DEER = 1278176744691597312 if config.IS_DEBUG else 1278173110105014406
|
||||
TURKEY = 1278176730619576321 if config.IS_DEBUG else 1278173091796877363
|
||||
DOG = 1278176719932751903 if config.IS_DEBUG else 1278173070846201856
|
||||
BEAVER = 1278176709530882091 if config.IS_DEBUG else 1278173051485425705
|
||||
CAMEL = 1278176699921465394 if config.IS_DEBUG else 1278173035018584086
|
||||
KOALA = 1278176687493746709 if config.IS_DEBUG else 1278173021345021952
|
||||
HIPPO = 1278176675846295603 if config.IS_DEBUG else 1278173007419932804
|
||||
OSTRICH = 1278176661317095494 if config.IS_DEBUG else 1278172994669248523
|
||||
RHINO = 1278176650340864000 if config.IS_DEBUG else 1278172980848885832
|
||||
SQUIRREL = 1278176637627924570 if config.IS_DEBUG else 1278172965590274088
|
||||
CHAMELEON = 1278176625392877648 if config.IS_DEBUG else 1278172953305022496
|
||||
FROG = 1278176614512857088 if config.IS_DEBUG else 1278172937043710094
|
||||
SKUNK = 1278176604509442079 if config.IS_DEBUG else 1278172923495976981
|
||||
WALRUS = 1278176554723311678 if config.IS_DEBUG else 1278172912368619603
|
||||
HEDGEHOG = 1278176541750067252 if config.IS_DEBUG else 1278172899911405678
|
||||
PIG = 1278176531415564332 if config.IS_DEBUG else 1278172885239861338
|
||||
HEN = 1278176518643646485 if config.IS_DEBUG else 1278172871302184994
|
||||
TURTLE = 1278176507428343891 if config.IS_DEBUG else 1278172858199314455
|
||||
MOUSE = 1278175957047578696 if config.IS_DEBUG else 1278172846442414081
|
||||
RABBIT = 1278175944007487489 if config.IS_DEBUG else 1278172831921999945
|
||||
GOAT = 1278175933915856938 if config.IS_DEBUG else 1278172815421472808
|
||||
LION = 1278175922964664382 if config.IS_DEBUG else 1278172803534684235
|
||||
WOLF = 1278175902047535165 if config.IS_DEBUG else 1278172791052570687
|
||||
SNAKE = 1278175869331968061 if config.IS_DEBUG else 1278172776259260547
|
||||
LLAMA = 1278175850008674325 if config.IS_DEBUG else 1278172764913799168
|
||||
CRAB = 1278175835634925620 if config.IS_DEBUG else 1278172752322232330
|
||||
OWL = 1278175822947029152 if config.IS_DEBUG else 1278172738309193749
|
||||
BAT = 1278175813572886550 if config.IS_DEBUG else 1278172726426865724
|
||||
ZEBRA = 1278175799962505216 if config.IS_DEBUG else 1278172715114565753
|
||||
FOX = 1278175788818104474 if config.IS_DEBUG else 1278172702955274292
|
||||
GORILLA = 1278175775891259456 if config.IS_DEBUG else 1278172687797194824
|
||||
SLOTH = 1278175765661356175 if config.IS_DEBUG else 1278172676837343242
|
||||
PUFFER_FISH = 1278175753497743360 if config.IS_DEBUG else 1278172665093558294
|
||||
BEAR = 1278175741569142867 if config.IS_DEBUG else 1278172653076746301
|
||||
GIRAFFE = 1278175732601983006 if config.IS_DEBUG else 1278172640296702073
|
||||
PENGUIN = 1278175721956573236 if config.IS_DEBUG else 1278172627629899808
|
||||
PARROT = 1278175706798489642 if config.IS_DEBUG else 1278172600702337074
|
||||
except ImportError:
|
||||
print("dotenv is not installed. No .env file was loaded.")
|
||||
|
||||
IS_DEBUG = os.getenv("IS_DEBUG", "false").lower() == "true"
|
||||
|
||||
class CatMask(enum.IntEnum):
|
||||
DINOSAUR = 1278255435652010014 if config.IS_DEBUG else 1278254725833297965
|
||||
SHLEEPY_CAT = 1278255475066146816 if config.IS_DEBUG else 1278254779767984181
|
||||
COZY_CAT = 1278255420124696663 if config.IS_DEBUG else 1278254704324771893
|
||||
COQUETTE_CAT = 1278255408254816300 if config.IS_DEBUG else 1278254684200636428
|
||||
PANIC_CAT = 1278255451993145345 if config.IS_DEBUG else 1278254760700674098
|
||||
TOKEN = os.getenv("TOKEN")
|
||||
|
||||
if TOKEN is None:
|
||||
raise RuntimeError(
|
||||
"No token provided. Make sure to set the TOKEN environment variable."
|
||||
)
|
||||
|
||||
MASKS_LIST = list(Mask)
|
||||
CAT_MASKS_LIST = list(CatMask)
|
||||
SHARD_ID = int(os.getenv("SHARD_ID", "0"))
|
||||
SHARD_COUNT = int(os.getenv("SHARD_COUNT", "1"))
|
||||
|
||||
TIPS = [
|
||||
"You can report calls using `{prefix}report`.",
|
||||
"Help us sharing Payphone to make it more active!",
|
||||
"You can check your stats with `{prefix}me`.",
|
||||
"Emojis will only show if they're from a server Payphone's in.",
|
||||
"You cannot send links.",
|
||||
"[Vote for Payphone on top.gg](<https://top.gg/bot/1212940413795827763/vote>) to help us reach more users!",
|
||||
"[Write a review](<https://top.gg/bot/1212940413795827763#reviews>) to tell everyone your opinion about Payphone!",
|
||||
"Calls will be automatically hung up after 15 minutes if nobody talks.",
|
||||
"You can change Payphone's prefix with `{prefix}config prefix`.",
|
||||
"Do you have any suggestions? Use `{prefix}suggest`!",
|
||||
"You can use `{prefix}skip` to directly search for a new call.",
|
||||
"The links from `{prefix}history` will take you to that call.",
|
||||
"Want to stay hidden? Use `{prefix}mask on`!",
|
||||
"Did you know you can check your mask with `{prefix}mask`?",
|
||||
"Do you like Payphone? Consider [making a donation](<https://ko-fi.com/payphonedev>) to help us grow Payphone (and keep it running)!",
|
||||
"Using `{prefix}friendreq` is the best way to ask the other party to add you.",
|
||||
"Enable and disable channels using `{prefix}config enable` and `{prefix}config disable`!",
|
||||
"There's a secret easter egg command. Try to find it! (Hint: cat)",
|
||||
"Make sure to read Payphone's [Code of Conduct](<https://payphone.nyeki.dev/code-of-conduct>). Let's keep Payphone safe and fun for everyone!",
|
||||
f"Join [Payphone's official server](<{config.SERVER_INVITE}>) to participate in polls for new features!",
|
||||
"How high are you on the leaderboard? Use `{prefix}me`, `{prefix}leaderboard`, and `{prefix}leaderboard servers` to check it out!",
|
||||
]
|
||||
|
||||
MSGS_PHONE_CALLING = [
|
||||
"Dialing now! Please wait...",
|
||||
"Trying to connect you...",
|
||||
"Making the call, hang tight!",
|
||||
"Searching for someone to answer...",
|
||||
"Your call is being placed...",
|
||||
"Connecting your call now...",
|
||||
"Calling! One moment please...",
|
||||
"Attempting to reach your contact...",
|
||||
"Establishing the connection...",
|
||||
"Your call is on the way!",
|
||||
]
|
||||
|
||||
MSGS_PHONE_ANSWERED = [
|
||||
"Call connected! You're live!",
|
||||
"The call was answered.",
|
||||
"Someone picked up the call!",
|
||||
"Connected! Start talking.",
|
||||
"You're now connected!",
|
||||
"The call was successfully answered.",
|
||||
"Call picked up! You're connected.",
|
||||
"You're now live on the call!",
|
||||
"Call answered. Go ahead!",
|
||||
"Connection successful! You're live.",
|
||||
]
|
||||
|
||||
MSGS_PHONE_HANGUP_ME = [
|
||||
"Call ended. You hung up.",
|
||||
"You've disconnected the call.",
|
||||
"You ended the call.",
|
||||
"Call terminated. You hung up.",
|
||||
"You have hung up the call.",
|
||||
"You ended the conversation.",
|
||||
"You've finished the call.",
|
||||
"Call ended by you.",
|
||||
"You disconnected the call.",
|
||||
"You have ended the call.",
|
||||
]
|
||||
|
||||
MSGS_PHONE_HANGUP_YOU = [
|
||||
"The other party has hung up.",
|
||||
"Call ended by the other party.",
|
||||
"The other person disconnected.",
|
||||
"The call was terminated by the other party.",
|
||||
"The other person has hung up.",
|
||||
"Conversation ended by the other party.",
|
||||
"The call has been finished by the other party.",
|
||||
"The other party ended the call.",
|
||||
"Disconnected by the other party.",
|
||||
"The other person ended the call.",
|
||||
]
|
||||
|
||||
MSGS_PHONE_NO_ANSWER = [
|
||||
"No answer. Maybe try again later?",
|
||||
"It looks like nobody picked up.",
|
||||
"The call went unanswered.",
|
||||
"No response. Try calling again later.",
|
||||
"No one is available to answer.",
|
||||
"The call wasn't answered.",
|
||||
"Nobody answered the call.",
|
||||
"The call went unanswered. Try again soon!",
|
||||
"No answer at the moment.",
|
||||
"The call wasn't picked up.",
|
||||
]
|
||||
|
||||
MSGS_PHONE_NOT_IN_CALL = [
|
||||
"It looks like you're not in a call right now.",
|
||||
"No active call at the moment.",
|
||||
"You're not connected to any call.",
|
||||
"There's no call to hang up right now.",
|
||||
"You're not currently on a call.",
|
||||
"No call in progress.",
|
||||
"You're not in an active call.",
|
||||
"There isn't a call to end right now.",
|
||||
"You're not on a call.",
|
||||
"No ongoing call to disconnect.",
|
||||
]
|
||||
|
||||
CHANNELS_SEARCH_QUEUE = "channels:searching"
|
||||
CHANNELS_ONCALL_SET = "channels:on-call"
|
||||
MESSAGES_QUEUE = "messages"
|
||||
MESSAGE_CONTENTS_QUEUE = "message-contents"
|
||||
MESSAGES_PROPAGATION_MAP = "messages-propagation"
|
||||
|
||||
CALL_MESSAGES_TEMPLATE = "m:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
The key template for call messages hash.
|
||||
"""
|
||||
|
||||
CALL_MESSAGE_CONTENTS_TEMPLATE = "mc:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
The key template for call message contents hash.
|
||||
"""
|
||||
|
||||
CALL_MESSAGE_CONTENTS_HASH_TEMPLATE = "%s-%s"
|
||||
"""`%s-%s`: Call ID - Iteration.
|
||||
|
||||
The key template for call message contents hash keys.
|
||||
"""
|
||||
|
||||
CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE = "mfp:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
A map from propagation IDs to message IDs.
|
||||
"""
|
||||
|
||||
CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE = "mci:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
A map of messages to amount of iterations of their message contents.
|
||||
"""
|
||||
|
||||
CALL_MESSAGE_REPLIES_TEMPLATE = "mr:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
A map of message IDs to a `,`-delimited list of message IDs that reply to the message.
|
||||
"""
|
||||
|
||||
CALL_IS_ACTIVE = "cis:%s"
|
||||
"""`%s`: Call ID.
|
||||
|
||||
An expiring key that indicates whether the call is still active or not. This key will exist while
|
||||
the call is still active."""
|
||||
|
||||
CALL_IDS_TO_FLUSH = "call-ids-to-flush"
|
||||
"""A set of IDs of calls to flush to the DB."""
|
||||
|
||||
BAN_TEMPLATE = "b:%s"
|
||||
PREFIX_TEMPLATE = "p:%s"
|
||||
|
||||
EMBED_COLOR = 0xE2D9D0
|
||||
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
|
||||
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
|
||||
|
||||
INVISIBLE_CHARACTER = "\u200B"
|
||||
BRAILE_CHARACTER = "\u2800"
|
||||
|
||||
PRIMARY_COLOR = 0xE2D9D0
|
||||
SUPPORTER_COLOR = 0xAB9EFF
|
||||
MODERATOR_COLOR = 0xFF8787
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import converter
|
||||
|
||||
from tortoise import expressions
|
||||
|
||||
import orjson
|
||||
|
||||
from payphone.db import models
|
||||
from payphone.core import exceptions, constants
|
||||
|
||||
|
||||
class Timedelta(converter.Converter):
|
||||
"""Converts a string like "1d 2h" to a `datetime.timedelta` object."""
|
||||
|
||||
async def convert(self, _ctx: commands.Context, argument: str) -> timedelta:
|
||||
multipliers = {
|
||||
"s": 1,
|
||||
"m": 60,
|
||||
"h": 60 * 60,
|
||||
"d": 60 * 60 * 24,
|
||||
"w": 60 * 60 * 24 * 7,
|
||||
"M": 60 * 60 * 24 * 30,
|
||||
"y": 60 * 60 * 24 * 365,
|
||||
}
|
||||
|
||||
if argument[-1] in multipliers:
|
||||
try:
|
||||
unit = multipliers[argument[-1]]
|
||||
quantity = float(argument[:-1])
|
||||
|
||||
return timedelta(seconds=unit * quantity)
|
||||
|
||||
except Exception as e:
|
||||
raise exceptions.InvalidTimedeltaError(
|
||||
f'"{argument}" is not a valid time delta.'
|
||||
) from e
|
||||
|
||||
raise exceptions.InvalidTimedeltaError(
|
||||
f'"{argument}" is not a valid time delta.'
|
||||
)
|
||||
|
||||
|
||||
class DiscordUserID(converter.Converter):
|
||||
"""Converts a URL, ID, or reply into a Discord User ID."""
|
||||
|
||||
async def convert(self, ctx: commands.Context, argument: str) -> int:
|
||||
if argument.lower() == "me":
|
||||
return ctx.author.id
|
||||
|
||||
if argument.isdecimal() and len(argument) in range(17, 20):
|
||||
return int(argument)
|
||||
|
||||
if len(argument) > 17 and argument[2:-1].isdecimal():
|
||||
return int(argument[2:-1])
|
||||
|
||||
if argument.startswith("https://discord.com/users/") or argument.startswith(
|
||||
"discord://-/users/"
|
||||
):
|
||||
return int(argument.split("?", 1)[0].rsplit("/", 1)[1])
|
||||
|
||||
if argument.lower() in ["reply", "."]:
|
||||
if ctx.message.reference is not None:
|
||||
ref = ctx.message.reference
|
||||
|
||||
msg = ref.cached_message or await ctx.channel.fetch_message(
|
||||
ref.message_id
|
||||
)
|
||||
|
||||
if msg.author.bot:
|
||||
row = await models.Message.get_or_none(
|
||||
expressions.Q(id=msg.id, propagation_id=msg.id, join_type="OR")
|
||||
)
|
||||
|
||||
if row is not None:
|
||||
return row.user_id
|
||||
|
||||
channel = await models.Channel.get_or_none(id=ctx.channel.id)
|
||||
|
||||
if channel is None:
|
||||
raise converter.BadArgument()
|
||||
|
||||
original_message_id = await ctx.bot.redis.hget(
|
||||
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE
|
||||
% channel.call_id,
|
||||
ref.message_id,
|
||||
)
|
||||
|
||||
if original_message_id is None:
|
||||
raise converter.BadArgument()
|
||||
|
||||
original_message_json = await ctx.bot.redis.hget(
|
||||
constants.CALL_MESSAGES_TEMPLATE % channel.call_id,
|
||||
int(original_message_id),
|
||||
)
|
||||
|
||||
if original_message_json is None:
|
||||
raise converter.BadArgument()
|
||||
|
||||
original_message_dict = orjson.loads(original_message_json)
|
||||
|
||||
return original_message_dict["user_id"]
|
||||
|
||||
return msg.author.id
|
||||
|
||||
user = await models.User.get_or_none(username=argument)
|
||||
|
||||
if user is not None:
|
||||
return user.id
|
||||
|
||||
raise converter.BadArgument()
|
||||
@@ -1,21 +0,0 @@
|
||||
from discord.ext.commands import errors
|
||||
|
||||
|
||||
class ChannelDisabledError(errors.CheckFailure):
|
||||
"""Raised when the channel being run the command in is disabled."""
|
||||
|
||||
|
||||
class NotAModeratorError(errors.CheckFailure):
|
||||
"""Raised when a non-moderator tries to run a moderator-only command."""
|
||||
|
||||
|
||||
class InvalidTimedeltaError(Exception):
|
||||
"""Raised when the passed timedelta string cannot be parsed by the converter."""
|
||||
|
||||
|
||||
class BannedError(errors.CheckFailure):
|
||||
"""Raised when the user running the command is banned."""
|
||||
|
||||
|
||||
class NotOnCallError(errors.CheckFailure):
|
||||
"""Raised when the command is ran in a channel with no ongoing calls."""
|
||||
@@ -1,271 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import re
|
||||
|
||||
import discord
|
||||
|
||||
from payphone.db import models
|
||||
from payphone.core import constants
|
||||
|
||||
|
||||
def trim(text: str, length: int = 100) -> str:
|
||||
"""Trims a string if it's too long.
|
||||
|
||||
Args:
|
||||
text (str): The original text.
|
||||
length (int, optional): The max length of the string before trimming. Defaults to 100.
|
||||
|
||||
Returns:
|
||||
str: The trimmed (or not) text.
|
||||
"""
|
||||
|
||||
if len(text) > length:
|
||||
return text[:length - 3] + "..."
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def text_join(texts: list[str], join_word: str = "and") -> str:
|
||||
"""Joins a list with commas and the last item with an "and".
|
||||
|
||||
Args:
|
||||
texts (list[str]): The list of texts to concatenate.
|
||||
join_word (str): The word to use at the end of the joint.
|
||||
|
||||
Returns:
|
||||
str: The formatted concatenation.
|
||||
"""
|
||||
|
||||
if len(texts) > 1:
|
||||
return f"{", ".join(texts[:-1])} {join_word} {texts[-1]}"
|
||||
|
||||
return texts[0]
|
||||
|
||||
|
||||
def monospace_grid(values: list[str], space: int = 14) -> str:
|
||||
"""Creates a monospace grid.
|
||||
|
||||
Args:
|
||||
values (list[str]): All the values that the list must contain
|
||||
space (int, optional): The width of each column. Defaults to 14.
|
||||
|
||||
Returns:
|
||||
str: The formatted grid.
|
||||
"""
|
||||
|
||||
return "```" + "".join([v + " " * (space - len(v)) for v in values]) + "```"
|
||||
|
||||
|
||||
def monospace_table(
|
||||
rows: list[list[str | tuple[str, str]]],
|
||||
headers: list | None = None,
|
||||
column_separator: str = "|",
|
||||
header_separator: str = "-",
|
||||
intersection: str = "+",
|
||||
padding: int = 1,
|
||||
) -> str:
|
||||
"""Creates a monospace table.
|
||||
|
||||
Creates a monospace table. Row columns can be either a string with the column's content, or a
|
||||
tuple with two items, being the first one aligned to the left and the second one aligned to the
|
||||
right. Non-string values will be converted to string using `str(value)`.
|
||||
|
||||
Args:
|
||||
rows (list[list[str | tuple[str, str]]]): The rows.
|
||||
headers (list | None, optional): The headers.
|
||||
column_separator (str, optional): The column separator. Defaults to "|".
|
||||
header_separator (str, optional): The header separator. Defaults to "-".
|
||||
intersection (str, optional): The intersection character where column and row separators
|
||||
meet.
|
||||
padding (int, optional): The padding that each column has at its sides.
|
||||
|
||||
Returns:
|
||||
str: The monospace table.
|
||||
"""
|
||||
|
||||
max_sizes = (
|
||||
[
|
||||
max(
|
||||
*(
|
||||
[
|
||||
(
|
||||
len(str(row[i]))
|
||||
if not isinstance(row[i], tuple)
|
||||
else (sum(len(str(item)) for item in row[i]) + 1)
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
+ ([len(headers[i])] if headers is not None else [])
|
||||
)
|
||||
)
|
||||
for i in range(len(rows[0]))
|
||||
]
|
||||
if len(rows) >= 2
|
||||
else (
|
||||
[
|
||||
(
|
||||
len(str(col))
|
||||
if not isinstance(col, tuple)
|
||||
else (sum(len(str(item)) for item in col) + 1)
|
||||
)
|
||||
for col in rows[0]
|
||||
]
|
||||
if len(rows) > 0
|
||||
else []
|
||||
)
|
||||
)
|
||||
|
||||
def format_row(row: list) -> str:
|
||||
return column_separator.join(
|
||||
[
|
||||
(" " * padding)
|
||||
+ (
|
||||
(str(col) + (" " * (max_sizes[i] - len(str(col)))))
|
||||
if not isinstance(col, tuple)
|
||||
else (
|
||||
str(col[0])
|
||||
+ (" " * (max_sizes[i] - len(str(col[0])) - len(str(col[1]))))
|
||||
+ str(col[1])
|
||||
)
|
||||
)
|
||||
+ (" " * padding)
|
||||
for i, col in enumerate(row)
|
||||
]
|
||||
)
|
||||
|
||||
formatted_rows = []
|
||||
|
||||
for row in rows:
|
||||
formatted_rows.append(format_row(row))
|
||||
|
||||
table = "\n".join(formatted_rows)
|
||||
|
||||
if headers is not None:
|
||||
header = format_row(headers)
|
||||
|
||||
separator = intersection.join(
|
||||
[header_separator * (size + padding * 2) for size in max_sizes]
|
||||
)
|
||||
|
||||
table = f"{header}\n{separator}\n{table}"
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def clean(
|
||||
message: discord.Message, call_rss: list[models.CallUserChannelRelationship]
|
||||
) -> str:
|
||||
"""Cleans the message.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The message to clean.
|
||||
call_rss (list[models.CallUserChannelRelationship]): The call's relationships. They must
|
||||
have pre-selected their `profile` relation.
|
||||
|
||||
Returns:
|
||||
str: The cleaned message content.
|
||||
"""
|
||||
|
||||
cleaned_message = message.content
|
||||
|
||||
if message.mentions:
|
||||
for call_rs in call_rss:
|
||||
cleaned_message = cleaned_message.replace(
|
||||
f"<@{call_rs.user_id}>",
|
||||
(
|
||||
f"@**{call_rs.display_name}**"
|
||||
if message.channel.id == call_rs.channel_id
|
||||
else f"@**{call_rs.profile.name}**"
|
||||
),
|
||||
)
|
||||
|
||||
for user in message.mentions:
|
||||
cleaned_message = cleaned_message.replace(user.mention, "@*Not in call*")
|
||||
|
||||
for channel in message.channel_mentions:
|
||||
cleaned_message = cleaned_message.replace(
|
||||
f"<#{channel.id}>", f"#**{channel.name}**"
|
||||
)
|
||||
|
||||
for role in message.role_mentions:
|
||||
cleaned_message = cleaned_message.replace(f"<@&{role.id}>", f"@**{role.name}**")
|
||||
|
||||
cleaned_message = cleaned_message.replace(
|
||||
"@everyone", f"@{constants.INVISIBLE_CHARACTER}everyone"
|
||||
).replace("@here", f"@{constants.INVISIBLE_CHARACTER}here")
|
||||
|
||||
return cleaned_message
|
||||
|
||||
|
||||
def time_to_text(time_diff: timedelta) -> str:
|
||||
"""Converts a timedelta to its string representation.
|
||||
|
||||
Args:
|
||||
time_diff (timedelta): The timedelta.
|
||||
|
||||
Returns:
|
||||
str: The string representation.
|
||||
"""
|
||||
|
||||
years = time_diff.days // 365
|
||||
days = time_diff.days % 365
|
||||
hours, remainder = divmod(time_diff.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
parts = []
|
||||
if years > 0:
|
||||
parts.append(f"{years} year{"s" if years != 1 else ""}")
|
||||
if days > 0:
|
||||
parts.append(f"{days} day{"s" if days != 1 else ""}")
|
||||
if hours > 0:
|
||||
parts.append(f"{hours} hour{"s" if hours != 1 else ""}")
|
||||
if minutes > 0:
|
||||
parts.append(f"{minutes} min{"s" if minutes != 1 else ""}")
|
||||
if seconds > 0:
|
||||
parts.append(f"{seconds} sec{"s" if seconds != 1 else ""}")
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def parse_timedelta(text: str) -> timedelta:
|
||||
"""Parses a timedelta string formatted as 30h 45m to a timedelta object.
|
||||
|
||||
Args:
|
||||
text (str): The formatted string.
|
||||
|
||||
Raises:
|
||||
ValueError: The string is invalid.
|
||||
|
||||
Returns:
|
||||
datetime.timedelta: The timedelta object.
|
||||
"""
|
||||
|
||||
pattern = re.compile(
|
||||
r"(?:(\d+)y)?\s*" # years
|
||||
r"(?:(\d+)mo)?\s*" # months
|
||||
r"(?:(\d+)w)?\s*" # weeks
|
||||
r"(?:(\d+)d)?\s*" # days
|
||||
r"(?:(\d+)h)?\s*" # hours
|
||||
r"(?:(\d+)m)?\s*" # minutes
|
||||
r"(?:(\d+)s)?" # seconds
|
||||
)
|
||||
matches = pattern.match(text.strip())
|
||||
|
||||
if not matches:
|
||||
raise ValueError("Invalid time string format")
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds = matches.groups()
|
||||
|
||||
years = int(years) if years else 0
|
||||
months = int(months) if months else 0
|
||||
weeks = int(weeks) if weeks else 0
|
||||
days = int(days) if days else 0
|
||||
hours = int(hours) if hours else 0
|
||||
minutes = int(minutes) if minutes else 0
|
||||
seconds = int(seconds) if seconds else 0
|
||||
|
||||
days += weeks * 7
|
||||
days += months * 30
|
||||
days += years * 365
|
||||
|
||||
return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
from typing import List, Dict
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from payphone.cogs import base
|
||||
from payphone.core import formatting, config, constants, typing_utils
|
||||
|
||||
|
||||
class HelpView(discord.ui.View):
|
||||
"""The view sent with the help command."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
discord.ui.Button(
|
||||
label="Credits",
|
||||
style=discord.ButtonStyle.gray,
|
||||
url="https://payphone.nyeki.dev",
|
||||
),
|
||||
discord.ui.Button(
|
||||
label="Donate",
|
||||
style=discord.ButtonStyle.gray,
|
||||
url="https://ko-fi.com/payphonedev",
|
||||
),
|
||||
discord.ui.Button(
|
||||
label="Vote",
|
||||
style=discord.ButtonStyle.gray,
|
||||
url="https://top.gg/bot/1212940413795827763/vote",
|
||||
),
|
||||
discord.ui.Button(
|
||||
label="Server",
|
||||
style=discord.ButtonStyle.gray,
|
||||
url=config.SERVER_INVITE,
|
||||
),
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
|
||||
class HelpCommand(commands.HelpCommand):
|
||||
async def send_bot_help(
|
||||
self, mapping: Dict[base.Cog | None, List[commands.Command]]
|
||||
) -> None:
|
||||
bot_commands: List[commands.Command] = []
|
||||
|
||||
for cog, cog_commands in mapping.items():
|
||||
if cog is not None and not cog.show_in_help:
|
||||
continue
|
||||
|
||||
bot_commands += cog_commands
|
||||
|
||||
embed = discord.Embed(
|
||||
author=self.context.bot.embed_author,
|
||||
title="Payphone, call and connect with other people.",
|
||||
description=(
|
||||
"Here is the list of all available commands. You can see more details about a "
|
||||
f"specific command with `{self.context.clean_prefix}help <command>`."
|
||||
),
|
||||
thumbnail=self.context.bot.user.display_avatar.url,
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Commands",
|
||||
value=formatting.monospace_grid(
|
||||
[c.name for c in bot_commands], space=12
|
||||
),
|
||||
)
|
||||
],
|
||||
color=constants.EMBED_COLOR,
|
||||
footer=discord.EmbedFooter(
|
||||
text=f"Use {self.context.clean_prefix}help <command> to get more information about a specific command."
|
||||
),
|
||||
)
|
||||
|
||||
await self.context.send(embed=embed, view=HelpView())
|
||||
|
||||
async def send_command_help(self, command: commands.Command) -> None:
|
||||
command_name = (
|
||||
command.name
|
||||
if isinstance(command, discord.SlashCommand)
|
||||
else command.qualified_name
|
||||
)
|
||||
command_options = "".join(
|
||||
[f" <{option.name}>" for option in command.options]
|
||||
if isinstance(command, discord.SlashCommand)
|
||||
else [
|
||||
(
|
||||
f" [<{name}>]"
|
||||
if typing_utils.is_optional(info.annotation)
|
||||
else f" <{name}>"
|
||||
)
|
||||
for name, info in command.params.items()
|
||||
if name not in ["self", "ctx"]
|
||||
]
|
||||
)
|
||||
command_description = (
|
||||
command.description if command.description else command.help
|
||||
).replace("{prefix}", self.context.clean_prefix)
|
||||
|
||||
full_command_usage = (
|
||||
self.context.clean_prefix + command.qualified_name + command_options
|
||||
)
|
||||
reply_argument = (
|
||||
command.extras.get("reply_argument", None) if command.extras else None
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
author=discord.EmbedAuthor(
|
||||
name="Payphone", icon_url=self.context.bot.user.display_avatar.url
|
||||
),
|
||||
title=(
|
||||
command.extras["help_name"]
|
||||
if command.extras and "help_name" in command.extras
|
||||
else command_name.title()
|
||||
)
|
||||
+ " Command",
|
||||
description=command_description,
|
||||
thumbnail=self.context.bot.user.display_avatar.url,
|
||||
color=constants.EMBED_COLOR,
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Usage",
|
||||
value=f"{f"`┌─ [<{reply_argument}>]`\n" if reply_argument else ""}`{full_command_usage}`",
|
||||
)
|
||||
]
|
||||
+ (
|
||||
[
|
||||
discord.EmbedField(
|
||||
name="Aliases",
|
||||
value=", ".join(
|
||||
[
|
||||
f"`{self.context.clean_prefix}{alias}`"
|
||||
for alias in command.aliases
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
if len(command.aliases) > 0
|
||||
else []
|
||||
),
|
||||
footer=discord.EmbedFooter(
|
||||
f"Use {self.context.clean_prefix}help <command> to get more information about a specific command."
|
||||
),
|
||||
)
|
||||
|
||||
await self.context.send(embed=embed)
|
||||
|
||||
async def send_group_help(self, group):
|
||||
return await self.send_command_help(group)
|
||||
|
||||
async def send_error_message(self, error):
|
||||
await self.context.send(f"❌ {error}")
|
||||
@@ -34,5 +34,12 @@ def setup(process_name: str):
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG, handlers=[_get_handler(process_name)])
|
||||
|
||||
logging.getLogger("asyncio").addHandler(_get_handler(process_name, "asyncio"))
|
||||
logging.getLogger("discord").addHandler(_get_handler(process_name, "discord"))
|
||||
discord_logger = logging.getLogger("discord")
|
||||
discord_logger.handlers.clear()
|
||||
discord_logger.addHandler(_get_handler(process_name, "discord"))
|
||||
discord_logger.propagate = False
|
||||
|
||||
asyncio_logger = logging.getLogger("asyncio")
|
||||
asyncio_logger.handlers.clear()
|
||||
asyncio_logger.addHandler(_get_handler(process_name, "asyncio"))
|
||||
asyncio_logger.propagate = False
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
TIPS = [
|
||||
"You can report calls using `Apps > Report Message`.",
|
||||
"Help us sharing Payphone to make it more active!",
|
||||
# "You can check your stats with `{prefix}me`.",
|
||||
"Emojis will only show if they're from a server Payphone's in.",
|
||||
"You cannot send links.",
|
||||
"[Vote for Payphone on top.gg](<https://top.gg/bot/1212940413795827763/vote>) to help us reach more users!",
|
||||
"[Write a review](<https://top.gg/bot/1212940413795827763#reviews>) to tell everyone your opinion about Payphone!",
|
||||
"Calls will be automatically hung up after 15 minutes if nobody talks.",
|
||||
"You can change Payphone's prefix with `{prefix}config prefix`.",
|
||||
"Do you have any suggestions? Use `{prefix}suggest`!",
|
||||
"You can use `{prefix}skip` to directly search for a new call.",
|
||||
# "The links from `{prefix}history` will take you to that call.",
|
||||
"Want to stay hidden? Use `{prefix}mask on`!",
|
||||
"Did you know you can check your mask with `{prefix}mask`?",
|
||||
"Do you like Payphone? Consider [making a donation](<https://ko-fi.com/payphonedev>) to help us grow Payphone (and keep it running)!",
|
||||
"Using `{prefix}friendreq` is the best way to ask the other party to add you.",
|
||||
"Enable and disable channels using `{prefix}config enable` and `{prefix}config disable`!",
|
||||
"There's a secret easter egg command. Try to find it! (Hint: cat)",
|
||||
"Make sure to read Payphone's [Code of Conduct](<https://payphone.nyeki.dev/code-of-conduct>). Let's keep Payphone safe and fun for everyone!",
|
||||
"Join [Payphone's official server](<https://discord.gg/payphone>) to participate in polls for new features!",
|
||||
# "How high are you on the leaderboard? Use `{prefix}me`, `{prefix}leaderboard`, and `{prefix}leaderboard servers` to check it out!",
|
||||
]
|
||||
|
||||
CALLING = [
|
||||
"Dialing now! Please wait...",
|
||||
"Trying to connect you...",
|
||||
"Making the call, hang tight!",
|
||||
"Searching for someone to answer...",
|
||||
"Your call is being placed...",
|
||||
"Connecting your call now...",
|
||||
"Calling! One moment please...",
|
||||
"Attempting to reach your contact...",
|
||||
"Establishing the connection...",
|
||||
"Your call is on the way!",
|
||||
]
|
||||
|
||||
ANSWERED = [
|
||||
"Call connected! You're live!",
|
||||
"The call was answered.",
|
||||
"Someone picked up the call!",
|
||||
"Connected! Start talking.",
|
||||
"You're now connected!",
|
||||
"The call was successfully answered.",
|
||||
"Call picked up! You're connected.",
|
||||
"You're now live on the call!",
|
||||
"Call answered. Go ahead!",
|
||||
"Connection successful! You're live.",
|
||||
]
|
||||
|
||||
HANGUP = [
|
||||
"Call ended. You hung up.",
|
||||
"You've disconnected the call.",
|
||||
"You ended the call.",
|
||||
"Call terminated. You hung up.",
|
||||
"You have hung up the call.",
|
||||
"You ended the conversation.",
|
||||
"You've finished the call.",
|
||||
"Call ended by you.",
|
||||
"You disconnected the call.",
|
||||
"You have ended the call.",
|
||||
]
|
||||
|
||||
OTHER_PARTY_HANGUP = [
|
||||
"The other party has hung up.",
|
||||
"Call ended by the other party.",
|
||||
"The other person disconnected.",
|
||||
"The call was terminated by the other party.",
|
||||
"The other person has hung up.",
|
||||
"Conversation ended by the other party.",
|
||||
"The call has been finished by the other party.",
|
||||
"The other party ended the call.",
|
||||
"Disconnected by the other party.",
|
||||
"The other person ended the call.",
|
||||
]
|
||||
|
||||
NO_ANSWER = [
|
||||
"No answer. Maybe try again later?",
|
||||
"It looks like nobody picked up.",
|
||||
"The call went unanswered.",
|
||||
"No response. Try calling again later.",
|
||||
"No one is available to answer.",
|
||||
"The call wasn't answered.",
|
||||
"Nobody answered the call.",
|
||||
"The call went unanswered. Try again soon!",
|
||||
"No answer at the moment.",
|
||||
"The call wasn't picked up.",
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
import typing
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
def is_optional(type_hint) -> bool:
|
||||
"""Checks if the type_hint is optional or not.
|
||||
|
||||
Args:
|
||||
type_hint (typing.Type): The type hint.
|
||||
|
||||
Returns:
|
||||
bool: Whether the type hint is optional or not.
|
||||
"""
|
||||
|
||||
origin = typing.get_origin(type_hint)
|
||||
|
||||
if origin is typing.Union:
|
||||
args = typing.get_args(type_hint)
|
||||
return type(None) in args
|
||||
|
||||
if isinstance(type_hint, commands.Greedy):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import Callable, Iterable, TypeVar, Generator
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
def deduplicate(i: Iterable[T], key: Callable[[T], U] = lambda i: i) -> list[T]:
|
||||
"""Deduplicates an Iterable.
|
||||
|
||||
Args:
|
||||
i (Iterable[T]): The iterable with items to deduplicate.
|
||||
key (Callable[[T], U], optional): The key by which to deduplicate. Defaults to
|
||||
`lambda i: i`.
|
||||
|
||||
Returns:
|
||||
list[T]: A list with the deduplicated items.
|
||||
"""
|
||||
|
||||
found_keys = set()
|
||||
|
||||
result = []
|
||||
|
||||
for item in i:
|
||||
k = key(item)
|
||||
if k not in found_keys:
|
||||
result.append(item)
|
||||
found_keys.add(k)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def chunks(i: Iterable[T], size: int) -> Generator[list[T], None, None]:
|
||||
"""Chunk an iterable.
|
||||
|
||||
Args:
|
||||
i (Iterable[T]): The iterable.
|
||||
size (int): The size of each chunk.
|
||||
|
||||
Returns:
|
||||
Generator[list[T], None, None]: The iterable's chunks.
|
||||
"""
|
||||
|
||||
if size <= 0:
|
||||
raise ValueError("Chunks size can't be lower than 1.")
|
||||
|
||||
chunk = []
|
||||
|
||||
for item in i:
|
||||
chunk.append(item)
|
||||
|
||||
if len(chunk) == size:
|
||||
yield chunk
|
||||
chunk = []
|
||||
|
||||
if len(chunk) > 0:
|
||||
yield chunk
|
||||
@@ -1,104 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS "call" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ended_at" TIMESTAMPTZ
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "server" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"prefix" VARCHAR(3) NOT NULL DEFAULT 'p.'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "channel" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"enabled" BOOL,
|
||||
"webhook_id" BIGINT,
|
||||
"webhook_url" TEXT,
|
||||
"call_id" INT REFERENCES "call" ("id") ON DELETE CASCADE,
|
||||
"server_id" BIGINT NOT NULL REFERENCES "server" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"username" VARCHAR(32) NOT NULL,
|
||||
"is_masked" BOOL NOT NULL DEFAULT False,
|
||||
"is_moderator" BOOL NOT NULL DEFAULT False
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "profile" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"avatar_url" TEXT NOT NULL,
|
||||
"name" VARCHAR(32) NOT NULL,
|
||||
"color" INT,
|
||||
"server_id" BIGINT REFERENCES "server" ("id") ON DELETE CASCADE,
|
||||
"user_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "uid_profile_user_id_903480" UNIQUE ("user_id", "server_id")
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "calluserchannelrelationship" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"is_masked" BOOL NOT NULL,
|
||||
"mask" BIGINT NOT NULL,
|
||||
"call_id" INT NOT NULL REFERENCES "call" ("id") ON DELETE CASCADE,
|
||||
"channel_id" BIGINT NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||
"profile_id" INT NOT NULL REFERENCES "profile" ("id") ON DELETE CASCADE,
|
||||
"user_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
CONSTRAINT "uid_callusercha_call_id_fad9b8" UNIQUE ("call_id", "user_id", "channel_id")
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "message" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"mask" BIGINT,
|
||||
"was_deleted" BOOL NOT NULL DEFAULT False,
|
||||
"propagation_id" BIGINT,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"call_id" INT NOT NULL REFERENCES "call" ("id") ON DELETE CASCADE,
|
||||
"channel_id" BIGINT NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||
"profile_id" INT NOT NULL REFERENCES "profile" ("id") ON DELETE CASCADE,
|
||||
"propagation_channel_id" BIGINT REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||
"replying_to_id" BIGINT REFERENCES "message" ("id") ON DELETE SET NULL,
|
||||
"user_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_message_propaga_f0803c" ON "message" ("propagation_id");
|
||||
CREATE TABLE IF NOT EXISTS "messagecontent" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"content" VARCHAR(4096) NOT NULL,
|
||||
"was_propagated" BOOL NOT NULL DEFAULT True,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"message_id" BIGINT NOT NULL REFERENCES "message" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "report" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"reason" VARCHAR(500) NOT NULL,
|
||||
"priority" INT NOT NULL,
|
||||
"status" SMALLINT NOT NULL DEFAULT 2,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"reported_by_id" BIGINT REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"call_id" INT NOT NULL UNIQUE REFERENCES "call" ("id") ON DELETE CASCADE
|
||||
);
|
||||
COMMENT ON COLUMN "report"."status" IS 'ACCEPTED: 0\nDECLINED: 1\nON_REVIEW: 2';
|
||||
CREATE TABLE IF NOT EXISTS "ban" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"notification_message_id" BIGINT,
|
||||
"reason" VARCHAR(500) NOT NULL,
|
||||
"appeal" VARCHAR(500),
|
||||
"status" SMALLINT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMPTZ,
|
||||
"banned_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"report_id" INT REFERENCES "report" ("id") ON DELETE SET NULL,
|
||||
"user_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE NO ACTION
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_ban_notific_ea7b58" ON "ban" ("notification_message_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");
|
||||
COMMENT ON COLUMN "ban"."status" IS 'VALID: 0\nON_REVIEW: 1\nAPPEAL_ACCEPTED: 2\nAPPEAL_DECLINED: 3';
|
||||
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"version" VARCHAR(255) NOT NULL,
|
||||
"app" VARCHAR(100) NOT NULL,
|
||||
"content" JSONB NOT NULL
|
||||
);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
"""
|
||||
@@ -0,0 +1,55 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS "channel" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"server_id" BIGINT NOT NULL,
|
||||
"is_enabled" BOOL NOT NULL DEFAULT True
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_channel_server__d5ded1" ON "channel" ("server_id");
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");
|
||||
COMMENT ON TABLE "channel" IS 'A channel.';
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
"is_masked" BOOL NOT NULL DEFAULT False,
|
||||
"is_moderator" BOOL NOT NULL DEFAULT False,
|
||||
"is_supporter_until" TIMESTAMPTZ
|
||||
);
|
||||
COMMENT ON TABLE "user" IS 'A user.';
|
||||
CREATE TABLE IF NOT EXISTS "report" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"message_id" BIGINT NOT NULL UNIQUE,
|
||||
"message_content" TEXT NOT NULL,
|
||||
"channel_id" BIGINT NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"resolved" BOOL NOT NULL DEFAULT False,
|
||||
"resolved_at" TIMESTAMPTZ,
|
||||
"resolved_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_report_resolve_32f036" ON "report" ("resolved");
|
||||
COMMENT ON TABLE "report" IS 'A report of a message.';
|
||||
CREATE TABLE IF NOT EXISTS "ban" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMPTZ,
|
||||
"created_by_id" BIGINT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"expired_by_id" BIGINT REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||
"report_id" INT REFERENCES "report" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");
|
||||
COMMENT ON TABLE "ban" IS 'A ban.';
|
||||
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"version" VARCHAR(255) NOT NULL,
|
||||
"app" VARCHAR(100) NOT NULL,
|
||||
"content" JSONB NOT NULL
|
||||
);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
"""
|
||||
@@ -1,15 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" ADD "leaderboard_calls_count" INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE "server" ADD "leaderboard_position" INT NOT NULL DEFAULT 0;
|
||||
CREATE INDEX "idx_server_leaderb_47c354" ON "server" ("leaderboard_position");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_server_leaderb_47c354";
|
||||
ALTER TABLE "server" DROP COLUMN "leaderboard_calls_count";
|
||||
ALTER TABLE "server" DROP COLUMN "leaderboard_position";"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" ADD "vanity_code" VARCHAR(32);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" DROP COLUMN "vanity_code";"""
|
||||
@@ -1,13 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" DROP COLUMN "appeal";
|
||||
ALTER TABLE "ban" DROP COLUMN "status";"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" ADD "appeal" VARCHAR(500);
|
||||
ALTER TABLE "ban" ADD "status" SMALLINT NOT NULL DEFAULT 0;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" ADD "found_easter_eggs" SMALLINT NOT NULL DEFAULT 0;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" DROP COLUMN "found_easter_eggs";"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "report" ALTER COLUMN "priority" TYPE DOUBLE PRECISION USING "priority"::DOUBLE PRECISION;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "report" ALTER COLUMN "priority" TYPE INT USING "priority"::INT;"""
|
||||
@@ -1,19 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "ban" ADD "unbanned_by_id" BIGINT;
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
ALTER TABLE "ban" ADD CONSTRAINT "fk_ban_user_e41857cf" FOREIGN KEY ("unbanned_by_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
||||
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_report_status_d6b157";
|
||||
ALTER TABLE "ban" DROP CONSTRAINT "fk_ban_user_e41857cf";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "ban" DROP COLUMN "unbanned_by_id";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");"""
|
||||
@@ -1,21 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_report_status_d6b157";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" ADD "reviewed_by_id" BIGINT;
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;
|
||||
ALTER TABLE "report" ADD CONSTRAINT "fk_report_user_67e68360" FOREIGN KEY ("reviewed_by_id") REFERENCES "user" ("id") ON DELETE SET NULL;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "report" DROP CONSTRAINT "fk_report_user_67e68360";
|
||||
DROP INDEX "idx_report_status_d6b157";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" DROP COLUMN "reviewed_by_id";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;"""
|
||||
@@ -1,21 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_report_status_d6b157";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "user" ADD "is_deleting" BOOL NOT NULL DEFAULT False;
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;
|
||||
CREATE INDEX "idx_user_is_dele_2b4cfd" ON "user" ("is_deleting");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_report_status_d6b157";
|
||||
DROP INDEX "idx_user_is_dele_2b4cfd";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "user" DROP COLUMN "is_deleting";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_report_status_d6b157" ON "report" ("status", "priority");;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" ALTER COLUMN "banned_by_id" DROP NOT NULL;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" ALTER COLUMN "banned_by_id" SET NOT NULL;"""
|
||||
@@ -0,0 +1,17 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" DROP CONSTRAINT "ban_user_id_fkey";"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "ban" ADD CONSTRAINT "ban_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE NO ACTION;"""
|
||||
@@ -0,0 +1,17 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" ALTER COLUMN "prefix" TYPE VARCHAR(5) USING "prefix"::VARCHAR(5);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" ALTER COLUMN "prefix" TYPE VARCHAR(3) USING "prefix"::VARCHAR(3);"""
|
||||
@@ -0,0 +1,17 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
@@ -1,17 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "calluserchannelrelationship" ADD "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
ALTER TABLE "user" ADD "leaderboard_position" INT;
|
||||
CREATE INDEX "idx_callusercha_created_e72939" ON "calluserchannelrelationship" ("created_at");
|
||||
CREATE INDEX "idx_user_leaderb_1dcee5" ON "user" ("leaderboard_position");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_callusercha_created_e72939";
|
||||
DROP INDEX "idx_user_leaderb_1dcee5";
|
||||
ALTER TABLE "user" DROP COLUMN "leaderboard_position";
|
||||
ALTER TABLE "calluserchannelrelationship" DROP COLUMN "created_at";"""
|
||||
@@ -0,0 +1,19 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" DROP COLUMN "resolved_by_id";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" ADD "resolved_by_id" BIGINT;
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" ADD "joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" DROP COLUMN "joined_at";"""
|
||||
@@ -0,0 +1,21 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" ADD "resolved_by_id" BIGINT;
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;
|
||||
ALTER TABLE "report" ADD CONSTRAINT "fk_report_user_e65f1760" FOREIGN KEY ("resolved_by_id") REFERENCES "user" ("id") ON DELETE CASCADE;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_channel_server__d940e4";
|
||||
ALTER TABLE "report" DROP CONSTRAINT "fk_report_user_e65f1760";
|
||||
DROP INDEX "idx_ban_user_id_37a9cf";
|
||||
ALTER TABLE "report" DROP COLUMN "resolved_by_id";
|
||||
CREATE INDEX "idx_ban_user_id_37a9cf" ON "ban" ("user_id", "expires_at");;
|
||||
CREATE INDEX "idx_channel_server__d940e4" ON "channel" ("server_id", "is_enabled");;"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" ADD "leaderboard_calls_count" INT;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" DROP COLUMN "leaderboard_calls_count";"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
CREATE INDEX "idx_ban_created_490013" ON "ban" ("created_at");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_ban_created_490013";"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
CREATE INDEX "idx_user_usernam_9987ab" ON "user" ("username");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_user_usernam_9987ab";"""
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" ADD "name" VARCHAR(100);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "server" DROP COLUMN "name";"""
|
||||
+91
-394
@@ -1,406 +1,103 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import enum
|
||||
|
||||
from tortoise import models, fields, indexes
|
||||
|
||||
import discord
|
||||
from discord import webhook
|
||||
|
||||
import aiohttp
|
||||
|
||||
from payphone.core import config, constants
|
||||
from payphone.cogs.easter import EasterEggCommand
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
id: int = fields.BigIntField(primary_key=True)
|
||||
username: str = fields.CharField(max_length=32, db_index=True)
|
||||
is_masked: bool = fields.BooleanField(default=False)
|
||||
is_moderator: bool = fields.BooleanField(default=False)
|
||||
is_deleting: bool = fields.BooleanField(default=False, db_index=True)
|
||||
found_easter_eggs: int = fields.IntEnumField(EasterEggCommand, default=0)
|
||||
profiles: fields.BackwardFKRelation["Profile"]
|
||||
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
|
||||
leaderboard_position: int | None = fields.IntField(
|
||||
default=None, null=True, db_index=True
|
||||
)
|
||||
leaderboard_calls_count: int | None = fields.IntField(default=None, null=True)
|
||||
joined_at: datetime = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Call(models.Model):
|
||||
started_at = fields.DatetimeField(auto_now_add=True, db_index=True)
|
||||
ended_at = fields.DatetimeField(null=True, default=None)
|
||||
|
||||
channels: fields.BackwardFKRelation["Channel"]
|
||||
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
|
||||
report: fields.BackwardOneToOneRelation["Report"]
|
||||
messages: fields.BackwardFKRelation["Message"]
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user: fields.ForeignKeyRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", related_name="profiles", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
user_id: int
|
||||
server: fields.ForeignKeyNullableRelation["Server"] = fields.ForeignKeyField(
|
||||
"models.Server",
|
||||
related_name="profiles",
|
||||
null=True,
|
||||
on_delete=fields.OnDelete.CASCADE,
|
||||
)
|
||||
avatar_url: str = fields.TextField()
|
||||
name: str = fields.CharField(max_length=32)
|
||||
color: int | None = fields.IntField(null=True, default=None)
|
||||
|
||||
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
|
||||
|
||||
@property
|
||||
def jump_url(self) -> str:
|
||||
"""The URL that'll open the user in Discord.
|
||||
|
||||
Returns:
|
||||
str: The URL.
|
||||
"""
|
||||
|
||||
return f"discord://-/users/{self.user_id}"
|
||||
|
||||
@property
|
||||
def link(self) -> str:
|
||||
"""A markdown link that opens the user in Discord.
|
||||
|
||||
Returns:
|
||||
str: The markdown link.
|
||||
"""
|
||||
|
||||
return f"[{self.name}]({self.jump_url})"
|
||||
|
||||
@property
|
||||
def embed_author(self) -> discord.EmbedAuthor:
|
||||
"""Returns this profile as an EmbedAuthor object.
|
||||
|
||||
Returns:
|
||||
discord.EmbedAuthor: The EmbedAuthor object.
|
||||
"""
|
||||
|
||||
return discord.EmbedAuthor(name=self.name, icon_url=self.avatar_url)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("user_id", "server_id")]
|
||||
|
||||
|
||||
class Server(models.Model):
|
||||
id: int = fields.BigIntField(primary_key=True)
|
||||
name: str = fields.CharField(max_length=100, null=True, default=None)
|
||||
prefix: str = fields.CharField(max_length=5, default="p.")
|
||||
leaderboard_position: int = fields.IntField(default=0, db_index=True)
|
||||
leaderboard_calls_count: int = fields.IntField(default=0)
|
||||
vanity_code: str = fields.CharField(max_length=32, null=True, default=None)
|
||||
channels: fields.BackwardFKRelation["Channel"]
|
||||
profiles: fields.BackwardFKRelation["Profile"]
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
id = fields.BigIntField(primary_key=True)
|
||||
server: fields.ForeignKeyRelation[Server] = fields.ForeignKeyField(
|
||||
"models.Server", related_name="channels", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
call: fields.ForeignKeyNullableRelation["Call"] = fields.ForeignKeyField(
|
||||
"models.Call", null=True, related_name="channels"
|
||||
)
|
||||
enabled: bool | None = fields.BooleanField(null=True, default=None)
|
||||
webhook_id: int | None = fields.BigIntField(null=True)
|
||||
webhook_url: str | None = fields.TextField(null=True)
|
||||
|
||||
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
|
||||
messages: fields.BackwardFKRelation["Message"]
|
||||
propagated_messages: fields.BackwardFKRelation["Message"]
|
||||
|
||||
def webhook(self, session: aiohttp.ClientSession) -> webhook.Webhook | None:
|
||||
"""Initializes the channel's webhook instance.
|
||||
|
||||
Args:
|
||||
session (aiohttp.ClientSession): An initialized aiohttp client session.
|
||||
|
||||
Returns:
|
||||
webhook.Webhook | None: The webhook, or None if not initialized.
|
||||
"""
|
||||
|
||||
if self.webhook_url is None:
|
||||
return None
|
||||
|
||||
return webhook.Webhook.from_url(
|
||||
self.webhook_url, session=session, bot_token=config.TOKEN
|
||||
)
|
||||
|
||||
class Meta:
|
||||
index = [("server_id", "enabled")]
|
||||
|
||||
|
||||
class CallUserChannelRelationship(models.Model):
|
||||
call: fields.ForeignKeyRelation["Call"] = fields.ForeignKeyField(
|
||||
"models.Call", related_name="call_rss", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
user: fields.ForeignKeyRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", related_name="call_rss", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
profile: fields.ForeignKeyRelation["Profile"] = fields.ForeignKeyField(
|
||||
"models.Profile", related_name="call_rss", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
channel: fields.ForeignKeyRelation["Channel"] = fields.ForeignKeyField(
|
||||
"models.Channel", related_name="call_rss", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
is_masked: bool = fields.BooleanField()
|
||||
mask: int = fields.BigIntField()
|
||||
created_at: datetime = fields.DatetimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
@property
|
||||
def mask_name(self) -> str:
|
||||
"""Returns the mask's name.
|
||||
|
||||
Returns:
|
||||
str: The mask's name.
|
||||
"""
|
||||
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
return mask.name.replace("_", " ").lower().title()
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""Returns the displayed name.
|
||||
|
||||
Returns:
|
||||
str: The displayed name.
|
||||
"""
|
||||
|
||||
if self.is_masked:
|
||||
return self.mask_name
|
||||
|
||||
return self.profile.name
|
||||
|
||||
@property
|
||||
def display_avatar(self) -> str:
|
||||
"""Returns the displayed avatar URL.
|
||||
|
||||
Returns:
|
||||
str: The displayed avatar URL.
|
||||
"""
|
||||
|
||||
if self.is_masked:
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
return f"https://payphone.nyeki.dev/assets/masks/{mask.name.lower().replace("_", "-")}.png"
|
||||
|
||||
return self.profile.avatar_url
|
||||
|
||||
@property
|
||||
def mask_emoji(self) -> str:
|
||||
"""Returns the mask's emoji.
|
||||
|
||||
Returns:
|
||||
str: The mask's emoji (<:name:id>).
|
||||
"""
|
||||
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
mask_name = mask.name.lower().replace("_", "")
|
||||
|
||||
return f"<:{mask_name}:{self.mask}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = [("call_id", "user_id", "channel_id")]
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
id = fields.BigIntField(primary_key=True)
|
||||
call: fields.ForeignKeyRelation["Call"] = fields.ForeignKeyField(
|
||||
"models.Call", related_name="messages", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
call_id: int
|
||||
channel: fields.ForeignKeyRelation["Channel"] = fields.ForeignKeyField(
|
||||
"models.Channel", related_name="messages", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
channel_id: int
|
||||
user = fields.ForeignKeyRelation("models.User", on_delete=fields.OnDelete.CASCADE)
|
||||
user_id: int
|
||||
profile: fields.ForeignKeyRelation["Profile"] = fields.ForeignKeyField(
|
||||
"models.Profile", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
profile_id: int
|
||||
replying_to: fields.ForeignKeyNullableRelation["Message"] = fields.ForeignKeyField(
|
||||
"models.Message",
|
||||
related_name="replies",
|
||||
on_delete=fields.OnDelete.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
mask = fields.BigIntField(null=True)
|
||||
was_deleted = fields.BooleanField(default=False)
|
||||
propagation_id = fields.BigIntField(null=True, db_index=True)
|
||||
propagation_channel: fields.ForeignKeyNullableRelation["Channel"] = (
|
||||
fields.ForeignKeyField(
|
||||
"models.Channel", related_name="propagated_messages", null=True
|
||||
)
|
||||
)
|
||||
propagation_channel_id: int
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
contents: fields.BackwardFKRelation["MessageContent"]
|
||||
|
||||
@property
|
||||
def mask_name(self) -> str:
|
||||
"""Returns the mask's name.
|
||||
|
||||
Returns:
|
||||
str: The mask's name.
|
||||
"""
|
||||
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
return mask.name.replace("_", " ").lower().title()
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""Returns the displayed name.
|
||||
|
||||
Returns:
|
||||
str: The displayed name.
|
||||
"""
|
||||
|
||||
if self.mask is not None:
|
||||
return self.mask_name
|
||||
|
||||
return self.profile.name
|
||||
|
||||
@property
|
||||
def display_avatar(self) -> str:
|
||||
"""Returns the displayed avatar URL.
|
||||
|
||||
Returns:
|
||||
str: The displayed avatar URL.
|
||||
"""
|
||||
|
||||
if self.mask is not None:
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
return f"https://payphone.nyeki.dev/assets/masks/{mask.name.lower().replace("_", "-")}.png"
|
||||
|
||||
return self.profile.avatar_url
|
||||
|
||||
@property
|
||||
def mask_emoji(self) -> str:
|
||||
"""Returns the mask's emoji.
|
||||
|
||||
Returns:
|
||||
str: The mask's emoji (<:name:id>).
|
||||
"""
|
||||
|
||||
try:
|
||||
mask = constants.Mask(self.mask)
|
||||
|
||||
except ValueError:
|
||||
mask = constants.CatMask(self.mask)
|
||||
|
||||
mask_name = mask.name.lower().replace("_", "")
|
||||
|
||||
return f"<:{mask_name}:{self.mask}>"
|
||||
|
||||
def jump_url(self, is_this_channel: bool = True) -> str | None:
|
||||
"""Returns the jump URL for this message.
|
||||
|
||||
Args:
|
||||
is_this_channel (bool, optional): Whether the message sent in this channel or not.
|
||||
Defaults to True.
|
||||
|
||||
Returns:
|
||||
str | None: The URL, or None if the message was not propagated.
|
||||
"""
|
||||
|
||||
if not is_this_channel:
|
||||
return f"https://discord.com/channels/{self.channel.server_id}/{self.channel.id}/{self.id}"
|
||||
|
||||
if self.propagation_id is not None:
|
||||
return f"https://discord.com/channels/{self.propagation_channel.server_id}/{self.propagation_channel.id}/{self.propagation_id}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class MessageContent(models.Model):
|
||||
message = fields.ForeignKeyField(
|
||||
"models.Message", related_name="contents", on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
message_id: int
|
||||
content = fields.CharField(max_length=4096)
|
||||
was_propagated = fields.BooleanField(default=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class ReportStatus(enum.IntEnum):
|
||||
ACCEPTED = 0
|
||||
DECLINED = 1
|
||||
ON_REVIEW = 2
|
||||
from tortoise import models, fields
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
call: fields.OneToOneRelation["Call"] = fields.OneToOneField(
|
||||
"models.Call", related_name="report"
|
||||
)
|
||||
reported_by: fields.ForeignKeyNullableRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", null=True, on_delete=fields.OnDelete.CASCADE
|
||||
)
|
||||
reviewed_by: fields.ForeignKeyNullableRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", null=True, on_delete=fields.OnDelete.SET_NULL, related_name="reports_reviewed"
|
||||
)
|
||||
reason: str = fields.CharField(max_length=500)
|
||||
priority: float = fields.FloatField()
|
||||
status: ReportStatus = fields.IntEnumField(
|
||||
ReportStatus, default=ReportStatus.ON_REVIEW, db_index=True
|
||||
)
|
||||
created_at: datetime = fields.DatetimeField(auto_now_add=True)
|
||||
"""
|
||||
A report of a message.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
indexes = [indexes.Index(fields=("status", "priority"))]
|
||||
message_id = fields.BigIntField(unique=True)
|
||||
message_content = fields.TextField()
|
||||
channel_id = fields.BigIntField()
|
||||
user_id = fields.BigIntField()
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
resolved = fields.BooleanField(default=False, index=True)
|
||||
resolved_at = fields.DatetimeField(null=True)
|
||||
resolved_by = fields.ForeignKeyField("models.User", null=True)
|
||||
|
||||
|
||||
class UserFlag(enum.IntFlag):
|
||||
"""
|
||||
Flags for a user.
|
||||
"""
|
||||
|
||||
NONE = 0
|
||||
MODERATOR = 1
|
||||
SUPPORTER = 2
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
"""
|
||||
A user.
|
||||
"""
|
||||
|
||||
id = fields.BigIntField(pk=True)
|
||||
|
||||
is_masked = fields.BooleanField(default=False)
|
||||
is_moderator = fields.BooleanField(default=False)
|
||||
is_supporter_until = fields.DatetimeField(null=True)
|
||||
|
||||
@property
|
||||
def flags(self) -> UserFlag:
|
||||
"""
|
||||
The flags of the user.
|
||||
"""
|
||||
|
||||
flags = UserFlag.NONE
|
||||
|
||||
if self.is_moderator:
|
||||
flags |= UserFlag.MODERATOR
|
||||
|
||||
if (
|
||||
self.is_supporter_until is not None
|
||||
and self.is_supporter_until > datetime.now(timezone.utc)
|
||||
):
|
||||
flags |= UserFlag.SUPPORTER
|
||||
|
||||
return flags
|
||||
|
||||
|
||||
class Ban(models.Model):
|
||||
user_id: int = fields.BigIntField()
|
||||
report: fields.ForeignKeyNullableRelation["Report"] = fields.ForeignKeyField(
|
||||
"models.Report",
|
||||
related_name="bans",
|
||||
null=True,
|
||||
on_delete=fields.OnDelete.SET_NULL,
|
||||
)
|
||||
notification_message_id: int = fields.BigIntField(null=True, db_index=True)
|
||||
reason: str = fields.CharField(max_length=500)
|
||||
banned_by: fields.ForeignKeyRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", related_name="bans_created", null=True
|
||||
)
|
||||
unbanned_by: fields.ForeignKeyNullableRelation["User"] = fields.ForeignKeyField(
|
||||
"models.User", related_name="bans_removed", null=True, default=None
|
||||
)
|
||||
created_at: datetime = fields.DatetimeField(auto_now_add=True, db_index=True)
|
||||
expires_at: datetime | None = fields.DatetimeField(null=True)
|
||||
"""
|
||||
A ban.
|
||||
"""
|
||||
|
||||
user_id = fields.BigIntField()
|
||||
report = fields.ForeignKeyField("models.Report", null=True)
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
expires_at = fields.DatetimeField(null=True)
|
||||
created_by = fields.ForeignKeyField("models.User", related_name="created_bans")
|
||||
expired_by = fields.ForeignKeyField("models.User", related_name="bans", null=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [indexes.Index(fields=("user_id", "expires_at"))]
|
||||
"""
|
||||
Meta information.
|
||||
"""
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["user_id", "expires_at"]),
|
||||
]
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
"""
|
||||
A channel.
|
||||
"""
|
||||
|
||||
id = fields.BigIntField(pk=True)
|
||||
server_id = fields.BigIntField(index=True)
|
||||
|
||||
is_enabled = fields.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Meta information.
|
||||
"""
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["server_id", "is_enabled"]),
|
||||
]
|
||||
|
||||
+26
-2
@@ -1,9 +1,33 @@
|
||||
import tortoise
|
||||
|
||||
from payphone.core import config
|
||||
from payphone.core import constants
|
||||
|
||||
|
||||
CONFIG = {
|
||||
"connections": {
|
||||
"default": {
|
||||
"engine": "tortoise.backends.asyncpg",
|
||||
"credentials": {
|
||||
"host": "localhost" if constants.IS_DEBUG else "postgresql",
|
||||
"port": "5432",
|
||||
"user": "payphone",
|
||||
"password": "payphone",
|
||||
"database": "payphone",
|
||||
},
|
||||
"minsize": 5,
|
||||
"maxsize": 100,
|
||||
},
|
||||
},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["payphone.db.models", "aerich.models"],
|
||||
"default_connection": "default",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def init() -> None:
|
||||
"""Initialize the database and it's connections."""
|
||||
|
||||
await tortoise.Tortoise.init(config.TORTOISE_ORM, use_tz=True)
|
||||
await tortoise.Tortoise.init(CONFIG, use_tz=True)
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
from typing import Tuple
|
||||
|
||||
import discord
|
||||
|
||||
from payphone.db import models
|
||||
|
||||
|
||||
async def get_channel(channel: discord.TextChannel) -> models.Channel:
|
||||
"""Returns a channel from the database, creating or updating it if it already exists.
|
||||
|
||||
Args:
|
||||
channel (discord.TextChannel): The channel to fetch.
|
||||
|
||||
Returns:
|
||||
models.Channel: The channel row.
|
||||
"""
|
||||
|
||||
row, _ = await models.Channel.update_or_create(
|
||||
id=channel.id, server=channel.guild.id
|
||||
)
|
||||
|
||||
channel_name = channel.name.replace("discord", "d").replace("wumpus", "w")
|
||||
webhook_name = f"Payphone #{channel_name}"
|
||||
|
||||
if row.webhook_id is None:
|
||||
webhook = await channel.create_webhook(
|
||||
name=webhook_name, reason="First call started in channel."
|
||||
)
|
||||
|
||||
row.webhook_id = webhook.id
|
||||
row.webhook_url = webhook.url
|
||||
|
||||
await row.save(update_fields=["webhook_id", "webhook_url"])
|
||||
|
||||
else:
|
||||
found = False
|
||||
|
||||
for webhook in await channel.webhooks():
|
||||
if webhook.id != row.webhook_id:
|
||||
if webhook.token is None:
|
||||
await webhook.delete(reason="Webhook is unaccessible by Payphone.")
|
||||
break
|
||||
|
||||
else:
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
webhook = await channel.create_webhook(
|
||||
name=webhook_name, reason="Previous webhook was deleted."
|
||||
)
|
||||
|
||||
row.webhook_id = webhook.id
|
||||
row.webhook_url = webhook.url
|
||||
|
||||
await row.save(update_fields=["webhook_id", "webhook_url"])
|
||||
|
||||
return row
|
||||
|
||||
|
||||
async def get_user(
|
||||
user: discord.User | discord.Member,
|
||||
) -> Tuple[models.User, models.Profile]:
|
||||
"""Returns a user from the database, creating or updating it if it already exists.
|
||||
|
||||
Args:
|
||||
user (discord.User | discord.Member): The user or member to fetch.
|
||||
|
||||
Returns:
|
||||
Tuple[models.User, models.Profile]: The user and profile.
|
||||
"""
|
||||
|
||||
user_row, _ = await models.User.update_or_create(
|
||||
{"username": user.name}, id=user.id
|
||||
)
|
||||
|
||||
profile_row, _ = await models.Profile.update_or_create(
|
||||
{
|
||||
"avatar_url": user.display_avatar.url,
|
||||
"name": user.display_name or user.name,
|
||||
},
|
||||
user=user_row,
|
||||
server_id=user.guild.id if isinstance(user, discord.Member) else None,
|
||||
)
|
||||
|
||||
return (user_row, profile_row)
|
||||
@@ -0,0 +1 @@
|
||||
from payphone.tasks import connect, expire
|
||||
@@ -0,0 +1,32 @@
|
||||
import asyncio
|
||||
|
||||
import random
|
||||
|
||||
from payphone.bot import Payphone
|
||||
from payphone.core import constants, messages, logging
|
||||
|
||||
|
||||
async def main():
|
||||
logging.setup("Connect")
|
||||
|
||||
bot = Payphone()
|
||||
|
||||
await bot.login(constants.TOKEN)
|
||||
|
||||
while True:
|
||||
pairs = await bot.call_handler.connect()
|
||||
|
||||
message = random.choice(messages.ANSWERED)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
bot.get_partial_messageable(channel).send(f"☎️ {message}")
|
||||
for pair in pairs
|
||||
for channel in pair
|
||||
]
|
||||
)
|
||||
|
||||
if pairs:
|
||||
logging.logger.info("Connected %s calls.", len(pairs))
|
||||
|
||||
await asyncio.sleep(1)
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
|
||||
from payphone.bot import Payphone
|
||||
from payphone.core import constants, logging
|
||||
|
||||
|
||||
async def main():
|
||||
logging.setup("Expire")
|
||||
|
||||
bot = Payphone()
|
||||
|
||||
await bot.login(constants.TOKEN)
|
||||
|
||||
while True:
|
||||
pairs = await bot.call_handler.expire()
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
bot.get_partial_messageable(channel).send(
|
||||
"☎️ The call was hung up because nobody responded."
|
||||
)
|
||||
for pair in pairs
|
||||
for channel in pair
|
||||
]
|
||||
)
|
||||
|
||||
if pairs:
|
||||
logging.logger.info("Expired %s calls.", len(pairs))
|
||||
|
||||
await asyncio.sleep(1)
|
||||
Reference in New Issue
Block a user