mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[V3] Report Tool (#1281)
* Okay, let's fix the issues here hopefully. * This is working now * Unfinished, and needs a lot of testing. * more work * working * minor thing to remove * improve i18n and usage feedback
This commit is contained in:
parent
487d256f46
commit
8756b22f5a
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@ -22,6 +22,8 @@ redbot/core/sentry_setup.py @Kowlin @tekulvw
|
|||||||
redbot/core/utils/chat_formatting.py @tekulvw
|
redbot/core/utils/chat_formatting.py @tekulvw
|
||||||
redbot/core/utils/mod.py @palmtree5
|
redbot/core/utils/mod.py @palmtree5
|
||||||
redbot/core/utils/data_converter.py @mikeshardmind
|
redbot/core/utils/data_converter.py @mikeshardmind
|
||||||
|
redbot/core/utils/antispam.py @mikeshardmind
|
||||||
|
redbot/core/utils/tunnel.py @mikeshardmind
|
||||||
|
|
||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
@ -40,6 +42,7 @@ redbot/cogs/modlog/* @palmtree5
|
|||||||
redbot/cogs/streams/* @Twentysix26 @palmtree5
|
redbot/cogs/streams/* @Twentysix26 @palmtree5
|
||||||
redbot/cogs/trivia/* @Tobotimus
|
redbot/cogs/trivia/* @Tobotimus
|
||||||
redbot/cogs/dataconverter/* @mikeshardmind
|
redbot/cogs/dataconverter/* @mikeshardmind
|
||||||
|
redbot/cogs/reports/* @mikeshardmind
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/* @tekulvw @palmtree5
|
docs/* @tekulvw @palmtree5
|
||||||
|
|||||||
6
redbot/cogs/reports/__init__.py
Normal file
6
redbot/cogs/reports/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from redbot.core.bot import Red
|
||||||
|
from .reports import Reports
|
||||||
|
|
||||||
|
|
||||||
|
def setup(bot: Red):
|
||||||
|
bot.add_cog(Reports(bot))
|
||||||
378
redbot/cogs/reports/reports.py
Normal file
378
redbot/cogs/reports/reports.py
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from typing import Union
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from redbot.core import Config, checks, RedContext
|
||||||
|
from redbot.core.utils.chat_formatting import pagify, box
|
||||||
|
from redbot.core.utils.antispam import AntiSpam
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
from redbot.core.i18n import CogI18n
|
||||||
|
from redbot.core.utils.tunnel import Tunnel
|
||||||
|
|
||||||
|
|
||||||
|
_ = CogI18n("Reports", __file__)
|
||||||
|
|
||||||
|
log = logging.getLogger("red.reports")
|
||||||
|
|
||||||
|
|
||||||
|
class Reports:
|
||||||
|
|
||||||
|
default_guild_settings = {
|
||||||
|
"output_channel": None,
|
||||||
|
"active": False,
|
||||||
|
"next_ticket": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
default_report = {
|
||||||
|
'report': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# This can be made configureable later if it
|
||||||
|
# becomes an issue.
|
||||||
|
# Intervals should be a list of tuples in the form
|
||||||
|
# (period: timedelta, max_frequency: int)
|
||||||
|
# see redbot/core/utils/antispam.py for more details
|
||||||
|
|
||||||
|
intervals = [
|
||||||
|
(timedelta(seconds=5), 1),
|
||||||
|
(timedelta(minutes=5), 3),
|
||||||
|
(timedelta(hours=1), 10),
|
||||||
|
(timedelta(days=1), 24)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, bot: Red):
|
||||||
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(
|
||||||
|
self, 78631113035100160, force_registration=True)
|
||||||
|
self.config.register_guild(**self.default_guild_settings)
|
||||||
|
self.config.register_custom('REPORT', **self.default_report)
|
||||||
|
self.antispam = {}
|
||||||
|
self.user_cache = []
|
||||||
|
self.tunnel_store = {}
|
||||||
|
# (guild, ticket#):
|
||||||
|
# {'tun': Tunnel, 'msgs': List[int]}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tunnels(self):
|
||||||
|
return [
|
||||||
|
x['tun'] for x in self.tunnel_store.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
def __unload(self):
|
||||||
|
for tun in self.tunnels:
|
||||||
|
tun.close()
|
||||||
|
|
||||||
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
|
@commands.guild_only()
|
||||||
|
@commands.group(name="reportset")
|
||||||
|
async def reportset(self, ctx: RedContext):
|
||||||
|
"""
|
||||||
|
settings for reports
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
|
@reportset.command(name="output")
|
||||||
|
async def setoutput(self, ctx: RedContext, channel: discord.TextChannel):
|
||||||
|
"""sets the output channel"""
|
||||||
|
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||||
|
await ctx.send(_("Report Channel Set."))
|
||||||
|
|
||||||
|
@checks.admin_or_permissions(manage_guild=True)
|
||||||
|
@reportset.command(name="toggleactive")
|
||||||
|
async def report_toggle(self, ctx: RedContext):
|
||||||
|
"""Toggles whether the Reporting tool is enabled or not"""
|
||||||
|
|
||||||
|
active = await self.config.guild(ctx.guild).active()
|
||||||
|
active = not active
|
||||||
|
await self.config.guild(ctx.guild).active.set(active)
|
||||||
|
if active:
|
||||||
|
await ctx.send(_("Reporting now enabled"))
|
||||||
|
else:
|
||||||
|
await ctx.send(_("Reporting disabled."))
|
||||||
|
|
||||||
|
async def internal_filter(self, m: discord.Member, mod=False, perms=None):
|
||||||
|
ret = False
|
||||||
|
if mod:
|
||||||
|
guild = m.guild
|
||||||
|
admin_role = discord.utils.get(
|
||||||
|
guild.roles, id=await self.bot.db.guild(guild).admin_role()
|
||||||
|
)
|
||||||
|
mod_role = discord.utils.get(
|
||||||
|
guild.roles, id=await self.bot.db.guild(guild).mod_role()
|
||||||
|
)
|
||||||
|
ret |= any(r in m.roles for r in (mod_role, admin_role))
|
||||||
|
if perms:
|
||||||
|
ret |= m.guild_permissions >= perms
|
||||||
|
# The following line is for consistency with how perms are handled
|
||||||
|
# in Red, though I'm not sure it makse sense to use here.
|
||||||
|
ret |= await self.bot.is_owner(m)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def discover_guild(self, author: discord.User, *,
|
||||||
|
mod: bool=False,
|
||||||
|
permissions: Union[discord.Permissions, dict]={},
|
||||||
|
prompt: str=""):
|
||||||
|
"""
|
||||||
|
discovers which of shared guilds between the bot
|
||||||
|
and provided user based on conditions (mod or permissions is an or)
|
||||||
|
|
||||||
|
prompt is for providing a user prompt for selection
|
||||||
|
"""
|
||||||
|
shared_guilds = []
|
||||||
|
if isinstance(permissions, discord.Permissions):
|
||||||
|
perms = permissions
|
||||||
|
else:
|
||||||
|
permissions = discord.Permissions(**perms)
|
||||||
|
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
x = guild.get_member(author.id)
|
||||||
|
if x is not None:
|
||||||
|
if await self.internal_filter(x, mod, perms):
|
||||||
|
shared_guilds.append(guild)
|
||||||
|
if len(shared_guilds) == 0:
|
||||||
|
raise ValueError("No Qualifying Shared Guilds")
|
||||||
|
return
|
||||||
|
if len(shared_guilds) == 1:
|
||||||
|
return shared_guilds[0]
|
||||||
|
output = ""
|
||||||
|
guilds = sorted(shared_guilds, key=lambda g: g.name)
|
||||||
|
for i, guild in enumerate(guilds, 1):
|
||||||
|
output += "{}: {}\n".format(i, guild.name)
|
||||||
|
output += "\n{}".format(prompt)
|
||||||
|
|
||||||
|
for page in pagify(output, delims=["\n"]):
|
||||||
|
dm = await author.send(box(page))
|
||||||
|
|
||||||
|
def pred(m):
|
||||||
|
return m.author == author and m.channel == dm.channel
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = await self.bot.wait_for(
|
||||||
|
'message', check=pred, timeout=45
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await author.send(
|
||||||
|
_("You took too long to select. Try again later.")
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = int(message.content.strip())
|
||||||
|
guild = guilds[message - 1]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
await author.send(_("That wasn't a valid choice."))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return guild
|
||||||
|
|
||||||
|
async def send_report(self, msg: discord.Message, guild: discord.Guild):
|
||||||
|
|
||||||
|
author = guild.get_member(msg.author.id)
|
||||||
|
report = msg.clean_content
|
||||||
|
avatar = author.avatar_url
|
||||||
|
|
||||||
|
em = discord.Embed(description=report)
|
||||||
|
em.set_author(
|
||||||
|
name=_('Report from {0.display_name}').format(author),
|
||||||
|
icon_url=avatar
|
||||||
|
)
|
||||||
|
|
||||||
|
ticket_number = await self.config.guild(guild).next_ticket()
|
||||||
|
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||||
|
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||||
|
|
||||||
|
channel_id = await self.config.guild(guild).output_channel()
|
||||||
|
channel = guild.get_channel(channel_id)
|
||||||
|
if channel is not None:
|
||||||
|
try:
|
||||||
|
await channel.send(embed=em)
|
||||||
|
except (discord.Forbidden, discord.HTTPException):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
|
||||||
|
{'user_id': author.id, 'report': report}
|
||||||
|
)
|
||||||
|
return ticket_number
|
||||||
|
|
||||||
|
@commands.group(name="report", invoke_without_command=True)
|
||||||
|
async def report(self, ctx: RedContext):
|
||||||
|
"Follow the prompts to make a report"
|
||||||
|
author = ctx.author
|
||||||
|
guild = ctx.guild
|
||||||
|
if guild is None:
|
||||||
|
guild = await self.discover_guild(
|
||||||
|
author,
|
||||||
|
prompt=_("Select a server to make a report in by number.")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await ctx.message.delete()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
if guild is None:
|
||||||
|
return
|
||||||
|
g_active = await self.config.guild(guild).active()
|
||||||
|
if not g_active:
|
||||||
|
return await author.send(
|
||||||
|
_("Reporting has not been enabled for this server")
|
||||||
|
)
|
||||||
|
if guild.id not in self.antispam:
|
||||||
|
self.antispam[guild.id] = {}
|
||||||
|
if author.id not in self.antispam[guild.id]:
|
||||||
|
self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
|
||||||
|
if self.antispam[guild.id][author.id].spammy:
|
||||||
|
return await author.send(
|
||||||
|
_("You've sent a few too many of these recently. "
|
||||||
|
"Contact a server admin to resolve this, or try again "
|
||||||
|
"later.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if author.id in self.user_cache:
|
||||||
|
return await author.send(
|
||||||
|
_("Finish making your prior report "
|
||||||
|
"before making an additional one")
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.guild:
|
||||||
|
try:
|
||||||
|
await ctx.message.delete()
|
||||||
|
except (discord.Forbidden, discord.HTTPException):
|
||||||
|
pass
|
||||||
|
self.user_cache.append(author.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dm = await author.send(
|
||||||
|
_("Please respond to this message with your Report."
|
||||||
|
"\nYour report should be a single message")
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(
|
||||||
|
_("This requires DMs enabled.")
|
||||||
|
)
|
||||||
|
self.user_cache.remove(author.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
def pred(m):
|
||||||
|
return m.author == author and m.channel == dm.channel
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = await self.bot.wait_for(
|
||||||
|
'message', check=pred, timeout=180
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await author.send(
|
||||||
|
_("You took too long. Try again later.")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
val = await self.send_report(message, guild)
|
||||||
|
if val is None:
|
||||||
|
await author.send(
|
||||||
|
_("There was an error sending your report.")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await author.send(
|
||||||
|
_("Your report was submitted. (Ticket #{})").format(val)
|
||||||
|
)
|
||||||
|
self.antispam[guild.id][author.id].stamp()
|
||||||
|
|
||||||
|
self.user_cache.remove(author.id)
|
||||||
|
|
||||||
|
async def on_raw_reaction_add(self, payload):
|
||||||
|
"""
|
||||||
|
oh dear....
|
||||||
|
"""
|
||||||
|
if not str(payload.emoji) == "\N{NEGATIVE SQUARED CROSS MARK}":
|
||||||
|
return
|
||||||
|
|
||||||
|
_id = payload.message_id
|
||||||
|
t = next(filter(
|
||||||
|
lambda x: _id in x[1]['msgs'],
|
||||||
|
self.tunnel_store.items()
|
||||||
|
), None)
|
||||||
|
|
||||||
|
if t is None:
|
||||||
|
return
|
||||||
|
tun = t[1]['tun']
|
||||||
|
if payload.user_id in [x.id for x in tun.members]:
|
||||||
|
await tun.react_close(
|
||||||
|
uid=payload.user_id,
|
||||||
|
message=_("{closer} has closed the correspondence")
|
||||||
|
)
|
||||||
|
self.tunnel_store.pop(t[0], None)
|
||||||
|
|
||||||
|
async def on_message(self, message: discord.Message):
|
||||||
|
for k, v in self.tunnel_store.items():
|
||||||
|
topic = _("Re: ticket# {1} in {0.name}").format(*k)
|
||||||
|
# Tunnels won't forward unintended messages, this is safe
|
||||||
|
msgs = await v['tun'].communicate(message=message, topic=topic)
|
||||||
|
if msgs:
|
||||||
|
self.tunnel_store[k]['msgs'] = msgs
|
||||||
|
|
||||||
|
@checks.mod_or_permissions(manage_members=True)
|
||||||
|
@report.command(name='interact')
|
||||||
|
async def response(self, ctx, ticket_number: int):
|
||||||
|
"""
|
||||||
|
opens a message tunnel between things you say in this channel
|
||||||
|
and the ticket opener's direct messages
|
||||||
|
|
||||||
|
tunnels do not persist across bot restarts
|
||||||
|
"""
|
||||||
|
|
||||||
|
# note, mod_or_permissions is an implicit guild_only
|
||||||
|
guild = ctx.guild
|
||||||
|
rec = await self.config.custom(
|
||||||
|
'REPORT', guild.id, ticket_number).report()
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = guild.get_member(rec.get('user_id'))
|
||||||
|
except KeyError:
|
||||||
|
return await ctx.send(
|
||||||
|
_("That ticket doesn't seem to exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return await ctx.send(
|
||||||
|
_("That user isn't here anymore.")
|
||||||
|
)
|
||||||
|
|
||||||
|
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
|
||||||
|
|
||||||
|
if tun is None:
|
||||||
|
return await ctx.send(
|
||||||
|
_("Either you or the user you are trying to reach already "
|
||||||
|
"has an open communication.")
|
||||||
|
)
|
||||||
|
|
||||||
|
big_topic = _(
|
||||||
|
"{who} opened a 2-way communication."
|
||||||
|
"about ticket number {ticketnum}. Anything you say or upload here "
|
||||||
|
"(8MB file size limitation on uploads) "
|
||||||
|
"will be forwarded to them until the communication is closed.\n"
|
||||||
|
"You can close a communication at any point "
|
||||||
|
"by reacting with the X to the last message recieved. "
|
||||||
|
"\nAny message succesfully forwarded with be marked with a check."
|
||||||
|
"\nTunnels are not persistent across bot restarts."
|
||||||
|
)
|
||||||
|
topic = big_topic.format(
|
||||||
|
ticketnum=ticket_number,
|
||||||
|
who=_("A moderator in `{guild.name}` has").format(guild=guild)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
m = await tun.communicate(
|
||||||
|
message=ctx.message, topic=topic, skip_message_content=True
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await ctx.send(_("User has disabled DMs."))
|
||||||
|
tun.close()
|
||||||
|
else:
|
||||||
|
self.tunnel_store[(guild, ticket_number)] = {'tun': tun, 'msgs': m}
|
||||||
|
await ctx.send(
|
||||||
|
big_topic.format(who=_("You have"), ticketnum=ticket_number)
|
||||||
|
)
|
||||||
62
redbot/core/utils/antispam.py
Normal file
62
redbot/core/utils/antispam.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Tuple, List
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
Interval = Tuple[timedelta, int]
|
||||||
|
AntiSpamInterval = namedtuple('AntiSpamInterval', ['period', 'frequency'])
|
||||||
|
|
||||||
|
|
||||||
|
class AntiSpam:
|
||||||
|
"""
|
||||||
|
Custom class which is more flexible than using discord.py's
|
||||||
|
`commands.cooldown()`
|
||||||
|
|
||||||
|
Can be intialized with a custom set of intervals
|
||||||
|
These should be provided as a list of tuples in the form
|
||||||
|
(timedelta, quantity)
|
||||||
|
|
||||||
|
Where quantity represents the maximum amount of times
|
||||||
|
something should be allowed in an interval.
|
||||||
|
"""
|
||||||
|
# TODO : Decorator interface for command check using `spammy`
|
||||||
|
# with insertion of the antispam element into context
|
||||||
|
# for manual stamping on succesful command completion
|
||||||
|
|
||||||
|
default_intervals = [
|
||||||
|
(timedelta(seconds=5), 3),
|
||||||
|
(timedelta(minutes=1), 5),
|
||||||
|
(timedelta(hours=1), 10),
|
||||||
|
(timedelta(days=1), 24)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, intervals: List[Interval]):
|
||||||
|
self.__event_timestamps = []
|
||||||
|
_itvs = intervals if intervals else self.default_intervals
|
||||||
|
self.__intervals = [
|
||||||
|
AntiSpamInterval(*x) for x in _itvs
|
||||||
|
]
|
||||||
|
self.__discard_after = max([x.period for x in self.__intervals])
|
||||||
|
|
||||||
|
def __interval_check(self, interval: AntiSpamInterval):
|
||||||
|
return len(
|
||||||
|
[t for t in self.__event_timestamps
|
||||||
|
if (t + interval.period) > datetime.utcnow()]
|
||||||
|
) >= interval.frequency
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spammy(self):
|
||||||
|
"""
|
||||||
|
use this to check if any interval criteria are met
|
||||||
|
"""
|
||||||
|
return any(self.__interval_check(x) for x in self.__intervals)
|
||||||
|
|
||||||
|
def stamp(self):
|
||||||
|
"""
|
||||||
|
Use this to mark an event that counts against the intervals
|
||||||
|
as happening now
|
||||||
|
"""
|
||||||
|
self.__event_timestamps.append(datetime.utcnow())
|
||||||
|
self.__event_timestamps = [
|
||||||
|
t for t in self.__event_timestamps
|
||||||
|
if t + self.__discard_after > datetime.utcnow()
|
||||||
|
]
|
||||||
135
redbot/core/utils/tunnel.py
Normal file
135
redbot/core/utils/tunnel.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import discord
|
||||||
|
from datetime import datetime
|
||||||
|
from redbot.core.utils.chat_formatting import pagify
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
|
||||||
|
class TunnelMeta(type):
|
||||||
|
"""
|
||||||
|
lets prevent having multiple tunnels with the same
|
||||||
|
places involved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
lockout_tuple = (
|
||||||
|
(kwargs.get('sender'), kwargs.get('origin')),
|
||||||
|
kwargs.get('recipient')
|
||||||
|
)
|
||||||
|
if not (
|
||||||
|
any(
|
||||||
|
lockout_tuple[0] == x[0]
|
||||||
|
for x in _instances.keys()
|
||||||
|
) or any(
|
||||||
|
lockout_tuple[1] == x[1]
|
||||||
|
for x in _instances.keys()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_instances[lockout_tuple] = super(
|
||||||
|
TunnelMeta, cls).__call__(*args, **kwargs)
|
||||||
|
return _instances[lockout_tuple]
|
||||||
|
elif lockout_tuple in _instances:
|
||||||
|
return _instances[lockout_tuple]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Tunnel(metaclass=TunnelMeta):
|
||||||
|
"""
|
||||||
|
A tunnel interface for messages
|
||||||
|
|
||||||
|
This will return None on init if the destination
|
||||||
|
or source + origin pair is already in use
|
||||||
|
|
||||||
|
You should close tunnels when done with them
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *,
|
||||||
|
sender: discord.Member,
|
||||||
|
origin: discord.TextChannel,
|
||||||
|
recipient: discord.User):
|
||||||
|
self.sender = sender
|
||||||
|
self.origin = origin
|
||||||
|
self.recipient = recipient
|
||||||
|
self.last_interaction = datetime.utcnow()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
lockout_tuple = ((self.sender, self.origin), self.recipient)
|
||||||
|
_instances.pop(lockout_tuple, None)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.__del__()
|
||||||
|
|
||||||
|
async def react_close(self, *, uid: int, message: str):
|
||||||
|
send_to = self.origin if uid == self.sender.id else self.sender
|
||||||
|
closer = next(filter(
|
||||||
|
lambda x: x.id == uid, (self.sender, self.recipient)), None)
|
||||||
|
await send_to.send(
|
||||||
|
message.format(closer=closer)
|
||||||
|
)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def members(self):
|
||||||
|
return (self.sender, self.recipient)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def minutes_since(self):
|
||||||
|
return (self.last_interaction - datetime.utcnow()).minutes
|
||||||
|
|
||||||
|
async def communicate(self, *,
|
||||||
|
message: discord.Message,
|
||||||
|
topic: str=None,
|
||||||
|
skip_message_content: bool=False):
|
||||||
|
if message.channel == self.origin \
|
||||||
|
and message.author == self.sender:
|
||||||
|
send_to = self.recipient
|
||||||
|
elif message.author == self.recipient \
|
||||||
|
and isinstance(message.channel, discord.DMChannel):
|
||||||
|
send_to = self.origin
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not skip_message_content:
|
||||||
|
content = "\n".join((topic, message.content)) if topic \
|
||||||
|
else message.content
|
||||||
|
else:
|
||||||
|
content = topic
|
||||||
|
|
||||||
|
attach = None
|
||||||
|
if message.attachments:
|
||||||
|
files = []
|
||||||
|
size = 0
|
||||||
|
max_size = 8 * 1024 * 1024
|
||||||
|
for a in message.attachments:
|
||||||
|
_fp = io.BytesIO()
|
||||||
|
await a.save(_fp)
|
||||||
|
size += sys.getsizeof(_fp)
|
||||||
|
if size > max_size:
|
||||||
|
await send_to.send(
|
||||||
|
"Could not forward attatchments. "
|
||||||
|
"Total size of attachments in a single "
|
||||||
|
"message must be less than 8MB."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
files.append(
|
||||||
|
discord.File(_fp, filename=a.filename)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attach = files
|
||||||
|
|
||||||
|
rets = []
|
||||||
|
for page in pagify(content):
|
||||||
|
rets.append(
|
||||||
|
await send_to.send(content, files=attach)
|
||||||
|
)
|
||||||
|
if attach:
|
||||||
|
del attach
|
||||||
|
|
||||||
|
await message.add_reaction("\N{WHITE HEAVY CHECK MARK}")
|
||||||
|
await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")
|
||||||
|
self.last_interaction = datetime.utcnow()
|
||||||
|
await rets[-1].add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")
|
||||||
|
return [rets[-1].id, message.id]
|
||||||
Loading…
x
Reference in New Issue
Block a user