1143 lines
35 KiB
Python
1143 lines
35 KiB
Python
from datetime import datetime, timezone, timedelta
|
|
|
|
import secrets
|
|
|
|
import discord
|
|
from discord import errors
|
|
from discord.ext import commands, tasks
|
|
from discord.utils import find
|
|
|
|
from tortoise import transactions, exceptions
|
|
|
|
import aiohttp
|
|
|
|
import orjson
|
|
|
|
from payphone import bot
|
|
from payphone.db import utils as dbutils, models
|
|
from payphone.cogs import base
|
|
from payphone.cogs.calls import embeds
|
|
from payphone.core import constants, checks, censorship, formatting
|
|
|
|
|
|
class Calls(base.Cog):
|
|
def __init__(self, bot_instance: bot.Payphone):
|
|
self.bot = bot_instance
|
|
|
|
def cog_unload(self) -> None:
|
|
return super().cog_unload()
|
|
|
|
@commands.command(aliases=["c"])
|
|
@commands.guild_only()
|
|
@commands.cooldown(1, 15, commands.BucketType.channel)
|
|
@checks.is_unbanned()
|
|
@transactions.atomic()
|
|
async def call(self, ctx: commands.Context) -> None:
|
|
"""
|
|
Call and connect with other people.
|
|
"""
|
|
|
|
if ctx.channel.is_nsfw():
|
|
await ctx.send("❌ Calls cannot be started in NSFW channels.")
|
|
return
|
|
|
|
user_id = ctx.author.id
|
|
channel_id = ctx.channel.id
|
|
|
|
is_on_call = await self.bot.redis.sismember(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id)
|
|
)
|
|
|
|
if is_on_call:
|
|
await ctx.reply(
|
|
f"☎️ You're already on a call! Use `{ctx.clean_prefix}hangup` to hangup."
|
|
)
|
|
return
|
|
|
|
is_searching = await self.bot.redis.sismember(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(channel_id)
|
|
)
|
|
|
|
if is_searching:
|
|
await ctx.reply(
|
|
f"☎️ You're already searching for a call! Use `{ctx.clean_prefix}hangup` to stop searching."
|
|
)
|
|
return
|
|
|
|
last_channel_calls = [
|
|
i[1]
|
|
for i in (
|
|
await models.CallUserChannelRelationship.filter(channel_id=channel_id)
|
|
.limit(5)
|
|
.distinct()
|
|
.order_by("-id")
|
|
.values_list("id", "call_id")
|
|
)
|
|
]
|
|
last_user_calls = [
|
|
i[1]
|
|
for i in (
|
|
await models.CallUserChannelRelationship.filter(user_id=user_id)
|
|
.limit(5)
|
|
.limit(5)
|
|
.distinct()
|
|
.order_by("-id")
|
|
.values_list("id", "call_id")
|
|
)
|
|
]
|
|
|
|
last_calls = set(last_user_calls + last_channel_calls)
|
|
|
|
last_calls_rss = await models.CallUserChannelRelationship.filter(
|
|
call_id__in=last_calls
|
|
)
|
|
last_calls_rss_channels_map = {}
|
|
|
|
for call_rs in last_calls_rss:
|
|
if call_rs.user_id == user_id or call_rs.channel_id == channel_id:
|
|
last_calls_rss_channels_map[call_rs.call_id] = call_rs.channel_id
|
|
|
|
last_calls_other_channels_ids = set()
|
|
|
|
for call_rs in last_calls_rss:
|
|
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
|
last_calls_other_channels_ids.add(call_rs.channel_id)
|
|
|
|
other_channel_id: int = await self.bot.redis.spop(
|
|
constants.CHANNELS_SEARCH_QUEUE
|
|
)
|
|
i = 0
|
|
|
|
while True:
|
|
# For some weird reason, sometimes it returns None and sometimes b"None".
|
|
if other_channel_id is None or other_channel_id == b"None":
|
|
other_channel_id = None
|
|
break
|
|
|
|
# self.bot.redis returns the ID as a bytes string.
|
|
other_channel_id = int(other_channel_id)
|
|
|
|
if i == 5:
|
|
break
|
|
|
|
i += 1
|
|
|
|
if other_channel_id in last_calls_other_channels_ids:
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
|
)
|
|
|
|
other_channel_id = await self.bot.redis.spop(
|
|
constants.CHANNELS_SEARCH_QUEUE
|
|
)
|
|
|
|
if other_channel_id is None:
|
|
break
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
channel = await dbutils.get_channel(ctx.channel)
|
|
|
|
if other_channel_id is not None:
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id), str(other_channel_id)
|
|
)
|
|
|
|
msg_phone_answered = f"☎️ {secrets.choice(constants.MSGS_PHONE_ANSWERED)}"
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel_id).send(
|
|
msg_phone_answered
|
|
)
|
|
|
|
call = await models.Call.create(is_nsfw=ctx.channel.is_nsfw())
|
|
|
|
user, profile = await dbutils.get_user(ctx.author)
|
|
|
|
await models.CallUserChannelRelationship.create(
|
|
call=call,
|
|
channel_id=channel_id,
|
|
user=user,
|
|
profile=profile,
|
|
is_masked=user.is_masked,
|
|
mask=secrets.choice(constants.MASKS_LIST).value,
|
|
)
|
|
|
|
await models.Channel.filter(
|
|
id__in=[other_channel_id, channel.id]
|
|
).update(call=call)
|
|
|
|
except errors.DiscordException:
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET,
|
|
str(channel_id),
|
|
str(other_channel_id),
|
|
)
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(channel_id)
|
|
)
|
|
msg_phone_answered = ""
|
|
|
|
await ctx.send(
|
|
(
|
|
f"**TIP**: {secrets.choice(constants.TIPS).replace("{prefix}", ctx.clean_prefix)}\n"
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_CALLING)}\n"
|
|
)
|
|
+ msg_phone_answered
|
|
)
|
|
|
|
else:
|
|
await self.bot.redis.sadd(constants.CHANNELS_SEARCH_QUEUE, str(channel_id))
|
|
|
|
await ctx.send(
|
|
(
|
|
f"**TIP**: {secrets.choice(constants.TIPS).replace("{prefix}", ctx.clean_prefix)}\n"
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_CALLING)}\n"
|
|
)
|
|
)
|
|
|
|
@commands.command(aliases=["s"])
|
|
@commands.guild_only()
|
|
@commands.cooldown(1, 15, commands.BucketType.channel)
|
|
@checks.is_unbanned()
|
|
@transactions.atomic()
|
|
async def skip(self, ctx: commands.Context) -> None:
|
|
"""
|
|
Hang up the current call and search for another.
|
|
"""
|
|
|
|
if ctx.channel.is_nsfw():
|
|
await ctx.send("❌ Calls cannot be started in NSFW channels.")
|
|
return
|
|
|
|
user_id = ctx.author.id
|
|
channel_id = ctx.channel.id
|
|
|
|
is_on_call = await self.bot.redis.sismember(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id)
|
|
)
|
|
|
|
if not is_on_call:
|
|
await ctx.send("❌ You're not in a call!")
|
|
return
|
|
|
|
call = await models.Call.get(channels__id=channel_id).prefetch_related(
|
|
"channels"
|
|
)
|
|
|
|
await self.bot.redis.sadd(constants.CALL_IDS_TO_FLUSH, call.id)
|
|
|
|
call.ended_at = datetime.now(timezone.utc)
|
|
await call.save()
|
|
|
|
channel = find(lambda c: c.id == channel_id, call.channels)
|
|
other_channel = find(lambda c: c.id != channel_id, call.channels)
|
|
|
|
if other_channel is not None:
|
|
other_channel.call = None
|
|
await other_channel.save()
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel.id).send(
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_YOU)}"
|
|
)
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET,
|
|
*(
|
|
[str(channel_id)]
|
|
+ ([str(other_channel.id)] if other_channel is not None else [])
|
|
),
|
|
)
|
|
|
|
last_channel_calls = [
|
|
i[1]
|
|
for i in (
|
|
await models.CallUserChannelRelationship.filter(channel_id=channel_id)
|
|
.limit(5)
|
|
.distinct()
|
|
.order_by("-id")
|
|
.values_list("id", "call_id")
|
|
)
|
|
]
|
|
last_user_calls = [
|
|
i[1]
|
|
for i in (
|
|
await models.CallUserChannelRelationship.filter(user_id=user_id)
|
|
.limit(5)
|
|
.limit(5)
|
|
.distinct()
|
|
.order_by("-id")
|
|
.values_list("id", "call_id")
|
|
)
|
|
]
|
|
|
|
last_calls = set(last_user_calls + last_channel_calls)
|
|
|
|
last_calls_rss = await models.CallUserChannelRelationship.filter(
|
|
call_id__in=last_calls
|
|
)
|
|
last_calls_rss_channels_map = {}
|
|
|
|
for call_rs in last_calls_rss:
|
|
if call_rs.user_id == user_id or call_rs.channel_id == channel_id:
|
|
last_calls_rss_channels_map[call_rs.call_id] = call_rs.channel_id
|
|
|
|
last_calls_other_channels_ids = set()
|
|
|
|
for call_rs in last_calls_rss:
|
|
if last_calls_rss_channels_map[call_rs.call_id] != call_rs.channel_id:
|
|
last_calls_other_channels_ids.add(call_rs.channel_id)
|
|
|
|
other_channel_id: int = await self.bot.redis.spop(
|
|
constants.CHANNELS_SEARCH_QUEUE
|
|
)
|
|
i = 0
|
|
|
|
while True:
|
|
# For some weird reason, sometimes it returns None and sometimes b"b'None'".
|
|
if other_channel_id is None or other_channel_id == b"b'None'":
|
|
other_channel_id = None
|
|
break
|
|
|
|
# self.bot.redis returns the ID as a bytes string.
|
|
other_channel_id = int(other_channel_id)
|
|
|
|
if i == 5:
|
|
break
|
|
|
|
i += 1
|
|
|
|
if other_channel_id in last_calls_other_channels_ids:
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(other_channel_id)
|
|
)
|
|
|
|
other_channel_id = await self.bot.redis.spop(
|
|
constants.CHANNELS_SEARCH_QUEUE
|
|
)
|
|
|
|
if other_channel_id is None:
|
|
break
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
if other_channel_id is not None:
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id), str(other_channel_id)
|
|
)
|
|
|
|
msg_phone_answered = f"☎️ {secrets.choice(constants.MSGS_PHONE_ANSWERED)}"
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel_id).send(
|
|
msg_phone_answered
|
|
)
|
|
|
|
call = await models.Call.create(is_nsfw=ctx.channel.is_nsfw())
|
|
|
|
user, profile = await dbutils.get_user(ctx.author)
|
|
|
|
await models.CallUserChannelRelationship.create(
|
|
call=call,
|
|
channel_id=channel_id,
|
|
user=user,
|
|
profile=profile,
|
|
is_masked=user.is_masked,
|
|
mask=secrets.choice(constants.MASKS_LIST).value,
|
|
)
|
|
|
|
await models.Channel.filter(
|
|
id__in=[other_channel_id, channel.id]
|
|
).update(call=call)
|
|
|
|
except errors.DiscordException:
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET,
|
|
str(channel_id),
|
|
str(other_channel_id),
|
|
)
|
|
await self.bot.redis.sadd(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(channel_id)
|
|
)
|
|
msg_phone_answered = ""
|
|
|
|
await ctx.send(
|
|
(
|
|
f"**TIP**: {secrets.choice(constants.TIPS).replace("{prefix}", ctx.clean_prefix)}\n"
|
|
f"☎️ You skipped the call.\n"
|
|
)
|
|
+ (
|
|
msg_phone_answered
|
|
if msg_phone_answered
|
|
else f"☎️ {secrets.choice(constants.MSGS_PHONE_CALLING)}\n"
|
|
)
|
|
)
|
|
|
|
else:
|
|
await self.bot.redis.sadd(constants.CHANNELS_SEARCH_QUEUE, str(channel_id))
|
|
|
|
channel.call = None
|
|
await channel.save()
|
|
|
|
await ctx.send(
|
|
(
|
|
f"**TIP**: {secrets.choice(constants.TIPS).replace("{prefix}", ctx.clean_prefix)}\n"
|
|
f"☎️ You skipped the call.\n"
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_CALLING)}\n"
|
|
)
|
|
)
|
|
|
|
@commands.command(aliases=["hang", "h"], extras={"help_name": "Hang Up"})
|
|
@commands.guild_only()
|
|
@checks.is_unbanned()
|
|
@transactions.atomic()
|
|
async def hangup(self, ctx: commands.Context) -> None:
|
|
"""
|
|
Hang up the phone.
|
|
"""
|
|
|
|
channel_id = ctx.channel.id
|
|
|
|
is_on_call = await self.bot.redis.sismember(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id)
|
|
)
|
|
|
|
if is_on_call:
|
|
channel = await dbutils.get_channel(ctx.channel)
|
|
|
|
try:
|
|
call = await models.Call.get(id=channel.call_id).prefetch_related(
|
|
"channels"
|
|
)
|
|
|
|
except exceptions.DoesNotExist:
|
|
await self.bot.redis.srem(constants.CHANNELS_ONCALL_SET, channel_id)
|
|
|
|
await ctx.send(f"☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_ME)}")
|
|
|
|
return
|
|
|
|
await self.bot.redis.sadd(constants.CALL_IDS_TO_FLUSH, call.id)
|
|
|
|
other_channel: models.Channel = find(
|
|
lambda c: c.id != channel_id, call.channels
|
|
)
|
|
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET,
|
|
*(
|
|
[str(channel_id)]
|
|
+ ([str(other_channel.id)] if other_channel is not None else [])
|
|
),
|
|
)
|
|
|
|
if other_channel is not None:
|
|
try:
|
|
await (await self.bot.get_or_fetch_channel(other_channel.id)).send(
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_YOU)}"
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
other_channel.call = None
|
|
|
|
await ctx.send(f"☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_ME)}")
|
|
|
|
channel.call = None
|
|
|
|
await models.Channel.bulk_update(
|
|
[channel] + ([other_channel] if other_channel is not None else []),
|
|
["call_id"],
|
|
)
|
|
|
|
call.ended_at = datetime.now(timezone.utc)
|
|
await call.save()
|
|
|
|
is_searching = await self.bot.redis.sismember(
|
|
constants.CHANNELS_SEARCH_QUEUE, str(channel_id)
|
|
)
|
|
|
|
if is_searching:
|
|
await self.bot.redis.srem(constants.CHANNELS_SEARCH_QUEUE, str(channel_id))
|
|
|
|
await ctx.send(f"☎️ {secrets.choice(constants.MSGS_PHONE_NO_ANSWER)}")
|
|
|
|
if not is_searching and not is_on_call:
|
|
await ctx.send(f"❌ {secrets.choice(constants.MSGS_PHONE_NOT_IN_CALL)}")
|
|
|
|
@commands.command()
|
|
@commands.guild_only()
|
|
@checks.is_unbanned()
|
|
async def report(
|
|
self, ctx: commands.Context, call_id: int | None = None, *, reason: str
|
|
) -> None:
|
|
"""
|
|
Report a call by it's ID, or the current/last call if no ID is specified.
|
|
|
|
Options:
|
|
- `[<call_id>]`: The ID of the call to report.
|
|
- `<reason>`: The reason why you're reporting the call.
|
|
"""
|
|
|
|
user, _ = await dbutils.get_user(ctx.author)
|
|
channel_id = ctx.channel.id
|
|
|
|
if call_id is None:
|
|
call_rs = (
|
|
await models.CallUserChannelRelationship.filter(channel_id=channel_id)
|
|
.order_by("-id")
|
|
.limit(1)
|
|
.first()
|
|
)
|
|
|
|
if call_rs is None:
|
|
await ctx.send("❌ No calls occurred on this channel.")
|
|
return
|
|
|
|
call_id = call_rs.call_id
|
|
|
|
call = await models.Call.get_or_none(id=call_id).prefetch_related(
|
|
"channels", "report", "call_rss"
|
|
)
|
|
|
|
if call is None:
|
|
await ctx.send(f"❌ There is no call with ID `{call_id}`.")
|
|
|
|
if call.report is not None:
|
|
await ctx.send(
|
|
"✅ This call has already been reported. Our moderator team will review it as soon as possible."
|
|
)
|
|
return
|
|
|
|
if not any(map(lambda rs: rs.user_id == user.id, call.call_rss)):
|
|
await ctx.send("❌ You have not participated in this call.")
|
|
return
|
|
|
|
reviewed_reports = await models.Report.filter(
|
|
reported_by_id=ctx.author.id,
|
|
status__in=[models.ReportStatus.ACCEPTED, models.ReportStatus.DECLINED],
|
|
)
|
|
|
|
priority = 0.5
|
|
|
|
if len(reviewed_reports) > 3:
|
|
accepted_reports = len(
|
|
[
|
|
report
|
|
for report in reviewed_reports
|
|
if report.status == models.ReportStatus.ACCEPTED
|
|
]
|
|
)
|
|
|
|
priority = accepted_reports / len(reviewed_reports)
|
|
|
|
await models.Report.create(
|
|
call_id=call_id, reported_by_id=user.id, reason=reason, priority=priority
|
|
)
|
|
|
|
message = "✅ The call has been reported."
|
|
|
|
if call.ended_at is None:
|
|
call.ended_at = datetime.now(timezone.utc)
|
|
await call.save()
|
|
|
|
await self.bot.redis.sadd(constants.CALL_IDS_TO_FLUSH, call.id)
|
|
|
|
updated_channels = []
|
|
|
|
for channel in call.channels:
|
|
channel.call = None
|
|
updated_channels.append(channel)
|
|
|
|
await models.Channel.bulk_update(updated_channels, ["call_id"])
|
|
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET,
|
|
str(updated_channels[0]),
|
|
str(updated_channels[1]),
|
|
)
|
|
|
|
message += f"\n☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_ME)}"
|
|
|
|
other_channel = find(lambda c: c.id != channel_id, updated_channels)
|
|
|
|
await self.bot.redis.srem(
|
|
constants.CHANNELS_ONCALL_SET, str(other_channel.id), str(channel_id)
|
|
)
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel.id).send(
|
|
f"☎️ {secrets.choice(constants.MSGS_PHONE_HANGUP_YOU)}"
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
await ctx.send(message)
|
|
|
|
@commands.command(aliases=["fr"], extras={"help_name": "Friend Request"})
|
|
@commands.guild_only()
|
|
@checks.is_unbanned()
|
|
@checks.is_on_call()
|
|
async def friendreq(self, ctx: commands.Context) -> None:
|
|
"""
|
|
Ask the other party to send you a friend request.
|
|
"""
|
|
|
|
user_id = ctx.author.id
|
|
channel_id = ctx.channel.id
|
|
|
|
call = await models.Call.get(channels__id=channel_id).prefetch_related(
|
|
"channels", "call_rss", "call_rss__profile"
|
|
)
|
|
|
|
other_channel = find(lambda c: c.id != channel_id, call.channels)
|
|
call_rs: models.CallUserChannelRelationship = find(
|
|
lambda rs: rs.user_id == user_id, call.call_rss
|
|
)
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel.id).send(
|
|
f"**{call_rs.mask_emoji + " " if call_rs.is_masked else ""}{call_rs.display_name}** (`{ctx.author.name}`) wants you to add them as a friend on Discord."
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
await ctx.send("⚠️ Your friend request couldn't be sent.")
|
|
return
|
|
|
|
await ctx.send("✅ Your friend request was sent!")
|
|
|
|
@commands.Cog.listener()
|
|
@commands.guild_only()
|
|
async def on_message(self, message: discord.Message) -> None:
|
|
"""Processes every message received.
|
|
|
|
Args:
|
|
message (discord.Message): The message received.
|
|
"""
|
|
|
|
if (
|
|
message.author.bot
|
|
or message.is_system()
|
|
or len(message.stickers) > 0
|
|
or not message.content
|
|
):
|
|
return
|
|
|
|
if (await self.bot.get_context(message)).valid:
|
|
return
|
|
|
|
user_id = message.author.id
|
|
channel_id = message.channel.id
|
|
|
|
is_on_call = await self.bot.redis.sismember(
|
|
constants.CHANNELS_ONCALL_SET, str(channel_id)
|
|
)
|
|
|
|
if not is_on_call:
|
|
return
|
|
|
|
is_banned = await self.bot.is_banned(user_id)
|
|
|
|
if is_banned:
|
|
return
|
|
|
|
channel = await dbutils.get_channel(message.channel)
|
|
|
|
try:
|
|
call = await models.Call.get(id=channel.call_id).prefetch_related(
|
|
"channels"
|
|
)
|
|
|
|
except exceptions.DoesNotExist:
|
|
await self.bot.redis.srem(constants.CHANNELS_ONCALL_SET, str(channel_id))
|
|
return
|
|
|
|
other_channel: models.Channel = find(
|
|
lambda c: c.id != channel_id, call.channels
|
|
)
|
|
|
|
if other_channel is None:
|
|
# BUG: The call wasn't properly set up.
|
|
return
|
|
|
|
user, profile = await dbutils.get_user(message.author)
|
|
|
|
call_rss = await models.CallUserChannelRelationship.filter(
|
|
call=call, channel=channel
|
|
).select_related("profile")
|
|
used_masks = [rs.mask for rs in call_rss]
|
|
|
|
call_rs, _ = await models.CallUserChannelRelationship.get_or_create(
|
|
{
|
|
"mask": secrets.choice(
|
|
[i for i in constants.MASKS_LIST if i not in used_masks]
|
|
),
|
|
"is_masked": user.is_masked,
|
|
"profile": profile,
|
|
},
|
|
call=call,
|
|
user=user,
|
|
channel=channel,
|
|
)
|
|
|
|
# Since .select_related() can't be used and profile was already fetched, we need to
|
|
# manually set it.
|
|
call_rs.profile = profile
|
|
|
|
blocked_word = censorship.moderate(message.content)
|
|
|
|
webhook_message = None
|
|
cleaned_content = formatting.trim(formatting.clean(message, call_rss), 2000)
|
|
|
|
replying_to_id = None
|
|
|
|
if blocked_word:
|
|
try:
|
|
await self.bot.get_partial_messageable(channel_id).send(
|
|
f"Your message wasn't delivered!\nBlocked word: `{formatting.trim(blocked_word, 10)}`."
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
prefix = await self.bot.get_prefix(server_id=other_channel.server_id)
|
|
|
|
try:
|
|
await self.bot.get_partial_messageable(other_channel.id).send(
|
|
f"**{call_rs.mask_emoji + " " if call_rs.is_masked else ""}{call_rs.display_name}** said something that's not allowed. If you think they're doing something inappropiate, use `{prefix}report`."
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
else:
|
|
async with aiohttp.ClientSession() as session:
|
|
webhook = other_channel.webhook(session)
|
|
|
|
embed = None
|
|
|
|
if message.reference:
|
|
embed, replying_message = await embeds.ReplyEmbed(
|
|
self.bot.redis, message.reference, call_id=call.id
|
|
)
|
|
|
|
if replying_message is not None:
|
|
replying_to_id = replying_message.id
|
|
|
|
try:
|
|
webhook_message = await webhook.send(
|
|
cleaned_content,
|
|
username=call_rs.display_name,
|
|
avatar_url=call_rs.display_avatar,
|
|
embed=embed,
|
|
wait=True,
|
|
)
|
|
|
|
except errors.NotFound:
|
|
other_channel.webhook_id = None
|
|
other_channel.webhook_url = None
|
|
|
|
await other_channel.save()
|
|
|
|
try:
|
|
await dbutils.get_channel(
|
|
await self.bot.get_or_fetch_channel(other_channel.id)
|
|
)
|
|
|
|
finally:
|
|
pass
|
|
|
|
pipe = self.bot.redis.pipeline()
|
|
|
|
pipe.set(constants.CALL_IS_ACTIVE % call.id, "", ex=60 * 15)
|
|
|
|
pipe.hset(
|
|
constants.CALL_MESSAGES_TEMPLATE % call.id,
|
|
message.id,
|
|
orjson.dumps(
|
|
{
|
|
"id": message.id,
|
|
"call_id": call.id,
|
|
"channel_id": channel.id,
|
|
"user_id": user.id,
|
|
"profile_id": profile.id,
|
|
"mask": call_rs.mask if call_rs.is_masked else None,
|
|
"propagation_id": webhook_message.id if webhook_message else None,
|
|
"propagation_channel_id": (
|
|
other_channel.id if webhook_message else None
|
|
),
|
|
"replying_to_id": replying_to_id,
|
|
"created_at": datetime.now(timezone.utc),
|
|
}
|
|
),
|
|
)
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % call.id,
|
|
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE % (message.id, 1),
|
|
orjson.dumps(
|
|
{
|
|
"message_id": message.id,
|
|
"content": cleaned_content,
|
|
"was_propagated": not bool(blocked_word),
|
|
"created_at": datetime.now(timezone.utc),
|
|
}
|
|
),
|
|
)
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % call.id, message.id, 1
|
|
)
|
|
|
|
if webhook_message is not None:
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call.id,
|
|
webhook_message.id,
|
|
message.id,
|
|
)
|
|
|
|
if replying_to_id is not None:
|
|
other_replies = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id, replying_to_id
|
|
)
|
|
|
|
if other_replies is None:
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
|
replying_to_id,
|
|
message.id,
|
|
)
|
|
|
|
else:
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % call.id,
|
|
replying_to_id,
|
|
other_replies.decode() + f",{message.id}",
|
|
)
|
|
|
|
await pipe.execute()
|
|
|
|
@commands.Cog.listener()
|
|
@commands.guild_only()
|
|
@transactions.atomic()
|
|
async def on_raw_message_delete(
|
|
self, payload: discord.RawMessageDeleteEvent
|
|
) -> None:
|
|
"""Processes a message deletion received.
|
|
|
|
Args:
|
|
payload (discord.RawMessageDeleteEvent): The received payload.
|
|
"""
|
|
|
|
message_id = payload.message_id
|
|
channel_id = payload.channel_id
|
|
|
|
channel = await models.Channel.get_or_none(id=channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
if channel.call_id is None:
|
|
return
|
|
|
|
message_json = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGES_TEMPLATE % channel.call_id, message_id
|
|
)
|
|
|
|
if message_json is None:
|
|
return
|
|
|
|
message_dict = orjson.loads(message_json)
|
|
|
|
message = models.Message(**message_dict)
|
|
|
|
message_dict["was_deleted"] = True
|
|
|
|
await self.bot.redis.hset(
|
|
constants.CALL_MESSAGES_TEMPLATE % channel.call_id,
|
|
message.id,
|
|
orjson.dumps(message_dict),
|
|
)
|
|
|
|
other_channel = await models.Channel.get_or_none(
|
|
id=message.propagation_channel_id
|
|
)
|
|
|
|
if other_channel is None:
|
|
return
|
|
|
|
if other_channel.call_id != message.call_id:
|
|
return
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
webhook = other_channel.webhook(session)
|
|
|
|
try:
|
|
await webhook.delete_message(message.propagation_id)
|
|
|
|
except errors.DiscordException:
|
|
# This message was already deleted.
|
|
pass
|
|
|
|
message_replies = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGE_REPLIES_TEMPLATE % channel.call_id, message.id
|
|
)
|
|
|
|
if message_replies is None:
|
|
return
|
|
|
|
for message_id in message_replies.decode().split(","):
|
|
try:
|
|
await webhook.edit_message(message_id, embed=None)
|
|
|
|
except errors.DiscordException:
|
|
pass
|
|
|
|
@commands.Cog.listener()
|
|
@commands.guild_only()
|
|
@transactions.atomic()
|
|
async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent) -> None:
|
|
"""Processes a message edit received.
|
|
|
|
Args:
|
|
payload (discord.RawMessageUpdateEvent): The event payload.
|
|
"""
|
|
|
|
if (
|
|
payload.cached_message is not None
|
|
and (
|
|
payload.cached_message.author.bot or payload.cached_message.is_system()
|
|
)
|
|
or "content" not in payload.data
|
|
):
|
|
return
|
|
|
|
message_id = payload.message_id
|
|
channel_id = payload.channel_id
|
|
content = formatting.trim(payload.data["content"], 2000)
|
|
|
|
channel = await models.Channel.get_or_none(id=channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
if channel.call_id is None:
|
|
return
|
|
|
|
message_json = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGES_TEMPLATE % channel.call_id, message_id
|
|
)
|
|
|
|
if message_json is None:
|
|
return
|
|
|
|
message = models.Message(**orjson.loads(message_json))
|
|
|
|
other_channel = await models.Channel.get(id=message.propagation_channel_id)
|
|
|
|
if other_channel.call_id != message.call_id:
|
|
return
|
|
|
|
original_message = await self.bot.get_partial_messageable(
|
|
channel_id
|
|
).fetch_message(message.id)
|
|
|
|
if original_message is None:
|
|
return
|
|
|
|
call_rss = await models.CallUserChannelRelationship.filter(
|
|
call_id=channel.call_id
|
|
).select_related("profile")
|
|
|
|
cleaned_content = formatting.clean(original_message, call_rss)
|
|
|
|
blocked_word = censorship.moderate(content)
|
|
|
|
iteration = int(
|
|
await self.bot.redis.hget(
|
|
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % channel.call_id,
|
|
message.id,
|
|
)
|
|
)
|
|
|
|
pipe = self.bot.redis.pipeline()
|
|
|
|
pipe.hincrby(
|
|
constants.CALL_MESSAGE_CONTENT_ITERATIONS_TEMPLATE % channel.call_id,
|
|
message.id,
|
|
)
|
|
pipe.hset(
|
|
constants.CALL_MESSAGE_CONTENTS_TEMPLATE % channel.call_id,
|
|
constants.CALL_MESSAGE_CONTENTS_HASH_TEMPLATE % (message.id, iteration + 1),
|
|
orjson.dumps(
|
|
{
|
|
"message_id": message.id,
|
|
"content": cleaned_content,
|
|
"was_propagated": not bool(blocked_word),
|
|
"created_at": datetime.now(timezone.utc),
|
|
}
|
|
),
|
|
)
|
|
|
|
await pipe.execute()
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
webhook = other_channel.webhook(session)
|
|
|
|
try:
|
|
await webhook.edit_message(
|
|
message.propagation_id,
|
|
content=(
|
|
cleaned_content
|
|
if blocked_word is None
|
|
else "*This message was edited and censored.*"
|
|
),
|
|
)
|
|
|
|
except errors.DiscordException:
|
|
# This message was deleted.
|
|
pass
|
|
|
|
# TODO: Edit replies.
|
|
|
|
@commands.Cog.listener()
|
|
async def on_raw_reaction_add(
|
|
self, payload: discord.RawReactionActionEvent
|
|
) -> None:
|
|
if payload.user_id == self.bot.user.id:
|
|
return
|
|
|
|
channel = await models.Channel.get_or_none(id=payload.channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
if channel.call_id is None:
|
|
return
|
|
|
|
call_id = channel.call_id
|
|
|
|
other_channel_id = None
|
|
other_channel_message_id = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call_id,
|
|
payload.message_id,
|
|
)
|
|
|
|
if other_channel_message_id is None:
|
|
other_channel_message_json = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGES_TEMPLATE % call_id, payload.message_id
|
|
)
|
|
|
|
if other_channel_message_json is None:
|
|
return
|
|
|
|
other_channel_message_dict = orjson.loads(other_channel_message_json)
|
|
|
|
if other_channel_message_dict["propagation_id"] is None:
|
|
# The message wasn't propagated
|
|
return
|
|
|
|
other_channel_id = other_channel_message_dict["propagation_channel_id"]
|
|
other_channel_message_id = other_channel_message_dict["propagation_id"]
|
|
|
|
else:
|
|
other_channel = await models.Channel.get_or_none(
|
|
call_id=call_id, id__not=payload.channel_id
|
|
)
|
|
|
|
if other_channel is not None:
|
|
other_channel_id = other_channel.id
|
|
|
|
else:
|
|
return
|
|
|
|
other_channel_message_id = int(other_channel_message_id)
|
|
|
|
channel = await self.bot.get_or_fetch_channel(other_channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
await channel.get_partial_message(other_channel_message_id).add_reaction(
|
|
payload.emoji
|
|
)
|
|
|
|
@commands.Cog.listener()
|
|
async def on_raw_reaction_remove(
|
|
self, payload: discord.RawReactionActionEvent
|
|
) -> None:
|
|
if payload.user_id == self.bot.user.id:
|
|
return
|
|
|
|
channel = await models.Channel.get_or_none(id=payload.channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
if channel.call_id is None:
|
|
return
|
|
|
|
call_id = channel.call_id
|
|
|
|
other_channel_id = None
|
|
other_channel_message_id = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGE_FROM_PROPAGATION_TEMPLATE % call_id,
|
|
payload.message_id,
|
|
)
|
|
|
|
if other_channel_message_id is None:
|
|
other_channel_message_json = await self.bot.redis.hget(
|
|
constants.CALL_MESSAGES_TEMPLATE % call_id, payload.message_id
|
|
)
|
|
|
|
if other_channel_message_json is None:
|
|
return
|
|
|
|
other_channel_message_dict = orjson.loads(other_channel_message_json)
|
|
|
|
if other_channel_message_dict["propagation_id"] is None:
|
|
# The message wasn't propagated
|
|
return
|
|
|
|
other_channel_id = other_channel_message_dict["propagation_channel_id"]
|
|
other_channel_message_id = other_channel_message_dict["propagation_id"]
|
|
|
|
else:
|
|
other_channel = await models.Channel.get_or_none(
|
|
call_id=call_id, id__not=payload.channel_id
|
|
)
|
|
|
|
if other_channel is not None:
|
|
other_channel_id = other_channel.id
|
|
|
|
else:
|
|
return
|
|
|
|
other_channel_message_id = int(other_channel_message_id)
|
|
|
|
channel = await self.bot.get_or_fetch_channel(other_channel_id)
|
|
|
|
if channel is None:
|
|
return
|
|
|
|
await channel.get_partial_message(other_channel_message_id).remove_reaction(
|
|
payload.emoji, self.bot.user
|
|
)
|
|
|
|
|
|
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(Calls(b))
|