Massive refactor/update/UX improvement dump. Closes #6.
This commit is contained in:
parent
f8e92375b0
commit
72590ec92c
@ -1,12 +1,10 @@
|
|||||||
import asyncpg
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import asyncio
|
import asyncpg
|
||||||
import os
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from pluralkit import db
|
from pluralkit import db
|
||||||
@ -136,4 +134,4 @@ def run():
|
|||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
client.run(bot_token)
|
client.run(bot_token)
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
import discord
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from pluralkit import db
|
from pluralkit import db
|
||||||
|
|
||||||
|
|
||||||
@ -71,8 +70,8 @@ class ChannelLogger:
|
|||||||
embed_set_author_name(embed, channel_name, member_name, system_name, member_avatar_url)
|
embed_set_author_name(embed, channel_name, member_name, system_name, member_avatar_url)
|
||||||
embed.set_footer(
|
embed.set_footer(
|
||||||
text="System ID: {} | Member ID: {} | Sender: {}#{} ({}) | Message ID: {}".format(system_hid, member_hid,
|
text="System ID: {} | Member ID: {} | Sender: {}#{} ({}) | Message ID: {}".format(system_hid, member_hid,
|
||||||
sender_name, sender_disc,
|
sender_name, sender_disc,
|
||||||
sender_id, message_id))
|
sender_id, message_id))
|
||||||
|
|
||||||
if message_image:
|
if message_image:
|
||||||
embed.set_thumbnail(url=message_image)
|
embed.set_thumbnail(url=message_image)
|
||||||
@ -99,6 +98,7 @@ class ChannelLogger:
|
|||||||
embed.timestamp = datetime.utcnow()
|
embed.timestamp = datetime.utcnow()
|
||||||
|
|
||||||
embed_set_author_name(embed, channel_name, member_name, system_name, member_avatar_url)
|
embed_set_author_name(embed, channel_name, member_name, system_name, member_avatar_url)
|
||||||
embed.set_footer(text="System ID: {} | Member ID: {} | Message ID: {}".format(system_hid, member_hid, message_id))
|
embed.set_footer(
|
||||||
|
text="System ID: {} | Member ID: {} | Message ID: {}".format(system_hid, member_hid, message_id))
|
||||||
|
|
||||||
await self.send_to_log_channel(log_channel, embed)
|
await self.send_to_log_channel(log_channel, embed)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
from typing import Tuple, Optional, Union
|
from typing import Tuple, Optional, Union
|
||||||
|
|
||||||
@ -10,8 +10,6 @@ from pluralkit.errors import PluralKitError
|
|||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
from pluralkit.system import System
|
from pluralkit.system import System
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.bot.commands")
|
|
||||||
|
|
||||||
|
|
||||||
def next_arg(arg_string: str) -> Tuple[str, Optional[str]]:
|
def next_arg(arg_string: str) -> Tuple[str, Optional[str]]:
|
||||||
# A basic quoted-arg parser
|
# A basic quoted-arg parser
|
||||||
@ -68,6 +66,19 @@ class CommandContext:
|
|||||||
popped, self.args = next_arg(self.args)
|
popped, self.args = next_arg(self.args)
|
||||||
return popped
|
return popped
|
||||||
|
|
||||||
|
def peek_str(self) -> Optional[str]:
|
||||||
|
if not self.args:
|
||||||
|
return None
|
||||||
|
popped, _ = next_arg(self.args)
|
||||||
|
return popped
|
||||||
|
|
||||||
|
def match(self, next) -> bool:
|
||||||
|
peeked = self.peek_str()
|
||||||
|
if peeked and peeked.lower() == next.lower():
|
||||||
|
self.pop_str()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
async def pop_system(self, error: CommandError = None) -> System:
|
async def pop_system(self, error: CommandError = None) -> System:
|
||||||
name = self.pop_str(error)
|
name = self.pop_str(error)
|
||||||
system = await utils.get_system_fuzzy(self.conn, self.client, name)
|
system = await utils.get_system_fuzzy(self.conn, self.client, name)
|
||||||
@ -103,6 +114,13 @@ class CommandContext:
|
|||||||
async def reply_warn(self, content=None, embed=None):
|
async def reply_warn(self, content=None, embed=None):
|
||||||
return await self.reply(content="\u26a0 {}".format(content or ""), embed=embed)
|
return await self.reply(content="\u26a0 {}".format(content or ""), embed=embed)
|
||||||
|
|
||||||
|
async def reply_ok_dm(self, content: str):
|
||||||
|
if isinstance(self.message.channel, discord.DMChannel):
|
||||||
|
await self.reply_ok(content="\u2705 {}".format(content or ""))
|
||||||
|
else:
|
||||||
|
await self.message.author.send(content="\u2705 {}".format(content or ""))
|
||||||
|
await self.reply_ok("DM'd!")
|
||||||
|
|
||||||
async def confirm_react(self, user: Union[discord.Member, discord.User], message: discord.Message):
|
async def confirm_react(self, user: Union[discord.Member, discord.User], message: discord.Message):
|
||||||
await message.add_reaction("\u2705") # Checkmark
|
await message.add_reaction("\u2705") # Checkmark
|
||||||
await message.add_reaction("\u274c") # Red X
|
await message.add_reaction("\u274c") # Red X
|
||||||
@ -138,6 +156,35 @@ import pluralkit.bot.commands.switch_commands
|
|||||||
import pluralkit.bot.commands.system_commands
|
import pluralkit.bot.commands.system_commands
|
||||||
|
|
||||||
|
|
||||||
|
async def command_root(ctx: CommandContext):
|
||||||
|
if ctx.match("system"):
|
||||||
|
await system_commands.system_root(ctx)
|
||||||
|
elif ctx.match("member"):
|
||||||
|
await member_commands.member_root(ctx)
|
||||||
|
elif ctx.match("link"):
|
||||||
|
await system_commands.account_link(ctx)
|
||||||
|
elif ctx.match("unlink"):
|
||||||
|
await system_commands.account_unlink(ctx)
|
||||||
|
elif ctx.match("message"):
|
||||||
|
await message_commands.message_info(ctx)
|
||||||
|
elif ctx.match("log"):
|
||||||
|
await mod_commands.set_log(ctx)
|
||||||
|
elif ctx.match("invite"):
|
||||||
|
await misc_commands.invite_link(ctx)
|
||||||
|
elif ctx.match("export"):
|
||||||
|
await misc_commands.export(ctx)
|
||||||
|
elif ctx.match("switch"):
|
||||||
|
await switch_commands.switch_root(ctx)
|
||||||
|
elif ctx.match("token"):
|
||||||
|
await api_commands.token_root(ctx)
|
||||||
|
elif ctx.match("import"):
|
||||||
|
await import_commands.import_root(ctx)
|
||||||
|
elif ctx.match("help"):
|
||||||
|
await misc_commands.help_root(ctx)
|
||||||
|
else:
|
||||||
|
raise CommandError("Unknown command {}. For a list of commands, type `pk;help commands`.".format(ctx.pop_str()))
|
||||||
|
|
||||||
|
|
||||||
async def run_command(ctx: CommandContext, func):
|
async def run_command(ctx: CommandContext, func):
|
||||||
# lol nested try
|
# lol nested try
|
||||||
try:
|
try:
|
||||||
@ -150,71 +197,20 @@ async def run_command(ctx: CommandContext, func):
|
|||||||
await ctx.reply(content=content, embed=embed)
|
await ctx.reply(content=content, embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def command_dispatch(client: discord.Client, message: discord.Message, conn) -> bool:
|
async def command_dispatch(client: discord.Client, message: discord.Message, conn) -> bool:
|
||||||
prefix = "^(pk(;|!)|<@{}> )".format(client.user.id)
|
prefix = "^(pk(;|!)|<@{}> )".format(client.user.id)
|
||||||
commands = [
|
regex = re.compile(prefix, re.IGNORECASE)
|
||||||
(r"system (new|register|create|init)", system_commands.new_system),
|
|
||||||
(r"system set", system_commands.system_set),
|
|
||||||
(r"system (name|rename)", system_commands.system_name),
|
|
||||||
(r"system description", system_commands.system_description),
|
|
||||||
(r"system avatar", system_commands.system_avatar),
|
|
||||||
(r"system tag", system_commands.system_tag),
|
|
||||||
(r"system link", system_commands.system_link),
|
|
||||||
(r"system unlink", system_commands.system_unlink),
|
|
||||||
(r"system (delete|remove|destroy|erase)", system_commands.system_delete),
|
|
||||||
(r"system", system_commands.system_info),
|
|
||||||
|
|
||||||
(r"front", system_commands.system_fronter),
|
cmd = message.content
|
||||||
(r"front history", system_commands.system_fronthistory),
|
match = regex.match(cmd)
|
||||||
(r"front percent(age)?", system_commands.system_frontpercent),
|
if match:
|
||||||
|
remaining_string = cmd[match.span()[1]:].strip()
|
||||||
(r"import tupperware", import_commands.import_tupperware),
|
ctx = CommandContext(
|
||||||
|
client=client,
|
||||||
(r"member (new|create|add|register)", member_commands.new_member),
|
message=message,
|
||||||
(r"member set", member_commands.member_set),
|
conn=conn,
|
||||||
(r"member (name|rename)", member_commands.member_name),
|
args=remaining_string
|
||||||
(r"member description", member_commands.member_description),
|
)
|
||||||
(r"member avatar", member_commands.member_avatar),
|
await run_command(ctx, command_root)
|
||||||
(r"member color", member_commands.member_color),
|
return True
|
||||||
(r"member (pronouns|pronoun)", member_commands.member_pronouns),
|
|
||||||
(r"member (birthday|birthdate)", member_commands.member_birthdate),
|
|
||||||
(r"member proxy", member_commands.member_proxy),
|
|
||||||
(r"member (delete|remove|destroy|erase)", member_commands.member_delete),
|
|
||||||
(r"member", member_commands.member_info),
|
|
||||||
|
|
||||||
(r"message", message_commands.message_info),
|
|
||||||
|
|
||||||
(r"log", mod_commands.set_log),
|
|
||||||
|
|
||||||
(r"invite", misc_commands.invite_link),
|
|
||||||
(r"export", misc_commands.export),
|
|
||||||
|
|
||||||
(r"help", misc_commands.show_help),
|
|
||||||
|
|
||||||
(r"switch move", switch_commands.switch_move),
|
|
||||||
(r"switch out", switch_commands.switch_out),
|
|
||||||
(r"switch", switch_commands.switch_member),
|
|
||||||
|
|
||||||
(r"token (refresh|expire|update)", api_commands.refresh_token),
|
|
||||||
(r"token", api_commands.get_token)
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern, func in commands:
|
|
||||||
regex = re.compile(prefix + pattern, re.IGNORECASE)
|
|
||||||
|
|
||||||
cmd = message.content
|
|
||||||
match = regex.match(cmd)
|
|
||||||
if match:
|
|
||||||
remaining_string = cmd[match.span()[1]:].strip()
|
|
||||||
|
|
||||||
ctx = CommandContext(
|
|
||||||
client=client,
|
|
||||||
message=message,
|
|
||||||
conn=conn,
|
|
||||||
args=remaining_string
|
|
||||||
)
|
|
||||||
|
|
||||||
await run_command(ctx, func)
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import logging
|
|
||||||
from discord import DMChannel
|
|
||||||
|
|
||||||
from pluralkit.bot.commands import CommandContext
|
from pluralkit.bot.commands import CommandContext
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
disclaimer = "Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`."
|
disclaimer = "Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`."
|
||||||
|
|
||||||
async def reply_dm(ctx: CommandContext, message: str):
|
|
||||||
await ctx.message.author.send(message)
|
|
||||||
|
|
||||||
if not isinstance(ctx.message.channel, DMChannel):
|
async def token_root(ctx: CommandContext):
|
||||||
await ctx.reply_ok("DM'd!")
|
if ctx.match("refresh") or ctx.match("expire") or ctx.match("invalidate") or ctx.match("update"):
|
||||||
|
await token_refresh(ctx)
|
||||||
|
else:
|
||||||
|
await token_get(ctx)
|
||||||
|
|
||||||
async def get_token(ctx: CommandContext):
|
|
||||||
|
async def token_get(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
if system.token:
|
if system.token:
|
||||||
@ -21,11 +19,13 @@ async def get_token(ctx: CommandContext):
|
|||||||
token = await system.refresh_token(ctx.conn)
|
token = await system.refresh_token(ctx.conn)
|
||||||
|
|
||||||
token_message = "Here's your API token: \n**`{}`**\n{}".format(token, disclaimer)
|
token_message = "Here's your API token: \n**`{}`**\n{}".format(token, disclaimer)
|
||||||
return await reply_dm(ctx, token_message)
|
return await ctx.reply_ok_dm(token_message)
|
||||||
|
|
||||||
async def refresh_token(ctx: CommandContext):
|
|
||||||
|
async def token_refresh(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
token = await system.refresh_token(ctx.conn)
|
token = await system.refresh_token(ctx.conn)
|
||||||
token_message = "Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\nHere's your new API token:\n**`{}`**\n{}".format(token, disclaimer)
|
token_message = "Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\nHere's your new API token:\n**`{}`**\n{}".format(
|
||||||
return await reply_dm(ctx, token_message)
|
token, disclaimer)
|
||||||
|
return await ctx.reply_ok_dm(token_message)
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import asyncio
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pluralkit.utils
|
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
async def import_root(ctx: CommandContext):
|
||||||
|
# Only one import method rn, so why not default to Tupperware?
|
||||||
|
await import_tupperware(ctx)
|
||||||
|
|
||||||
|
|
||||||
async def import_tupperware(ctx: CommandContext):
|
async def import_tupperware(ctx: CommandContext):
|
||||||
tupperware_member = ctx.message.guild.get_member(431544605209788416)
|
|
||||||
|
|
||||||
# Check if there's a Tupperware bot on the server
|
# Check if there's a Tupperware bot on the server
|
||||||
|
# Main instance of TW has that ID, at least
|
||||||
|
tupperware_member = ctx.message.guild.get_member(431544605209788416)
|
||||||
if not tupperware_member:
|
if not tupperware_member:
|
||||||
raise CommandError("This command only works in a server where the Tupperware bot is also present.")
|
raise CommandError("This command only works in a server where the Tupperware bot is also present.")
|
||||||
|
|
||||||
@ -20,8 +21,7 @@ async def import_tupperware(ctx: CommandContext):
|
|||||||
# If it doesn't, throw error
|
# If it doesn't, throw error
|
||||||
raise CommandError("This command only works in a channel where the Tupperware bot has read/send access.")
|
raise CommandError("This command only works in a channel where the Tupperware bot has read/send access.")
|
||||||
|
|
||||||
await ctx.reply(
|
await ctx.reply("Please reply to this message with `tul!list` (or the server equivalent).")
|
||||||
embed=embeds.status("Please reply to this message with `tul!list` (or the server equivalent)."))
|
|
||||||
|
|
||||||
# Check to make sure the message is sent by Tupperware, and that the Tupperware response actually belongs to the correct user
|
# Check to make sure the message is sent by Tupperware, and that the Tupperware response actually belongs to the correct user
|
||||||
def ensure_account(tw_msg):
|
def ensure_account(tw_msg):
|
||||||
@ -73,8 +73,9 @@ async def import_tupperware(ctx: CommandContext):
|
|||||||
# If this isn't the same page as last check, edit the status message
|
# If this isn't the same page as last check, edit the status message
|
||||||
if new_page != current_page:
|
if new_page != current_page:
|
||||||
last_found_time = datetime.utcnow()
|
last_found_time = datetime.utcnow()
|
||||||
await status_msg.edit(content="Multi-page member list found. Please manually scroll through all the pages. Read {}/{} pages.".format(
|
await status_msg.edit(
|
||||||
len(pages_found), total_pages))
|
content="Multi-page member list found. Please manually scroll through all the pages. Read {}/{} pages.".format(
|
||||||
|
len(pages_found), total_pages))
|
||||||
current_page = new_page
|
current_page = new_page
|
||||||
|
|
||||||
# And sleep a bit to prevent spamming the CPU
|
# And sleep a bit to prevent spamming the CPU
|
||||||
@ -91,15 +92,10 @@ async def import_tupperware(ctx: CommandContext):
|
|||||||
# Also edit the status message to indicate we're now importing, and it may take a while because there's probably a lot of members
|
# Also edit the status message to indicate we're now importing, and it may take a while because there's probably a lot of members
|
||||||
await status_msg.edit(content="All pages read. Now importing...")
|
await status_msg.edit(content="All pages read. Now importing...")
|
||||||
|
|
||||||
logger.debug("Importing from Tupperware...")
|
|
||||||
|
|
||||||
# Create new (nameless) system if there isn't any registered
|
# Create new (nameless) system if there isn't any registered
|
||||||
system = await ctx.get_system()
|
system = await ctx.get_system()
|
||||||
if system is None:
|
if system is None:
|
||||||
hid = pluralkit.utils.generate_hid()
|
system = await System.create_system(ctx.conn, ctx.message.author.id)
|
||||||
logger.debug("Creating new system (hid={})...".format(hid))
|
|
||||||
system = await db.create_system(ctx.conn, system_name=None, system_hid=hid)
|
|
||||||
await db.link_account(ctx.conn, system_id=system.id, account_id=ctx.message.author.id)
|
|
||||||
|
|
||||||
for embed in tupperware_page_embeds:
|
for embed in tupperware_page_embeds:
|
||||||
for field in embed["fields"]:
|
for field in embed["fields"]:
|
||||||
@ -133,24 +129,16 @@ async def import_tupperware(ctx: CommandContext):
|
|||||||
member_description = line
|
member_description = line
|
||||||
|
|
||||||
# Read by name - TW doesn't allow name collisions so we're safe here (prevents dupes)
|
# Read by name - TW doesn't allow name collisions so we're safe here (prevents dupes)
|
||||||
existing_member = await db.get_member_by_name(ctx.conn, system_id=system.id, member_name=name)
|
existing_member = await Member.get_member_by_name(ctx.conn, system.id, name)
|
||||||
if not existing_member:
|
if not existing_member:
|
||||||
# Or create a new member
|
# Or create a new member
|
||||||
hid = pluralkit.utils.generate_hid()
|
existing_member = await system.create_member(ctx.conn, name)
|
||||||
logger.debug("Creating new member {} (hid={})...".format(name, hid))
|
|
||||||
existing_member = await db.create_member(ctx.conn, system_id=system.id, member_name=name,
|
|
||||||
member_hid=hid)
|
|
||||||
|
|
||||||
# Save the new stuff in the DB
|
# Save the new stuff in the DB
|
||||||
logger.debug("Updating fields...")
|
await existing_member.set_proxy_tags(ctx.conn, member_prefix, member_suffix)
|
||||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="prefix", value=member_prefix)
|
await existing_member.set_avatar(ctx.conn, member_avatar)
|
||||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="suffix", value=member_suffix)
|
await existing_member.set_birthdate(ctx.conn, member_birthdate)
|
||||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="avatar_url",
|
await existing_member.set_description(ctx.conn, member_description)
|
||||||
value=member_avatar)
|
|
||||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="birthday",
|
|
||||||
value=member_birthdate)
|
|
||||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="description",
|
|
||||||
value=member_description)
|
|
||||||
|
|
||||||
await ctx.reply_ok(
|
await ctx.reply_ok(
|
||||||
"System information imported. Try using `pk;system` now.\nYou should probably remove your members from Tupperware to avoid double-posting.")
|
"System information imported. Try using `pk;system` now.\nYou should probably remove your members from Tupperware to avoid double-posting.")
|
||||||
|
@ -3,25 +3,74 @@ from pluralkit.bot import help
|
|||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
from pluralkit.errors import PluralKitError
|
from pluralkit.errors import PluralKitError
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
async def member_root(ctx: CommandContext):
|
||||||
|
if ctx.match("new") or ctx.match("create") or ctx.match("add") or ctx.match("register"):
|
||||||
|
await new_member(ctx)
|
||||||
|
elif ctx.match("help"):
|
||||||
|
await ctx.reply(help.member_commands)
|
||||||
|
elif ctx.match("set"):
|
||||||
|
await member_set(ctx)
|
||||||
|
# TODO "pk;member list"
|
||||||
|
|
||||||
|
if not ctx.has_next():
|
||||||
|
raise CommandError("Must pass a subcommand. For a list of subcommands, type `pk;member help`.")
|
||||||
|
|
||||||
|
await specific_member_root(ctx)
|
||||||
|
|
||||||
|
|
||||||
async def member_info(ctx: CommandContext):
|
async def specific_member_root(ctx: CommandContext):
|
||||||
member = await ctx.pop_member(
|
member = await ctx.pop_member(system_only=False)
|
||||||
error=CommandError("You must pass a member name or ID.", help=help.lookup_member), system_only=False)
|
|
||||||
|
if ctx.has_next():
|
||||||
|
# Following commands operate on members only in the caller's own system
|
||||||
|
# error if not, to make sure you can't destructively edit someone else's member
|
||||||
|
system = await ctx.ensure_system()
|
||||||
|
if not member.system == system.id:
|
||||||
|
raise CommandError("Member must be in your own system.")
|
||||||
|
|
||||||
|
if ctx.match("name") or ctx.match("rename"):
|
||||||
|
await member_name(ctx, member)
|
||||||
|
elif ctx.match("description"):
|
||||||
|
await member_description(ctx, member)
|
||||||
|
elif ctx.match("avatar") or ctx.match("icon"):
|
||||||
|
await member_avatar(ctx, member)
|
||||||
|
elif ctx.match("proxy") or ctx.match("tags"):
|
||||||
|
await member_proxy(ctx, member)
|
||||||
|
elif ctx.match("pronouns") or ctx.match("pronoun"):
|
||||||
|
await member_pronouns(ctx, member)
|
||||||
|
elif ctx.match("color") or ctx.match("colour"):
|
||||||
|
await member_color(ctx, member)
|
||||||
|
elif ctx.match("birthday") or ctx.match("birthdate"):
|
||||||
|
await member_birthdate(ctx, member)
|
||||||
|
elif ctx.match("delete") or ctx.match("remove") or ctx.match("destroy") or ctx.match("erase"):
|
||||||
|
await member_delete(ctx, member)
|
||||||
|
elif ctx.match("help"):
|
||||||
|
await ctx.reply(help.member_commands)
|
||||||
|
else:
|
||||||
|
raise CommandError(
|
||||||
|
"Unknown subcommand {}. For a list of all commands, type `pk;member help`".format(ctx.pop_str()))
|
||||||
|
else:
|
||||||
|
# Basic lookup
|
||||||
|
await member_info(ctx, member)
|
||||||
|
|
||||||
|
|
||||||
|
async def member_info(ctx: CommandContext, member: Member):
|
||||||
await ctx.reply(embed=await pluralkit.bot.embeds.member_card(ctx.conn, member))
|
await ctx.reply(embed=await pluralkit.bot.embeds.member_card(ctx.conn, member))
|
||||||
|
|
||||||
|
|
||||||
async def new_member(ctx: CommandContext):
|
async def new_member(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
if not ctx.has_next():
|
if not ctx.has_next():
|
||||||
raise CommandError("You must pass a name for the new member.", help=help.add_member)
|
raise CommandError("You must pass a name for the new member.")
|
||||||
|
|
||||||
new_name = ctx.remaining()
|
new_name = ctx.remaining()
|
||||||
|
|
||||||
existing_member = await Member.get_member_by_name(ctx.conn, system.id, new_name)
|
existing_member = await Member.get_member_by_name(ctx.conn, system.id, new_name)
|
||||||
if existing_member:
|
if existing_member:
|
||||||
msg = await ctx.reply_warn("There is already a member with this name, with the ID `{}`. Do you want to create a duplicate member anyway?".format(existing_member.hid))
|
msg = await ctx.reply_warn(
|
||||||
|
"There is already a member with this name, with the ID `{}`. Do you want to create a duplicate member anyway?".format(
|
||||||
|
existing_member.hid))
|
||||||
if not await ctx.confirm_react(ctx.message.author, msg):
|
if not await ctx.confirm_react(ctx.message.author, msg):
|
||||||
raise CommandError("Member creation cancelled.")
|
raise CommandError("Member creation cancelled.")
|
||||||
|
|
||||||
@ -37,18 +86,19 @@ async def new_member(ctx: CommandContext):
|
|||||||
|
|
||||||
async def member_set(ctx: CommandContext):
|
async def member_set(ctx: CommandContext):
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
"`pk;member set` has been retired. Please use the new member modifying commands: `pk;member [name|description|avatar|color|pronouns|birthdate]`.")
|
"`pk;member set` has been retired. Please use the new member modifying commands. Type `pk;member help` for a list.")
|
||||||
|
|
||||||
|
|
||||||
async def member_name(ctx: CommandContext):
|
async def member_name(ctx: CommandContext, member: Member):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
new_name = ctx.pop_str(CommandError("You must pass a new member name."))
|
||||||
new_name = ctx.pop_str(CommandError("You must pass a new member name.", help=help.edit_member))
|
|
||||||
|
|
||||||
# Warn if there's a member by the same name already
|
# Warn if there's a member by the same name already
|
||||||
existing_member = await Member.get_member_by_name(ctx.conn, system.id, new_name)
|
existing_member = await Member.get_member_by_name(ctx.conn, system.id, new_name)
|
||||||
if existing_member:
|
if existing_member and existing_member.id != member.id:
|
||||||
msg = await ctx.reply_warn("There is already a member with this name, with the ID `{}`. Do you want to rename this member anyway? This will result in two members with the same name.".format(existing_member.hid))
|
msg = await ctx.reply_warn(
|
||||||
|
"There is already another member with this name, with the ID `{}`. Do you want to rename this member anyway? This will result in two members with the same name.".format(
|
||||||
|
existing_member.hid))
|
||||||
if not await ctx.confirm_react(ctx.message.author, msg):
|
if not await ctx.confirm_react(ctx.message.author, msg):
|
||||||
raise CommandError("Member renaming cancelled.")
|
raise CommandError("Member renaming cancelled.")
|
||||||
|
|
||||||
@ -56,27 +106,28 @@ async def member_name(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("Member name updated.")
|
await ctx.reply_ok("Member name updated.")
|
||||||
|
|
||||||
if len(new_name) < 2 and not system.tag:
|
if len(new_name) < 2 and not system.tag:
|
||||||
await ctx.reply_warn("This member's new name is under 2 characters, and thus cannot be proxied. To prevent this, use a longer member name, or add a system tag.")
|
await ctx.reply_warn(
|
||||||
|
"This member's new name is under 2 characters, and thus cannot be proxied. To prevent this, use a longer member name, or add a system tag.")
|
||||||
elif len(new_name) > 32:
|
elif len(new_name) > 32:
|
||||||
exceeds_by = len(new_name) - 32
|
exceeds_by = len(new_name) - 32
|
||||||
await ctx.reply_warn("This member's new name is longer than 32 characters, and thus cannot be proxied. To prevent this, shorten the member name by {} characters.".format(exceeds_by))
|
await ctx.reply_warn(
|
||||||
|
"This member's new name is longer than 32 characters, and thus cannot be proxied. To prevent this, shorten the member name by {} characters.".format(
|
||||||
|
exceeds_by))
|
||||||
elif len(new_name) > system.get_member_name_limit():
|
elif len(new_name) > system.get_member_name_limit():
|
||||||
exceeds_by = len(new_name) - system.get_member_name_limit()
|
exceeds_by = len(new_name) - system.get_member_name_limit()
|
||||||
await ctx.reply_warn("This member's new name, when combined with the system tag `{}`, is longer than 32 characters, and thus cannot be proxied. To prevent this, shorten the name or system tag by at least {} characters.".format(system.tag, exceeds_by))
|
await ctx.reply_warn(
|
||||||
|
"This member's new name, when combined with the system tag `{}`, is longer than 32 characters, and thus cannot be proxied. To prevent this, shorten the name or system tag by at least {} characters.".format(
|
||||||
|
system.tag, exceeds_by))
|
||||||
|
|
||||||
|
|
||||||
async def member_description(ctx: CommandContext):
|
async def member_description(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
|
||||||
new_description = ctx.remaining() or None
|
new_description = ctx.remaining() or None
|
||||||
|
|
||||||
await member.set_description(ctx.conn, new_description)
|
await member.set_description(ctx.conn, new_description)
|
||||||
await ctx.reply_ok("Member description {}.".format("updated" if new_description else "cleared"))
|
await ctx.reply_ok("Member description {}.".format("updated" if new_description else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def member_avatar(ctx: CommandContext):
|
async def member_avatar(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
|
||||||
new_avatar_url = ctx.remaining() or None
|
new_avatar_url = ctx.remaining() or None
|
||||||
|
|
||||||
if new_avatar_url:
|
if new_avatar_url:
|
||||||
@ -88,53 +139,42 @@ async def member_avatar(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("Member avatar {}.".format("updated" if new_avatar_url else "cleared"))
|
await ctx.reply_ok("Member avatar {}.".format("updated" if new_avatar_url else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def member_color(ctx: CommandContext):
|
async def member_color(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
|
||||||
new_color = ctx.remaining() or None
|
new_color = ctx.remaining() or None
|
||||||
|
|
||||||
await member.set_color(ctx.conn, new_color)
|
await member.set_color(ctx.conn, new_color)
|
||||||
await ctx.reply_ok("Member color {}.".format("updated" if new_color else "cleared"))
|
await ctx.reply_ok("Member color {}.".format("updated" if new_color else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def member_pronouns(ctx: CommandContext):
|
async def member_pronouns(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
|
||||||
new_pronouns = ctx.remaining() or None
|
new_pronouns = ctx.remaining() or None
|
||||||
|
|
||||||
await member.set_pronouns(ctx.conn, new_pronouns)
|
await member.set_pronouns(ctx.conn, new_pronouns)
|
||||||
await ctx.reply_ok("Member pronouns {}.".format("updated" if new_pronouns else "cleared"))
|
await ctx.reply_ok("Member pronouns {}.".format("updated" if new_pronouns else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def member_birthdate(ctx: CommandContext):
|
async def member_birthdate(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
|
||||||
new_birthdate = ctx.remaining() or None
|
new_birthdate = ctx.remaining() or None
|
||||||
|
|
||||||
await member.set_birthdate(ctx.conn, new_birthdate)
|
await member.set_birthdate(ctx.conn, new_birthdate)
|
||||||
await ctx.reply_ok("Member birthdate {}.".format("updated" if new_birthdate else "cleared"))
|
await ctx.reply_ok("Member birthdate {}.".format("updated" if new_birthdate else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def member_proxy(ctx: CommandContext):
|
async def member_proxy(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.member_proxy))
|
|
||||||
|
|
||||||
if not ctx.has_next():
|
if not ctx.has_next():
|
||||||
prefix, suffix = None, None
|
prefix, suffix = None, None
|
||||||
else:
|
else:
|
||||||
# Sanity checking
|
# Sanity checking
|
||||||
example = ctx.remaining()
|
example = ctx.remaining()
|
||||||
if "text" not in example:
|
if "text" not in example:
|
||||||
raise CommandError("Example proxy message must contain the string 'text'.", help=help.member_proxy)
|
raise CommandError("Example proxy message must contain the string 'text'. For help, type `pk;help proxy`.")
|
||||||
|
|
||||||
if example.count("text") != 1:
|
if example.count("text") != 1:
|
||||||
raise CommandError("Example proxy message must contain the string 'text' exactly once.",
|
raise CommandError("Example proxy message must contain the string 'text' exactly once. For help, type `pk;help proxy`.")
|
||||||
help=help.member_proxy)
|
|
||||||
|
|
||||||
# Extract prefix and suffix
|
# Extract prefix and suffix
|
||||||
prefix = example[:example.index("text")].strip()
|
prefix = example[:example.index("text")].strip()
|
||||||
suffix = example[example.index("text") + 4:].strip()
|
suffix = example[example.index("text") + 4:].strip()
|
||||||
logger.debug("Matched prefix '{}' and suffix '{}'".format(prefix, suffix))
|
|
||||||
|
|
||||||
# DB stores empty strings as None, make that work
|
# DB stores empty strings as None, make that work
|
||||||
if not prefix:
|
if not prefix:
|
||||||
@ -144,13 +184,11 @@ async def member_proxy(ctx: CommandContext):
|
|||||||
|
|
||||||
async with ctx.conn.transaction():
|
async with ctx.conn.transaction():
|
||||||
await member.set_proxy_tags(ctx.conn, prefix, suffix)
|
await member.set_proxy_tags(ctx.conn, prefix, suffix)
|
||||||
await ctx.reply_ok("Proxy settings updated." if prefix or suffix else "Proxy settings cleared.")
|
await ctx.reply_ok(
|
||||||
|
"Proxy settings updated." if prefix or suffix else "Proxy settings cleared. If you meant to set your proxy tags, type `pk;help proxy` for help.")
|
||||||
|
|
||||||
|
|
||||||
async def member_delete(ctx: CommandContext):
|
async def member_delete(ctx: CommandContext, member: Member):
|
||||||
await ctx.ensure_system()
|
|
||||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.remove_member))
|
|
||||||
|
|
||||||
delete_confirm_msg = "Are you sure you want to delete {}? If so, reply to this message with the member's ID (`{}`).".format(
|
delete_confirm_msg = "Are you sure you want to delete {}? If so, reply to this message with the member's ID (`{}`).".format(
|
||||||
member.name, member.hid)
|
member.name, member.hid)
|
||||||
if not await ctx.confirm_text(ctx.message.author, ctx.message.channel, member.hid, delete_confirm_msg):
|
if not await ctx.confirm_text(ctx.message.author, ctx.message.channel, member.hid, delete_confirm_msg):
|
||||||
|
@ -1,62 +1,18 @@
|
|||||||
from pluralkit.bot import help
|
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
|
||||||
|
|
||||||
async def get_message_contents(client: discord.Client, channel_id: int, message_id: int):
|
|
||||||
channel = client.get_channel(channel_id)
|
|
||||||
if channel:
|
|
||||||
try:
|
|
||||||
original_message = await channel.get_message(message_id)
|
|
||||||
return original_message.content or None
|
|
||||||
except (discord.errors.Forbidden, discord.errors.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def message_info(ctx: CommandContext):
|
async def message_info(ctx: CommandContext):
|
||||||
mid_str = ctx.pop_str(CommandError("You must pass a message ID.", help=help.message_lookup))
|
mid_str = ctx.pop_str(CommandError("You must pass a message ID."))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mid = int(mid_str)
|
mid = int(mid_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise CommandError("You must pass a valid number as a message ID.", help=help.message_lookup)
|
raise CommandError("You must pass a valid number as a message ID.")
|
||||||
|
|
||||||
# Find the message in the DB
|
# Find the message in the DB
|
||||||
message = await db.get_message(ctx.conn, mid)
|
message = await db.get_message(ctx.conn, mid)
|
||||||
if not message:
|
if not message:
|
||||||
raise CommandError("Message with ID '{}' not found.".format(mid))
|
raise CommandError(
|
||||||
|
"Message with ID '{}' not found. Are you sure it's a message proxied by PluralKit?".format(mid))
|
||||||
|
|
||||||
# Get the original sender of the messages
|
await ctx.reply_ok(embed=await embeds.message_card(ctx.client, message))
|
||||||
try:
|
|
||||||
original_sender = await ctx.client.get_user_info(message.sender)
|
|
||||||
except discord.NotFound:
|
|
||||||
# Account was since deleted - rare but we're handling it anyway
|
|
||||||
original_sender = None
|
|
||||||
|
|
||||||
embed = discord.Embed()
|
|
||||||
embed.timestamp = discord.utils.snowflake_time(mid)
|
|
||||||
embed.colour = discord.Colour.blue()
|
|
||||||
|
|
||||||
if message.system_name:
|
|
||||||
system_value = "{} (`{}`)".format(message.system_name, message.system_hid)
|
|
||||||
else:
|
|
||||||
system_value = "`{}`".format(message.system_hid)
|
|
||||||
embed.add_field(name="System", value=system_value)
|
|
||||||
|
|
||||||
embed.add_field(name="Member", value="{} (`{}`)".format(message.name, message.hid))
|
|
||||||
|
|
||||||
if original_sender:
|
|
||||||
sender_name = "{}#{}".format(original_sender.name, original_sender.discriminator)
|
|
||||||
else:
|
|
||||||
sender_name = "(deleted account {})".format(message.sender)
|
|
||||||
|
|
||||||
embed.add_field(name="Sent by", value=sender_name)
|
|
||||||
|
|
||||||
message_content = await get_message_contents(ctx.client, message.channel, message.mid)
|
|
||||||
embed.description = message_content or "(unknown, message deleted)"
|
|
||||||
|
|
||||||
embed.set_author(name=message.name, icon_url=message.avatar_url or discord.Embed.Empty)
|
|
||||||
|
|
||||||
await ctx.reply_ok(embed=embed)
|
|
||||||
|
@ -1,61 +1,57 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from discord.utils import oauth_url
|
from discord.utils import oauth_url
|
||||||
|
|
||||||
import pluralkit.utils
|
from pluralkit.bot import help
|
||||||
from pluralkit.bot import utils, embeds
|
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
|
||||||
|
async def help_root(ctx: CommandContext):
|
||||||
async def show_help(ctx: CommandContext):
|
if ctx.match("commands"):
|
||||||
embed = embeds.status("")
|
await ctx.reply(help.all_commands)
|
||||||
embed.title = "PluralKit Help"
|
elif ctx.match("proxy"):
|
||||||
embed.set_footer(text="By Astrid (Ske#6201; pk;member qoxvy) | GitHub: https://github.com/xSke/PluralKit/")
|
await ctx.reply(help.proxy_guide)
|
||||||
|
|
||||||
category = ctx.pop_str() if ctx.has_next() else None
|
|
||||||
|
|
||||||
from pluralkit.bot.help import help_pages
|
|
||||||
if category in help_pages:
|
|
||||||
for name, text in help_pages[category]:
|
|
||||||
if name:
|
|
||||||
embed.add_field(name=name, value=text)
|
|
||||||
else:
|
|
||||||
embed.description = text
|
|
||||||
else:
|
else:
|
||||||
raise CommandError("Unknown help page '{}'.".format(category))
|
await ctx.reply(help.root)
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
async def invite_link(ctx: CommandContext):
|
async def invite_link(ctx: CommandContext):
|
||||||
client_id = os.environ["CLIENT_ID"]
|
client_id = os.environ["CLIENT_ID"]
|
||||||
|
|
||||||
permissions = discord.Permissions()
|
permissions = discord.Permissions()
|
||||||
|
|
||||||
|
# So the bot can actually add the webhooks it needs to do the proxy functionality
|
||||||
permissions.manage_webhooks = True
|
permissions.manage_webhooks = True
|
||||||
|
|
||||||
|
# So the bot can respond with status, error, and success messages
|
||||||
permissions.send_messages = True
|
permissions.send_messages = True
|
||||||
|
|
||||||
|
# So the bot can delete channels
|
||||||
permissions.manage_messages = True
|
permissions.manage_messages = True
|
||||||
|
|
||||||
|
# So the bot can respond with extended embeds, ex. member cards
|
||||||
permissions.embed_links = True
|
permissions.embed_links = True
|
||||||
|
|
||||||
|
# So the bot can send images too
|
||||||
permissions.attach_files = True
|
permissions.attach_files = True
|
||||||
|
|
||||||
|
# (unsure if it needs this, actually, might be necessary for message lookup)
|
||||||
permissions.read_message_history = True
|
permissions.read_message_history = True
|
||||||
|
|
||||||
|
# So the bot can add reactions for confirm/deny prompts
|
||||||
permissions.add_reactions = True
|
permissions.add_reactions = True
|
||||||
|
|
||||||
url = oauth_url(client_id, permissions)
|
url = oauth_url(client_id, permissions)
|
||||||
logger.debug("Sending invite URL: {}".format(url))
|
|
||||||
await ctx.reply_ok("Use this link to add PluralKit to your server: {}".format(url))
|
await ctx.reply_ok("Use this link to add PluralKit to your server: {}".format(url))
|
||||||
|
|
||||||
|
|
||||||
async def export(ctx: CommandContext):
|
async def export(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
members = await db.get_all_members(ctx.conn, system.id)
|
members = await system.get_members(ctx.conn)
|
||||||
accounts = await db.get_linked_accounts(ctx.conn, system.id)
|
accounts = await system.get_linked_account_ids(ctx.conn)
|
||||||
switches = await pluralkit.utils.get_front_history(ctx.conn, system.id, 999999)
|
switches = await system.get_switches(ctx.conn, 999999)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"name": system.name,
|
"name": system.name,
|
||||||
@ -81,10 +77,10 @@ async def export(ctx: CommandContext):
|
|||||||
"accounts": [str(uid) for uid in accounts],
|
"accounts": [str(uid) for uid in accounts],
|
||||||
"switches": [
|
"switches": [
|
||||||
{
|
{
|
||||||
"timestamp": timestamp.isoformat(),
|
"timestamp": switch.timestamp.isoformat(),
|
||||||
"members": [member.hid for member in members]
|
"members": [member.hid for member in await switch.fetch_members(ctx.conn)]
|
||||||
} for timestamp, members in switches
|
} for switch in switches
|
||||||
]
|
] # TODO: messages
|
||||||
}
|
}
|
||||||
|
|
||||||
f = io.BytesIO(json.dumps(data).encode("utf-8"))
|
f = io.BytesIO(json.dumps(data).encode("utf-8"))
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
|
||||||
|
|
||||||
async def set_log(ctx: CommandContext):
|
async def set_log(ctx: CommandContext):
|
||||||
if not ctx.message.author.guild_permissions.administrator:
|
if not ctx.message.author.guild_permissions.administrator:
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import dateparser
|
import dateparser
|
||||||
import humanize
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@ -7,24 +6,34 @@ import pluralkit.utils
|
|||||||
from pluralkit.bot import help
|
from pluralkit.bot import help
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
|
from pluralkit.utils import display_relative
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
async def switch_root(ctx: CommandContext):
|
||||||
|
if not ctx.has_next():
|
||||||
|
raise CommandError("You must use a subcommand. For a list of subcommands, type `pk;switch help`.")
|
||||||
|
|
||||||
|
if ctx.match("out"):
|
||||||
|
await switch_out(ctx)
|
||||||
|
elif ctx.match("move"):
|
||||||
|
await switch_move(ctx)
|
||||||
|
elif ctx.match("delete") or ctx.match("remove") or ctx.match("erase") or ctx.match("cancel"):
|
||||||
|
await switch_delete(ctx)
|
||||||
|
elif ctx.match("help"):
|
||||||
|
await ctx.reply(help.member_commands)
|
||||||
|
else:
|
||||||
|
await switch_member(ctx)
|
||||||
|
|
||||||
|
|
||||||
async def switch_member(ctx: CommandContext):
|
async def switch_member(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
if not ctx.has_next():
|
if not ctx.has_next():
|
||||||
raise CommandError("You must pass at least one member name or ID to register a switch to.",
|
raise CommandError("You must pass at least one member name or ID to register a switch to.")
|
||||||
help=help.switch_register)
|
|
||||||
|
|
||||||
members: List[Member] = []
|
members: List[Member] = []
|
||||||
for member_name in ctx.remaining().split(" "):
|
while ctx.has_next():
|
||||||
# Find the member
|
members.append(await ctx.pop_member())
|
||||||
member = await utils.get_member_fuzzy(ctx.conn, system.id, member_name)
|
|
||||||
if not member:
|
|
||||||
raise CommandError("Couldn't find member \"{}\".".format(member_name))
|
|
||||||
members.append(member)
|
|
||||||
|
|
||||||
# Compare requested switch IDs and existing fronter IDs to check for existing switches
|
# Compare requested switch IDs and existing fronter IDs to check for existing switches
|
||||||
# Lists, because order matters, it makes sense to just swap fronters
|
# Lists, because order matters, it makes sense to just swap fronters
|
||||||
@ -40,10 +49,7 @@ async def switch_member(ctx: CommandContext):
|
|||||||
raise CommandError("Duplicate members in member list.")
|
raise CommandError("Duplicate members in member list.")
|
||||||
|
|
||||||
# Log the switch
|
# Log the switch
|
||||||
async with ctx.conn.transaction():
|
await system.add_switch(ctx.conn, members)
|
||||||
switch_id = await db.add_switch(ctx.conn, system_id=system.id)
|
|
||||||
for member in members:
|
|
||||||
await db.add_switch_member(ctx.conn, switch_id=switch_id, member_id=member.id)
|
|
||||||
|
|
||||||
if len(members) == 1:
|
if len(members) == 1:
|
||||||
await ctx.reply_ok("Switch registered. Current fronter is now {}.".format(members[0].name))
|
await ctx.reply_ok("Switch registered. Current fronter is now {}.".format(members[0].name))
|
||||||
@ -55,20 +61,52 @@ async def switch_member(ctx: CommandContext):
|
|||||||
async def switch_out(ctx: CommandContext):
|
async def switch_out(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
# Get current fronters
|
switch = await system.get_latest_switch(ctx.conn)
|
||||||
fronters, _ = await pluralkit.utils.get_fronter_ids(ctx.conn, system_id=system.id)
|
if switch and not switch.members:
|
||||||
if not fronters:
|
|
||||||
raise CommandError("There's already no one in front.")
|
raise CommandError("There's already no one in front.")
|
||||||
|
|
||||||
# Log it, and don't log any members
|
# Log it, and don't log any members
|
||||||
await db.add_switch(ctx.conn, system_id=system.id)
|
await system.add_switch(ctx.conn, [])
|
||||||
await ctx.reply_ok("Switch-out registered.")
|
await ctx.reply_ok("Switch-out registered.")
|
||||||
|
|
||||||
|
|
||||||
|
async def switch_delete(ctx: CommandContext):
|
||||||
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
|
last_two_switches = await system.get_switches(ctx.conn, 2)
|
||||||
|
if not last_two_switches:
|
||||||
|
raise CommandError("You do not have a logged switch to delete.")
|
||||||
|
|
||||||
|
last_switch = last_two_switches[0]
|
||||||
|
next_last_switch = last_two_switches[1] if len(last_two_switches) > 1 else None
|
||||||
|
|
||||||
|
last_switch_members = ", ".join([member.name for member in await last_switch.fetch_members(ctx.conn)])
|
||||||
|
last_switch_time = display_relative(last_switch.timestamp)
|
||||||
|
|
||||||
|
if next_last_switch:
|
||||||
|
next_last_switch_members = ", ".join([member.name for member in await next_last_switch.fetch_members(ctx.conn)])
|
||||||
|
next_last_switch_time = display_relative(next_last_switch.timestamp)
|
||||||
|
msg = await ctx.reply_warn("This will delete the latest switch ({}, {} ago). The next latest switch is {} ({} ago). Is this okay?".format(last_switch_members, last_switch_time, next_last_switch_members, next_last_switch_time))
|
||||||
|
else:
|
||||||
|
msg = await ctx.reply_warn("This will delete the latest switch ({}, {} ago). You have no other switches logged. Is this okay?".format(last_switch_members, last_switch_time))
|
||||||
|
|
||||||
|
if not await ctx.confirm_react(ctx.message.author, msg):
|
||||||
|
raise CommandError("Switch deletion cancelled.")
|
||||||
|
|
||||||
|
await last_switch.delete(ctx.conn)
|
||||||
|
|
||||||
|
if next_last_switch:
|
||||||
|
# lol block scope amirite
|
||||||
|
# but yeah this is fine
|
||||||
|
await ctx.reply_ok("Switch deleted. Next latest switch is now {} ({} ago).".format(next_last_switch_members, next_last_switch_time))
|
||||||
|
else:
|
||||||
|
await ctx.reply_ok("Switch deleted. You now have no logged switches.")
|
||||||
|
|
||||||
|
|
||||||
async def switch_move(ctx: CommandContext):
|
async def switch_move(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
if not ctx.has_next():
|
if not ctx.has_next():
|
||||||
raise CommandError("You must pass a time to move the switch to.", help=help.switch_move)
|
raise CommandError("You must pass a time to move the switch to.")
|
||||||
|
|
||||||
# Parse the time to move to
|
# Parse the time to move to
|
||||||
new_time = dateparser.parse(ctx.remaining(), languages=["en"], settings={
|
new_time = dateparser.parse(ctx.remaining(), languages=["en"], settings={
|
||||||
@ -76,11 +114,11 @@ async def switch_move(ctx: CommandContext):
|
|||||||
"RETURN_AS_TIMEZONE_AWARE": False
|
"RETURN_AS_TIMEZONE_AWARE": False
|
||||||
})
|
})
|
||||||
if not new_time:
|
if not new_time:
|
||||||
raise CommandError("'{}' can't be parsed as a valid time.".format(ctx.remaining()), help=help.switch_move)
|
raise CommandError("'{}' can't be parsed as a valid time.".format(ctx.remaining()))
|
||||||
|
|
||||||
# Make sure the time isn't in the future
|
# Make sure the time isn't in the future
|
||||||
if new_time > datetime.utcnow():
|
if new_time > datetime.utcnow():
|
||||||
raise CommandError("Can't move switch to a time in the future.", help=help.switch_move)
|
raise CommandError("Can't move switch to a time in the future.")
|
||||||
|
|
||||||
# Make sure it all runs in a big transaction for atomicity
|
# Make sure it all runs in a big transaction for atomicity
|
||||||
async with ctx.conn.transaction():
|
async with ctx.conn.transaction():
|
||||||
@ -94,19 +132,25 @@ async def switch_move(ctx: CommandContext):
|
|||||||
second_last_timestamp, _ = last_two_switches[1]
|
second_last_timestamp, _ = last_two_switches[1]
|
||||||
|
|
||||||
if new_time < second_last_timestamp:
|
if new_time < second_last_timestamp:
|
||||||
time_str = humanize.naturaltime(pluralkit.utils.fix_time(second_last_timestamp))
|
time_str = display_relative(second_last_timestamp)
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
"Can't move switch to before last switch time ({}), as it would cause conflicts.".format(time_str))
|
"Can't move switch to before last switch time ({} ago), as it would cause conflicts.".format(time_str))
|
||||||
|
|
||||||
# Display the confirmation message w/ humanized times
|
# Display the confirmation message w/ humanized times
|
||||||
members = ", ".join([member.name for member in last_fronters]) or "nobody"
|
members = ", ".join([member.name for member in last_fronters]) or "nobody"
|
||||||
last_absolute = last_timestamp.isoformat(sep=" ", timespec="seconds")
|
last_absolute = last_timestamp.isoformat(sep=" ", timespec="seconds")
|
||||||
last_relative = humanize.naturaltime(pluralkit.utils.fix_time(last_timestamp))
|
last_relative = display_relative(last_timestamp)
|
||||||
new_absolute = new_time.isoformat(sep=" ", timespec="seconds")
|
new_absolute = new_time.isoformat(sep=" ", timespec="seconds")
|
||||||
new_relative = humanize.naturaltime(pluralkit.utils.fix_time(new_time))
|
new_relative = display_relative(new_time)
|
||||||
|
|
||||||
# Confirm with user
|
# Confirm with user
|
||||||
switch_confirm_message = await ctx.reply("This will move the latest switch ({}) from {} ({}) to {} ({}). Is this OK?".format(members, last_absolute, last_relative, new_absolute, new_relative))
|
switch_confirm_message = await ctx.reply(
|
||||||
|
"This will move the latest switch ({}) from {} ({} ago) to {} ({} ago). Is this OK?".format(members,
|
||||||
|
last_absolute,
|
||||||
|
last_relative,
|
||||||
|
new_absolute,
|
||||||
|
new_relative))
|
||||||
|
|
||||||
if not await ctx.confirm_react(ctx.message.author, switch_confirm_message):
|
if not await ctx.confirm_react(ctx.message.author, switch_confirm_message):
|
||||||
raise CommandError("Switch move cancelled.")
|
raise CommandError("Switch move cancelled.")
|
||||||
|
|
||||||
@ -116,6 +160,3 @@ async def switch_move(ctx: CommandContext):
|
|||||||
# Change the switch in the DB
|
# Change the switch in the DB
|
||||||
await db.move_last_switch(ctx.conn, system.id, switch_id, new_time)
|
await db.move_last_switch(ctx.conn, system.id, switch_id, new_time)
|
||||||
await ctx.reply_ok("Switch moved.")
|
await ctx.reply_ok("Switch moved.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,28 +3,73 @@ import humanize
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pluralkit.bot.embeds
|
import pluralkit.bot.embeds
|
||||||
import pluralkit.utils
|
|
||||||
from pluralkit.bot import help
|
from pluralkit.bot import help
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
from pluralkit.errors import ExistingSystemError, UnlinkingLastAccountError, PluralKitError, AccountAlreadyLinkedError
|
from pluralkit.errors import ExistingSystemError, UnlinkingLastAccountError, AccountAlreadyLinkedError
|
||||||
|
from pluralkit.utils import display_relative
|
||||||
logger = logging.getLogger("pluralkit.commands")
|
|
||||||
|
|
||||||
|
|
||||||
async def system_info(ctx: CommandContext):
|
async def system_root(ctx: CommandContext):
|
||||||
if ctx.has_next():
|
# Commands that operate without a specified system (usually defaults to the executor's own system)
|
||||||
system = await ctx.pop_system()
|
if ctx.match("name") or ctx.match("rename"):
|
||||||
|
await system_name(ctx)
|
||||||
|
elif ctx.match("description"):
|
||||||
|
await system_description(ctx)
|
||||||
|
elif ctx.match("avatar") or ctx.match("icon"):
|
||||||
|
await system_avatar(ctx)
|
||||||
|
elif ctx.match("tag"):
|
||||||
|
await system_tag(ctx)
|
||||||
|
elif ctx.match("new") or ctx.match("register") or ctx.match("create") or ctx.match("init"):
|
||||||
|
await system_new(ctx)
|
||||||
|
elif ctx.match("delete") or ctx.match("delete") or ctx.match("erase"):
|
||||||
|
await system_delete(ctx)
|
||||||
|
elif ctx.match("front") or ctx.match("fronter") or ctx.match("fronters"):
|
||||||
|
await system_fronter(ctx, await ctx.ensure_system())
|
||||||
|
elif ctx.match("fronthistory"):
|
||||||
|
await system_fronthistory(ctx, await ctx.ensure_system())
|
||||||
|
elif ctx.match("frontpercent") or ctx.match("frontbreakdown") or ctx.match("frontpercentage"):
|
||||||
|
await system_frontpercent(ctx, await ctx.ensure_system())
|
||||||
|
elif ctx.match("help"):
|
||||||
|
await ctx.reply(help.system_commands)
|
||||||
|
elif ctx.match("set"):
|
||||||
|
await system_set(ctx)
|
||||||
|
elif not ctx.has_next():
|
||||||
|
# (no argument, command ends here, default to showing own system)
|
||||||
|
await system_info(ctx, await ctx.ensure_system())
|
||||||
else:
|
else:
|
||||||
system = await ctx.ensure_system()
|
# If nothing matches, the next argument is likely a system name/ID, so delegate
|
||||||
|
# to the specific system root
|
||||||
|
await specified_system_root(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def specified_system_root(ctx: CommandContext):
|
||||||
|
# Commands that operate on a specified system (ie. not necessarily the command executor's)
|
||||||
|
system_name = ctx.pop_str()
|
||||||
|
system = await utils.get_system_fuzzy(ctx.conn, ctx.client, system_name)
|
||||||
|
if not system:
|
||||||
|
raise CommandError(
|
||||||
|
"Unable to find system `{}`. If you meant to run a command, type `pk;system help` for a list of system commands.".format(
|
||||||
|
system_name))
|
||||||
|
|
||||||
|
if ctx.match("front") or ctx.match("fronter"):
|
||||||
|
await system_fronter(ctx, system)
|
||||||
|
elif ctx.match("fronthistory"):
|
||||||
|
await system_fronthistory(ctx, system)
|
||||||
|
elif ctx.match("frontpercent") or ctx.match("frontbreakdown") or ctx.match("frontpercentage"):
|
||||||
|
await system_frontpercent(ctx, system)
|
||||||
|
else:
|
||||||
|
await system_info(ctx, system)
|
||||||
|
|
||||||
|
|
||||||
|
async def system_info(ctx: CommandContext, system: System):
|
||||||
await ctx.reply(embed=await pluralkit.bot.embeds.system_card(ctx.conn, ctx.client, system))
|
await ctx.reply(embed=await pluralkit.bot.embeds.system_card(ctx.conn, ctx.client, system))
|
||||||
|
|
||||||
|
|
||||||
async def new_system(ctx: CommandContext):
|
async def system_new(ctx: CommandContext):
|
||||||
system_name = ctx.remaining() or None
|
new_name = ctx.remaining() or None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await System.create_system(ctx.conn, ctx.message.author.id, system_name)
|
await System.create_system(ctx.conn, ctx.message.author.id, new_name)
|
||||||
except ExistingSystemError as e:
|
except ExistingSystemError as e:
|
||||||
raise CommandError(e.message)
|
raise CommandError(e.message)
|
||||||
|
|
||||||
@ -95,9 +140,10 @@ async def system_avatar(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("System avatar {}.".format("updated" if new_avatar_url else "cleared"))
|
await ctx.reply_ok("System avatar {}.".format("updated" if new_avatar_url else "cleared"))
|
||||||
|
|
||||||
|
|
||||||
async def system_link(ctx: CommandContext):
|
async def account_link(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
account_name = ctx.pop_str(CommandError("You must pass an account to link this system to.", help=help.link_account))
|
account_name = ctx.pop_str(CommandError(
|
||||||
|
"You must pass an account to link this system to. You can either use a \\@mention, or a raw account ID."))
|
||||||
|
|
||||||
# Do the sanity checking here too (despite it being done in System.link_account)
|
# Do the sanity checking here too (despite it being done in System.link_account)
|
||||||
# Because we want it to be done before the confirmation dialog is shown
|
# Because we want it to be done before the confirmation dialog is shown
|
||||||
@ -105,14 +151,15 @@ async def system_link(ctx: CommandContext):
|
|||||||
# Find account to link
|
# Find account to link
|
||||||
linkee = await utils.parse_mention(ctx.client, account_name)
|
linkee = await utils.parse_mention(ctx.client, account_name)
|
||||||
if not linkee:
|
if not linkee:
|
||||||
raise CommandError("Account not found.")
|
raise CommandError("Account `{}` not found.".format(account_name))
|
||||||
|
|
||||||
# Make sure account doesn't already have a system
|
# Make sure account doesn't already have a system
|
||||||
account_system = await System.get_by_account(ctx.conn, linkee.id)
|
account_system = await System.get_by_account(ctx.conn, linkee.id)
|
||||||
if account_system:
|
if account_system:
|
||||||
raise CommandError(AccountAlreadyLinkedError(account_system).message)
|
raise CommandError(AccountAlreadyLinkedError(account_system).message)
|
||||||
|
|
||||||
msg = await ctx.reply("{}, please confirm the link by clicking the \u2705 reaction on this message.".format(linkee.mention))
|
msg = await ctx.reply(
|
||||||
|
"{}, please confirm the link by clicking the \u2705 reaction on this message.".format(linkee.mention))
|
||||||
if not await ctx.confirm_react(linkee, msg):
|
if not await ctx.confirm_react(linkee, msg):
|
||||||
raise CommandError("Account link cancelled.")
|
raise CommandError("Account link cancelled.")
|
||||||
|
|
||||||
@ -120,7 +167,7 @@ async def system_link(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("Account linked to system.")
|
await ctx.reply_ok("Account linked to system.")
|
||||||
|
|
||||||
|
|
||||||
async def system_unlink(ctx: CommandContext):
|
async def account_unlink(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -131,24 +178,18 @@ async def system_unlink(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("Account unlinked.")
|
await ctx.reply_ok("Account unlinked.")
|
||||||
|
|
||||||
|
|
||||||
async def system_fronter(ctx: CommandContext):
|
async def system_fronter(ctx: CommandContext, system: System):
|
||||||
if ctx.has_next():
|
|
||||||
system = await ctx.pop_system()
|
|
||||||
else:
|
|
||||||
system = await ctx.ensure_system()
|
|
||||||
|
|
||||||
embed = await embeds.front_status(await system.get_latest_switch(ctx.conn), ctx.conn)
|
embed = await embeds.front_status(await system.get_latest_switch(ctx.conn), ctx.conn)
|
||||||
await ctx.reply(embed=embed)
|
await ctx.reply(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
async def system_fronthistory(ctx: CommandContext):
|
async def system_fronthistory(ctx: CommandContext, system: System):
|
||||||
if ctx.has_next():
|
|
||||||
system = await ctx.pop_system()
|
|
||||||
else:
|
|
||||||
system = await ctx.ensure_system()
|
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
front_history = await pluralkit.utils.get_front_history(ctx.conn, system.id, count=10)
|
front_history = await pluralkit.utils.get_front_history(ctx.conn, system.id, count=10)
|
||||||
|
|
||||||
|
if not front_history:
|
||||||
|
raise CommandError("You have no logged switches. Use `pk;switch´ to start logging.")
|
||||||
|
|
||||||
for i, (timestamp, members) in enumerate(front_history):
|
for i, (timestamp, members) in enumerate(front_history):
|
||||||
# Special case when no one's fronting
|
# Special case when no one's fronting
|
||||||
if len(members) == 0:
|
if len(members) == 0:
|
||||||
@ -158,13 +199,13 @@ async def system_fronthistory(ctx: CommandContext):
|
|||||||
|
|
||||||
# Make proper date string
|
# Make proper date string
|
||||||
time_text = timestamp.isoformat(sep=" ", timespec="seconds")
|
time_text = timestamp.isoformat(sep=" ", timespec="seconds")
|
||||||
rel_text = humanize.naturaltime(pluralkit.utils.fix_time(timestamp))
|
rel_text = display_relative(timestamp)
|
||||||
|
|
||||||
delta_text = ""
|
delta_text = ""
|
||||||
if i > 0:
|
if i > 0:
|
||||||
last_switch_time = front_history[i - 1][0]
|
last_switch_time = front_history[i - 1][0]
|
||||||
delta_text = ", for {}".format(humanize.naturaldelta(timestamp - last_switch_time))
|
delta_text = ", for {}".format(display_relative(timestamp - last_switch_time))
|
||||||
lines.append("**{}** ({}, {}{})".format(name, time_text, rel_text, delta_text))
|
lines.append("**{}** ({}, {} ago{})".format(name, time_text, rel_text, delta_text))
|
||||||
|
|
||||||
embed = embeds.status("\n".join(lines) or "(none)")
|
embed = embeds.status("\n".join(lines) or "(none)")
|
||||||
embed.title = "Past switches"
|
embed.title = "Past switches"
|
||||||
@ -183,9 +224,7 @@ async def system_delete(ctx: CommandContext):
|
|||||||
await ctx.reply_ok("System deleted.")
|
await ctx.reply_ok("System deleted.")
|
||||||
|
|
||||||
|
|
||||||
async def system_frontpercent(ctx: CommandContext):
|
async def system_frontpercent(ctx: CommandContext, system: System):
|
||||||
system = await ctx.ensure_system()
|
|
||||||
|
|
||||||
# Parse the time limit (will go this far back)
|
# Parse the time limit (will go this far back)
|
||||||
if ctx.remaining():
|
if ctx.remaining():
|
||||||
before = dateparser.parse(ctx.remaining(), languages=["en"], settings={
|
before = dateparser.parse(ctx.remaining(), languages=["en"], settings={
|
||||||
@ -265,6 +304,6 @@ async def system_frontpercent(ctx: CommandContext):
|
|||||||
embed.add_field(name=member.name if member else "(no fronter)",
|
embed.add_field(name=member.name if member else "(no fronter)",
|
||||||
value="{}% ({})".format(percent, humanize.naturaldelta(front_time)))
|
value="{}% ({})".format(percent, humanize.naturaldelta(front_time)))
|
||||||
|
|
||||||
embed.set_footer(text="Since {} ({})".format(span_start.isoformat(sep=" ", timespec="seconds"),
|
embed.set_footer(text="Since {} ({} ago)".format(span_start.isoformat(sep=" ", timespec="seconds"),
|
||||||
humanize.naturaltime(pluralkit.utils.fix_time(span_start))))
|
display_relative(span_start)))
|
||||||
await ctx.reply(embed=embed)
|
await ctx.reply(embed=embed)
|
||||||
|
@ -2,12 +2,12 @@ import discord
|
|||||||
import humanize
|
import humanize
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
import pluralkit
|
from pluralkit import db
|
||||||
from pluralkit.bot.utils import escape
|
from pluralkit.bot.utils import escape
|
||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
from pluralkit.switch import Switch
|
from pluralkit.switch import Switch
|
||||||
from pluralkit.system import System
|
from pluralkit.system import System
|
||||||
from pluralkit.utils import get_fronters
|
from pluralkit.utils import get_fronters, display_relative
|
||||||
|
|
||||||
|
|
||||||
def truncate_field_name(s: str) -> str:
|
def truncate_field_name(s: str) -> str:
|
||||||
@ -52,7 +52,8 @@ def status(text: str) -> discord.Embed:
|
|||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
def exception_log(message_content, author_name, author_discriminator, author_id, server_id, channel_id) -> discord.Embed:
|
def exception_log(message_content, author_name, author_discriminator, author_id, server_id,
|
||||||
|
channel_id) -> discord.Embed:
|
||||||
embed = discord.Embed()
|
embed = discord.Embed()
|
||||||
embed.colour = discord.Colour.dark_red()
|
embed.colour = discord.Colour.dark_red()
|
||||||
embed.title = truncate_title(message_content)
|
embed.title = truncate_title(message_content)
|
||||||
@ -82,7 +83,8 @@ async def system_card(conn, client: discord.Client, system: System) -> discord.E
|
|||||||
if fronters:
|
if fronters:
|
||||||
names = ", ".join([member.name for member in fronters])
|
names = ", ".join([member.name for member in fronters])
|
||||||
fronter_val = "{} (for {})".format(names, humanize.naturaldelta(switch_time))
|
fronter_val = "{} (for {})".format(names, humanize.naturaldelta(switch_time))
|
||||||
card.add_field(name="Current fronter" if len(fronters) == 1 else "Current fronters", value=truncate_field_body(fronter_val))
|
card.add_field(name="Current fronter" if len(fronters) == 1 else "Current fronters",
|
||||||
|
value=truncate_field_body(fronter_val))
|
||||||
|
|
||||||
account_names = []
|
account_names = []
|
||||||
for account_id in await system.get_linked_account_ids(conn):
|
for account_id in await system.get_linked_account_ids(conn):
|
||||||
@ -185,7 +187,53 @@ async def front_status(switch: Switch, conn) -> discord.Embed:
|
|||||||
if switch.timestamp:
|
if switch.timestamp:
|
||||||
embed.add_field(name="Since",
|
embed.add_field(name="Since",
|
||||||
value="{} ({})".format(switch.timestamp.isoformat(sep=" ", timespec="seconds"),
|
value="{} ({})".format(switch.timestamp.isoformat(sep=" ", timespec="seconds"),
|
||||||
humanize.naturaltime(pluralkit.utils.fix_time(switch.timestamp))))
|
display_relative(switch.timestamp)))
|
||||||
else:
|
else:
|
||||||
embed = error("No switches logged.")
|
embed = error("No switches logged.")
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
async def get_message_contents(client: discord.Client, channel_id: int, message_id: int):
|
||||||
|
channel = client.get_channel(channel_id)
|
||||||
|
if channel:
|
||||||
|
try:
|
||||||
|
original_message = await channel.get_message(message_id)
|
||||||
|
return original_message.content or None
|
||||||
|
except (discord.errors.Forbidden, discord.errors.NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def message_card(client: discord.Client, message: db.MessageInfo):
|
||||||
|
# Get the original sender of the messages
|
||||||
|
try:
|
||||||
|
original_sender = await client.get_user_info(message.sender)
|
||||||
|
except discord.NotFound:
|
||||||
|
# Account was since deleted - rare but we're handling it anyway
|
||||||
|
original_sender = None
|
||||||
|
|
||||||
|
embed = discord.Embed()
|
||||||
|
embed.timestamp = discord.utils.snowflake_time(message.mid)
|
||||||
|
embed.colour = discord.Colour.blue()
|
||||||
|
|
||||||
|
if message.system_name:
|
||||||
|
system_value = "{} (`{}`)".format(message.system_name, message.system_hid)
|
||||||
|
else:
|
||||||
|
system_value = "`{}`".format(message.system_hid)
|
||||||
|
embed.add_field(name="System", value=system_value)
|
||||||
|
|
||||||
|
embed.add_field(name="Member", value="{} (`{}`)".format(message.name, message.hid))
|
||||||
|
|
||||||
|
if original_sender:
|
||||||
|
sender_name = "{}#{}".format(original_sender.name, original_sender.discriminator)
|
||||||
|
else:
|
||||||
|
sender_name = "(deleted account {})".format(message.sender)
|
||||||
|
|
||||||
|
embed.add_field(name="Sent by", value=sender_name)
|
||||||
|
|
||||||
|
message_content = await get_message_contents(client, message.channel, message.mid)
|
||||||
|
embed.description = message_content or "(unknown, message deleted)"
|
||||||
|
|
||||||
|
embed.set_author(name=message.name, icon_url=message.avatar_url or discord.Embed.Empty)
|
||||||
|
return embed
|
||||||
|
@ -1,177 +1,123 @@
|
|||||||
categories = ("Help categories", """`pk;help system` - Details on system configuration.
|
all_commands = """
|
||||||
`pk;help member` - Details on member configuration.
|
**All commands**
|
||||||
`pk;help proxy` - Details on message proxying.
|
```
|
||||||
`pk;help switch` - Details on switch logging.
|
pk;system [system]
|
||||||
`pk;help mod` - Details on moderator operations.
|
pk;system new [system name
|
||||||
`pk;help import` - Details on data import from other services.""")
|
pk;system rename [new name]
|
||||||
getting_started = ("Getting started", """To get started using the bot, try running the following commands:
|
pk;system description [new description]
|
||||||
|
pk;system avatar [new avatar url]
|
||||||
|
pk;system tag [new system tag]
|
||||||
|
pk;system delete
|
||||||
|
pk;system [system] fronter
|
||||||
|
pk;system [system] fronthistory
|
||||||
|
pk;system [system] frontpercent
|
||||||
|
pk;member new <member name>
|
||||||
|
pk;member <member>
|
||||||
|
pk;member <member> rename <new name>
|
||||||
|
pk;member <member> description [new description]
|
||||||
|
pk;member <member> avatar [new avatar url]
|
||||||
|
pk;member <member> proxy [example match]
|
||||||
|
pk;member <member> pronouns [new pronouns]
|
||||||
|
pk;member <member> color [new color]
|
||||||
|
pk;member <member> birthday [new birthday]
|
||||||
|
pk;member <member> delete
|
||||||
|
pk;switch <member> [<other member>...]
|
||||||
|
pk;switch move <time to move>
|
||||||
|
pk;switch out
|
||||||
|
pk;switch delete
|
||||||
|
pk;link <other account>
|
||||||
|
pk;unlink
|
||||||
|
pk;message <message id>
|
||||||
|
pk;log <log channel>
|
||||||
|
pk;invite
|
||||||
|
pk;import
|
||||||
|
pk;export
|
||||||
|
pk;token
|
||||||
|
pk;token refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command notes**
|
||||||
|
Parameters in <angle brackets> are required, [square brackets] are optional .
|
||||||
|
System references can be a system ID, a Discord account ID or a \\@mention.
|
||||||
|
Member references can be a member ID or, for your own system, a member name.
|
||||||
|
Leaving an optional parameter blank will often clear the relevant value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
system_commands = """
|
||||||
|
**System commands**
|
||||||
|
|
||||||
|
```
|
||||||
|
pk;system [system]
|
||||||
|
pk;system new [system name
|
||||||
|
pk;system rename [new name]
|
||||||
|
pk;system description [new description]
|
||||||
|
pk;system avatar [new avatar url]
|
||||||
|
pk;system tag [new system tag]
|
||||||
|
pk;system delete
|
||||||
|
pk;system [system] fronter
|
||||||
|
pk;system [system] fronthistory
|
||||||
|
pk;system [system] frontpercent
|
||||||
|
pk;link <other account>
|
||||||
|
pk;unlink
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command notes**
|
||||||
|
Parameters in <angle brackets> are required, [square brackets] are optional .
|
||||||
|
System references can be a system ID, a Discord account ID or a \\@mention.
|
||||||
|
Leaving an optional parameter blank will often clear the relevant value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
member_commands = """
|
||||||
|
**Member commands**
|
||||||
|
```
|
||||||
|
pk;member new <member name>
|
||||||
|
pk;member <member>
|
||||||
|
pk;member <member> rename <new name>
|
||||||
|
pk;member <member> description [new description]
|
||||||
|
pk;member <member> avatar [new avatar url]
|
||||||
|
pk;member <member> proxy [example match]
|
||||||
|
pk;member <member> pronouns [new pronouns]
|
||||||
|
pk;member <member> color [new color]
|
||||||
|
pk;member <member> birthday [new birthday]
|
||||||
|
pk;member <member> delete
|
||||||
|
pk;switch <member> [<other member>...]
|
||||||
|
pk;switch move <time to move>
|
||||||
|
pk;switch out
|
||||||
|
pk;switch delete
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command notes**
|
||||||
|
Parameters in <angle brackets> are required, [square brackets] are optional .
|
||||||
|
Member references can be a member ID or, for your own system, a member name.
|
||||||
|
Leaving an optional parameter blank will often clear the relevant value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
proxy_guide = """
|
||||||
|
**Proxying**
|
||||||
|
Proxying through PluralKit lets system members have their own faux-account with their name and avatar.
|
||||||
|
You'll type a message from your account in *proxy tags*, and PluralKit will recognize those tags and repost the message with the proper details.
|
||||||
|
|
||||||
|
To set up a member's proxy tag, use the `pk;member <name> proxy [example match]` command.
|
||||||
|
|
||||||
|
You'll need to give the bot an "example match". Imagine you're proxying the word "text", and add that to the end.
|
||||||
|
For example: `pk;member John proxy [text]`. That will set the member John up to use square brackets as proxy tags.
|
||||||
|
Now saying something like `[hello world]` will proxy the text "hello world" with John's name and avatar.
|
||||||
|
You can also use other symbols, letters, numbers, et cetera, as prefixes, suffixes, or both. `J:text`, `$text` and `text]` are also examples of valid example matches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
root = """
|
||||||
|
**PluralKit**
|
||||||
|
PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.
|
||||||
|
|
||||||
|
**Getting started**
|
||||||
|
To get started using the bot, try running the following commands.
|
||||||
**1**. `pk;system new` - Create a system if you haven't already
|
**1**. `pk;system new` - Create a system if you haven't already
|
||||||
**2**. `pk;member add John` - Add a new member to your system
|
**2**. `pk;member add John` - Add a new member to your system
|
||||||
**3**. `pk;member proxy John [text]` - Set up square brackets as proxy tags
|
**3**. `pk;member John proxy [text]` - Set up square brackets as proxy tags
|
||||||
**4**. You're done! See the other help pages for more commands.""")
|
**4**. You're done!
|
||||||
discord_link = (
|
|
||||||
"Discord",
|
|
||||||
"""For feedback, bug reports, suggestions, or just chatting, join our Discord: https://discord.gg/PczBt78""")
|
|
||||||
registering_system = ("Registering a new system",
|
|
||||||
"""To use PluralKit, you must register a system for your account. You can use the `pk;system new` command for this. You can optionally add a system name after the command.""")
|
|
||||||
lookup_system = ("Looking up a system", """To look up a system's details, you can use the `pk;system` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;system` - Shows details of your own system.
|
|
||||||
`pk;system abcde` - Shows details of the system with the ID `abcde`.
|
|
||||||
`pk;system @JohnsAccount` - Shows details of the system linked to @JohnsAccount.""")
|
|
||||||
edit_system = ("Editing system properties", """You can use the `pk;system` commands to change your system properties. The properties you can change are name, description, and tag.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;system name My System` - sets your system name to "My System".
|
|
||||||
`pk;system description A really cool system.` - sets your system description.
|
|
||||||
`pk;system tag [MS]` - Sets the tag (which will be displayed after member names in messages) to "[MS]".
|
|
||||||
`pk;system avatar https://placekitten.com/400/400` - Changes your system's avatar to a linked image.
|
|
||||||
|
|
||||||
If you don't specify any value, the property will be cleared.""")
|
**More information**
|
||||||
link_account = ("Linking accounts", """If your system has multiple accounts, you can link all of them to your system, and you can use the bot from all of those accounts.
|
For a full list of commands, type `pk;help commands`.
|
||||||
|
For a more in-depth explanation of proxying, type `pk;help proxy`.
|
||||||
For example:
|
If you're an existing user of the Tupperware proxy bot, type `pk;import` to import your data from there.
|
||||||
`pk;system link @MyOtherAccount` - Links @MyOtherAccount to your system.
|
"""
|
||||||
|
|
||||||
You'll need to confirm the link from the other account.""")
|
|
||||||
unlink_accounts = (
|
|
||||||
"Unlinking accounts", """If you need to unlink an account, you can do that with the `pk;system unlink` command.""")
|
|
||||||
add_member = ("Adding a new member", """To add a new member to your system, use the `pk;member new` command. You'll need to add a member name.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;member new John`""")
|
|
||||||
lookup_member = ("Looking up a member", """To look up a member's details, you can use the `pk;member` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;member John` - Shows details of the member in your system named John.
|
|
||||||
`pk;member abcde` - Shows details of the member with the ID `abcde`.
|
|
||||||
|
|
||||||
You can use member IDs to look up members in other systems.""")
|
|
||||||
edit_member = ("Editing member properties", """You can use the `pk;member` commands to change a member's properties. The properties you can change are name, description, color, pronouns, birthdate and avatar.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;member rename John Joe` - Changes John's name to Joe.
|
|
||||||
`pk;member description John Pretty cool dude.` - Changes John's description.
|
|
||||||
`pk;member color John #ff0000` - Changes John's color to red.
|
|
||||||
`pk;member pronouns John he/him` - Changes John's pronouns.
|
|
||||||
`pk;member birthdate John 1996-02-27` - Changes John's birthdate to Feb 27, 1996. (Must be YYYY-MM-DD format).
|
|
||||||
`pk;member birthdate John 02-27` - Changes John's birthdate to February 27th, with no year.
|
|
||||||
`pk;member avatar John https://placekitten.com/400/400` - Changes John's avatar to a linked image.
|
|
||||||
`pk;member avatar John @JohnsAccount` - Changes John's avatar to the avatar of the mentioned account.
|
|
||||||
|
|
||||||
If you don't specify any value, the property will be cleared.""")
|
|
||||||
remove_member = ("Removing a member", """If you want to delete a member, you can use the `pk;member delete` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;member delete John`
|
|
||||||
|
|
||||||
You will need to confirm the deletion.""")
|
|
||||||
member_proxy = ("Setting up member proxying", """To register a member for proxying, use the `pk;member proxy` command.
|
|
||||||
|
|
||||||
You will need to pass an "example proxy" message containing "text", surrounded by the brackets or prefixes you want to select.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;member proxy John [text]` - Registers John to use [square brackets] as proxy brackets.
|
|
||||||
`pk;member proxy John J:text` - Registers John to use the prefix "J:".
|
|
||||||
|
|
||||||
After setting proxy tags, you can use them in any message, and they'll be interpreted by the bot and proxied appropriately.""")
|
|
||||||
system_tag = ("Setting your system tag", """To set your system tag, use the `pk;system tag` command.
|
|
||||||
|
|
||||||
The tag is appended to the name of all proxied messages.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;system tag [MS]` - Sets your system tag to "[MS]".
|
|
||||||
`pk;system tag :heart:` - Sets your system tag to the heart emoji.
|
|
||||||
|
|
||||||
Note you can only use default Discord emojis, not custom server emojis.""")
|
|
||||||
message_lookup = ("Looking up a message", """You can look up a message by its ID using the `pk;message` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;message 467638937402212352` - Shows information about the message by that ID.
|
|
||||||
|
|
||||||
To get a message ID, turn on Developer Mode in your client's Appearance settings, right click, and press "Copy ID".""")
|
|
||||||
message_delete = ("Deleting messages",
|
|
||||||
"""You can delete your own messages by reacting with the ❌ emoji on it. Note that this only works on messages sent from your account.""")
|
|
||||||
switch_register = ("Registering a switch", """To log a switch in your system, use the `pk;switch` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;switch John` - Registers a switch with John as fronter.
|
|
||||||
`pk;switch John Jill` - Registers a switch John and Jill as co-fronters.""")
|
|
||||||
switch_out = ("Switching out", """You can use the `pk;switch out` command to register a switch with no one in front.""")
|
|
||||||
switch_move = ("Moving a switch", """You can move the latest switch you have registered using the `pk;switch move` command.
|
|
||||||
|
|
||||||
This is useful if you log the switch a while after it happened, and you want to properly backdate it in the history.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;switch move 10 minutes ago` - Moves the latest switch to 10 minutes ago
|
|
||||||
`pk;switch move 11pm EST` - Moves the latest switch to 11pm EST
|
|
||||||
|
|
||||||
Note that you can't move the switch further back than the second-last logged switch, and you can't move a switch to a time in the future.
|
|
||||||
|
|
||||||
The default time zone for absolute times is UTC, but you can specify other time zones in the command itself, as given in the example.""")
|
|
||||||
front_history = ("Viewing fronting history", """To view front history, you can use the `pk;front` and `pk;front history` commands.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;front` - Shows the current fronter(s) in your own system.
|
|
||||||
`pk;front abcde` - Shows the current fronter in the system with the ID `abcde`.
|
|
||||||
`pk;front history` - Shows the past 10 switches in your own system.
|
|
||||||
`pk;front history @JohnsAccount` - Shows the past 10 switches in the system linked to @JohnsAccount.""")
|
|
||||||
front_breakdown = ("Viewing a front breakdown", """To see a per-member breakdown of your switches, use the `pk;front percent` command. You can optionally give it a time limit to only count switches after that point.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;front percent` - Shows a front breakdown for your system since you started logging switches
|
|
||||||
`pk;front percent 1 day` - Shows a front breakdown for your system for the past day
|
|
||||||
`pk;front percent Jan 1st 2018` - Shows a front breakdown for your system since January 1st, 2018
|
|
||||||
|
|
||||||
Note that the percentages don't necessarily add up to 100%, as multiple members can be listed as fronting at a time.""")
|
|
||||||
logging_channel = ("Setting up a logging channel", """To designate a channel for the bot to log posted messages to, use the `pk;log` command.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
`pk;log #message-log` - Configures the bot to log to #message-log.""")
|
|
||||||
tupperware_import = ("Importing from Tupperware", """If you already have a registered system on Tupperware, you can use the `pk;import tupperware` command to import it into PluralKit.
|
|
||||||
|
|
||||||
Note the command only works on a server and channel where the Tupperware bot is already present.""")
|
|
||||||
help_pages = {
|
|
||||||
None: [
|
|
||||||
(None,
|
|
||||||
"""PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more."""),
|
|
||||||
getting_started,
|
|
||||||
categories,
|
|
||||||
discord_link
|
|
||||||
],
|
|
||||||
"system": [
|
|
||||||
registering_system,
|
|
||||||
lookup_system,
|
|
||||||
edit_system,
|
|
||||||
link_account,
|
|
||||||
unlink_accounts
|
|
||||||
],
|
|
||||||
"member": [
|
|
||||||
add_member,
|
|
||||||
lookup_member,
|
|
||||||
edit_member,
|
|
||||||
remove_member
|
|
||||||
],
|
|
||||||
"proxy": [
|
|
||||||
member_proxy,
|
|
||||||
system_tag,
|
|
||||||
message_lookup,
|
|
||||||
message_delete
|
|
||||||
],
|
|
||||||
"switch": [
|
|
||||||
switch_register,
|
|
||||||
switch_out,
|
|
||||||
switch_move,
|
|
||||||
front_history,
|
|
||||||
front_breakdown
|
|
||||||
],
|
|
||||||
"mod": [
|
|
||||||
(None,
|
|
||||||
"Note that all moderation commands require you to have administrator privileges on the server they're used on."),
|
|
||||||
logging_channel
|
|
||||||
],
|
|
||||||
"import": [
|
|
||||||
tupperware_import
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import discord
|
import discord
|
||||||
import logging
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -9,7 +8,6 @@ from pluralkit.bot.channel_logger import ChannelLogger
|
|||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
from pluralkit.system import System
|
from pluralkit.system import System
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.bot.proxy")
|
|
||||||
|
|
||||||
class ProxyError(Exception):
|
class ProxyError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -21,6 +19,7 @@ def fix_webhook(webhook: discord.Webhook) -> discord.Webhook:
|
|||||||
webhook._adapter.http = None
|
webhook._adapter.http = None
|
||||||
return webhook
|
return webhook
|
||||||
|
|
||||||
|
|
||||||
async def get_or_create_webhook_for_channel(conn, bot_user: discord.User, channel: discord.TextChannel):
|
async def get_or_create_webhook_for_channel(conn, bot_user: discord.User, channel: discord.TextChannel):
|
||||||
# First, check if we have one saved in the DB
|
# First, check if we have one saved in the DB
|
||||||
webhook_from_db = await db.get_webhook(conn, channel.id)
|
webhook_from_db = await db.get_webhook(conn, channel.id)
|
||||||
@ -46,7 +45,8 @@ async def get_or_create_webhook_for_channel(conn, bot_user: discord.User, channe
|
|||||||
# If not, we create one and save it
|
# If not, we create one and save it
|
||||||
created_webhook = await channel.create_webhook(name="PluralKit Proxy Webhook")
|
created_webhook = await channel.create_webhook(name="PluralKit Proxy Webhook")
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
raise ProxyError("PluralKit does not have the \"Manage Webhooks\" permission, and thus cannot proxy your message. Please contact a server administrator.")
|
raise ProxyError(
|
||||||
|
"PluralKit does not have the \"Manage Webhooks\" permission, and thus cannot proxy your message. Please contact a server administrator.")
|
||||||
|
|
||||||
await db.add_webhook(conn, channel.id, created_webhook.id, created_webhook.token)
|
await db.add_webhook(conn, channel.id, created_webhook.id, created_webhook.token)
|
||||||
return fix_webhook(created_webhook)
|
return fix_webhook(created_webhook)
|
||||||
@ -74,9 +74,13 @@ async def send_proxy_message(conn, original_message: discord.Message, system: Sy
|
|||||||
# Bounds check the combined name to avoid silent erroring
|
# Bounds check the combined name to avoid silent erroring
|
||||||
full_username = "{} {}".format(member.name, system.tag or "").strip()
|
full_username = "{} {}".format(member.name, system.tag or "").strip()
|
||||||
if len(full_username) < 2:
|
if len(full_username) < 2:
|
||||||
raise ProxyError("The webhook's name, `{}`, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag.".format(full_username))
|
raise ProxyError(
|
||||||
|
"The webhook's name, `{}`, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag.".format(
|
||||||
|
full_username))
|
||||||
if len(full_username) > 32:
|
if len(full_username) > 32:
|
||||||
raise ProxyError("The webhook's name, `{}`, is longer than 32 characters, and thus cannot be proxied. Please change the member name or use a shorter system tag.".format(full_username))
|
raise ProxyError(
|
||||||
|
"The webhook's name, `{}`, is longer than 32 characters, and thus cannot be proxied. Please change the member name or use a shorter system tag.".format(
|
||||||
|
full_username))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sent_message = await webhook.send(
|
sent_message = await webhook.send(
|
||||||
@ -122,7 +126,8 @@ async def send_proxy_message(conn, original_message: discord.Message, system: Sy
|
|||||||
try:
|
try:
|
||||||
await original_message.delete()
|
await original_message.delete()
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
raise ProxyError("PluralKit does not have permission to delete user messages. Please contact a server administrator.")
|
raise ProxyError(
|
||||||
|
"PluralKit does not have permission to delete user messages. Please contact a server administrator.")
|
||||||
|
|
||||||
|
|
||||||
async def try_proxy_message(conn, message: discord.Message, logger: ChannelLogger, bot_user: discord.User) -> bool:
|
async def try_proxy_message(conn, message: discord.Message, logger: ChannelLogger, bot_user: discord.User) -> bool:
|
||||||
@ -153,7 +158,7 @@ async def try_proxy_message(conn, message: discord.Message, logger: ChannelLogge
|
|||||||
# So, we now have enough information to successfully proxy a message
|
# So, we now have enough information to successfully proxy a message
|
||||||
async with conn.transaction():
|
async with conn.transaction():
|
||||||
try:
|
try:
|
||||||
await send_proxy_message(conn, message, system, member, inner_message, logger, bot_user)
|
await send_proxy_message(conn, message, system, member, inner_message, logger, bot_user)
|
||||||
except ProxyError as e:
|
except ProxyError as e:
|
||||||
await message.channel.send("\u274c {}".format(str(e)))
|
await message.channel.send("\u274c {}".format(str(e)))
|
||||||
|
|
||||||
@ -205,4 +210,4 @@ async def try_delete_by_reaction(conn, client: discord.Client, message_id: int,
|
|||||||
# Then delete the original message
|
# Then delete the original message
|
||||||
await original_message.delete()
|
await original_message.delete()
|
||||||
|
|
||||||
await handle_deleted_message(conn, client, message_id, original_message.content, logger)
|
await handle_deleted_message(conn, client, message_id, original_message.content, logger)
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
|
import discord
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import discord
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pluralkit import db
|
from pluralkit import db
|
||||||
from pluralkit.system import System
|
|
||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
|
from pluralkit.system import System
|
||||||
|
|
||||||
logger = logging.getLogger("pluralkit.utils")
|
logger = logging.getLogger("pluralkit.utils")
|
||||||
|
|
||||||
@ -21,7 +20,8 @@ def bounds_check_member_name(new_name, system_tag):
|
|||||||
|
|
||||||
if system_tag:
|
if system_tag:
|
||||||
if len("{} {}".format(new_name, system_tag)) > 32:
|
if len("{} {}".format(new_name, system_tag)) > 32:
|
||||||
return "This name, combined with the system tag ({}), would exceed the maximum length of 32 characters. Please reduce the length of the tag, or use a shorter name.".format(system_tag)
|
return "This name, combined with the system tag ({}), would exceed the maximum length of 32 characters. Please reduce the length of the tag, or use a shorter name.".format(
|
||||||
|
system_tag)
|
||||||
|
|
||||||
|
|
||||||
async def parse_mention(client: discord.Client, mention: str) -> Optional[discord.User]:
|
async def parse_mention(client: discord.Client, mention: str) -> Optional[discord.User]:
|
||||||
@ -39,18 +39,19 @@ async def parse_mention(client: discord.Client, mention: str) -> Optional[discor
|
|||||||
except (ValueError, discord.NotFound):
|
except (ValueError, discord.NotFound):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_channel_mention(mention: str, server: discord.Guild) -> Optional[discord.TextChannel]:
|
def parse_channel_mention(mention: str, server: discord.Guild) -> Optional[discord.TextChannel]:
|
||||||
match = re.fullmatch("<#(\\d+)>", mention)
|
match = re.fullmatch("<#(\\d+)>", mention)
|
||||||
if match:
|
if match:
|
||||||
return server.get_channel(int(match.group(1)))
|
return server.get_channel(int(match.group(1)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return server.get_channel(int(mention))
|
return server.get_channel(int(mention))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def get_system_fuzzy(conn, client: discord.Client, key) -> System:
|
async def get_system_fuzzy(conn, client: discord.Client, key) -> Optional[System]:
|
||||||
if isinstance(key, discord.User):
|
if isinstance(key, discord.User):
|
||||||
return await db.get_system_by_account(conn, account_id=key.id)
|
return await db.get_system_by_account(conn, account_id=key.id)
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ async def get_member_fuzzy(conn, system_id: int, key: str, system_only=True) ->
|
|||||||
if member is not None:
|
if member is not None:
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
def sanitize(text):
|
def sanitize(text):
|
||||||
# Insert a zero-width space in @everyone so it doesn't trigger
|
# Insert a zero-width space in @everyone so it doesn't trigger
|
||||||
return text.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere")
|
return text.replace("@everyone", "@\u200beveryone").replace("@here", "@\u200bhere")
|
||||||
|
@ -294,6 +294,11 @@ async def add_switch_member(conn, switch_id: int, member_id: int):
|
|||||||
logger.debug("Adding switch member (switch={}, member={})".format(switch_id, member_id))
|
logger.debug("Adding switch member (switch={}, member={})".format(switch_id, member_id))
|
||||||
await conn.execute("insert into switch_members (switch, member) values ($1, $2)", switch_id, member_id)
|
await conn.execute("insert into switch_members (switch, member) values ($1, $2)", switch_id, member_id)
|
||||||
|
|
||||||
|
@db_wrap
|
||||||
|
async def delete_switch(conn, switch_id: int):
|
||||||
|
logger.debug("Deleting switch (id={})".format(switch_id))
|
||||||
|
await conn.execute("delete from switches where id = $1", switch_id)
|
||||||
|
|
||||||
@db_wrap
|
@db_wrap
|
||||||
async def get_server_info(conn, server_id: int):
|
async def get_server_info(conn, server_id: int):
|
||||||
return await conn.fetchrow("select * from servers where id = $1", server_id)
|
return await conn.fetchrow("select * from servers where id = $1", server_id)
|
||||||
|
@ -106,6 +106,8 @@ class Member(namedtuple("Member",
|
|||||||
async def set_birthdate(self, conn, new_date: Union[date, str]):
|
async def set_birthdate(self, conn, new_date: Union[date, str]):
|
||||||
"""
|
"""
|
||||||
Set or clear the birthdate of a member. To hide the birth year, pass a year of 0001.
|
Set or clear the birthdate of a member. To hide the birth year, pass a year of 0001.
|
||||||
|
|
||||||
|
If passed a string, will attempt to parse the string as a date.
|
||||||
:raises: InvalidDateStringError
|
:raises: InvalidDateStringError
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@ -7,5 +8,13 @@ from pluralkit.member import Member
|
|||||||
|
|
||||||
|
|
||||||
class Switch(namedtuple("Switch", ["id", "system", "timestamp", "members"])):
|
class Switch(namedtuple("Switch", ["id", "system", "timestamp", "members"])):
|
||||||
|
id: int
|
||||||
|
system: int
|
||||||
|
timestamp: datetime
|
||||||
|
members: List[int]
|
||||||
|
|
||||||
async def fetch_members(self, conn) -> List[Member]:
|
async def fetch_members(self, conn) -> List[Member]:
|
||||||
return await db.get_members(conn, self.members)
|
return await db.get_members(conn, self.members)
|
||||||
|
|
||||||
|
async def delete(self, conn):
|
||||||
|
await db.delete_switch(conn, self.id)
|
@ -128,6 +128,14 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def add_switch(self, conn, members: List[Member]):
|
||||||
|
async with conn.transaction():
|
||||||
|
switch_id = await db.add_switch(conn, self.id)
|
||||||
|
|
||||||
|
# TODO: batch query here
|
||||||
|
for member in members:
|
||||||
|
await db.add_switch_member(conn, switch_id, member.id)
|
||||||
|
|
||||||
def get_member_name_limit(self) -> int:
|
def get_member_name_limit(self) -> int:
|
||||||
"""Returns the maximum length a member's name or nickname is allowed to be in order for the member to be proxied. Depends on the system tag."""
|
"""Returns the maximum length a member's name or nickname is allowed to be in order for the member to be proxied. Depends on the system tag."""
|
||||||
if self.tag:
|
if self.tag:
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
|
import humanize
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pluralkit import db
|
from pluralkit import db
|
||||||
from pluralkit.errors import InvalidAvatarURLError
|
from pluralkit.errors import InvalidAvatarURLError
|
||||||
|
|
||||||
|
|
||||||
def fix_time(time: datetime):
|
def display_relative(time: Union[datetime, timedelta]) -> str:
|
||||||
"""Convert a naive datetime from UTC to local time. humanize's methods expect a local naive time and not a time in UTC."""
|
if isinstance(time, datetime):
|
||||||
# TODO: replace with methods that call humanize directly, to hide implementation details
|
time = datetime.utcnow() - time
|
||||||
return time.replace(tzinfo=timezone.utc).astimezone().replace(tzinfo=None)
|
return humanize.naturaldelta(time)
|
||||||
|
|
||||||
|
|
||||||
async def get_fronter_ids(conn, system_id) -> (List[int], datetime):
|
async def get_fronter_ids(conn, system_id) -> (List[int], datetime):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user