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 asyncio
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
import asyncpg
|
||||
import discord
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from pluralkit import db
|
||||
@ -136,4 +134,4 @@ def run():
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
client.run(bot_token)
|
||||
client.run(bot_token)
|
||||
|
@ -1,8 +1,7 @@
|
||||
import discord
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
|
||||
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_footer(
|
||||
text="System ID: {} | Member ID: {} | Sender: {}#{} ({}) | Message ID: {}".format(system_hid, member_hid,
|
||||
sender_name, sender_disc,
|
||||
sender_id, message_id))
|
||||
sender_name, sender_disc,
|
||||
sender_id, message_id))
|
||||
|
||||
if message_image:
|
||||
embed.set_thumbnail(url=message_image)
|
||||
@ -99,6 +98,7 @@ class ChannelLogger:
|
||||
embed.timestamp = datetime.utcnow()
|
||||
|
||||
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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
import logging
|
||||
import re
|
||||
from typing import Tuple, Optional, Union
|
||||
|
||||
@ -10,8 +10,6 @@ from pluralkit.errors import PluralKitError
|
||||
from pluralkit.member import Member
|
||||
from pluralkit.system import System
|
||||
|
||||
logger = logging.getLogger("pluralkit.bot.commands")
|
||||
|
||||
|
||||
def next_arg(arg_string: str) -> Tuple[str, Optional[str]]:
|
||||
# A basic quoted-arg parser
|
||||
@ -68,6 +66,19 @@ class CommandContext:
|
||||
popped, self.args = next_arg(self.args)
|
||||
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:
|
||||
name = self.pop_str(error)
|
||||
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):
|
||||
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):
|
||||
await message.add_reaction("\u2705") # Checkmark
|
||||
await message.add_reaction("\u274c") # Red X
|
||||
@ -138,6 +156,35 @@ import pluralkit.bot.commands.switch_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):
|
||||
# lol nested try
|
||||
try:
|
||||
@ -150,71 +197,20 @@ async def run_command(ctx: CommandContext, func):
|
||||
await ctx.reply(content=content, embed=embed)
|
||||
|
||||
|
||||
|
||||
async def command_dispatch(client: discord.Client, message: discord.Message, conn) -> bool:
|
||||
prefix = "^(pk(;|!)|<@{}> )".format(client.user.id)
|
||||
commands = [
|
||||
(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),
|
||||
regex = re.compile(prefix, re.IGNORECASE)
|
||||
|
||||
(r"front", system_commands.system_fronter),
|
||||
(r"front history", system_commands.system_fronthistory),
|
||||
(r"front percent(age)?", system_commands.system_frontpercent),
|
||||
|
||||
(r"import tupperware", import_commands.import_tupperware),
|
||||
|
||||
(r"member (new|create|add|register)", member_commands.new_member),
|
||||
(r"member set", member_commands.member_set),
|
||||
(r"member (name|rename)", member_commands.member_name),
|
||||
(r"member description", member_commands.member_description),
|
||||
(r"member avatar", member_commands.member_avatar),
|
||||
(r"member color", member_commands.member_color),
|
||||
(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
|
||||
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, command_root)
|
||||
return True
|
||||
return False
|
||||
|
@ -1,18 +1,16 @@
|
||||
import logging
|
||||
from discord import DMChannel
|
||||
|
||||
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`."
|
||||
|
||||
async def reply_dm(ctx: CommandContext, message: str):
|
||||
await ctx.message.author.send(message)
|
||||
|
||||
if not isinstance(ctx.message.channel, DMChannel):
|
||||
await ctx.reply_ok("DM'd!")
|
||||
async def token_root(ctx: CommandContext):
|
||||
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()
|
||||
|
||||
if system.token:
|
||||
@ -21,11 +19,13 @@ async def get_token(ctx: CommandContext):
|
||||
token = await system.refresh_token(ctx.conn)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
return await reply_dm(ctx, token_message)
|
||||
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)
|
||||
return await ctx.reply_ok_dm(token_message)
|
||||
|
@ -1,16 +1,17 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
import pluralkit.utils
|
||||
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):
|
||||
tupperware_member = ctx.message.guild.get_member(431544605209788416)
|
||||
|
||||
# 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:
|
||||
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
|
||||
raise CommandError("This command only works in a channel where the Tupperware bot has read/send access.")
|
||||
|
||||
await ctx.reply(
|
||||
embed=embeds.status("Please reply to this message with `tul!list` (or the server equivalent)."))
|
||||
await ctx.reply("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
|
||||
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 new_page != current_page:
|
||||
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(
|
||||
len(pages_found), total_pages))
|
||||
await status_msg.edit(
|
||||
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
|
||||
|
||||
# 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
|
||||
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
|
||||
system = await ctx.get_system()
|
||||
if system is None:
|
||||
hid = pluralkit.utils.generate_hid()
|
||||
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)
|
||||
system = await System.create_system(ctx.conn, ctx.message.author.id)
|
||||
|
||||
for embed in tupperware_page_embeds:
|
||||
for field in embed["fields"]:
|
||||
@ -133,24 +129,16 @@ async def import_tupperware(ctx: CommandContext):
|
||||
member_description = line
|
||||
|
||||
# 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:
|
||||
# Or create a new member
|
||||
hid = pluralkit.utils.generate_hid()
|
||||
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)
|
||||
existing_member = await system.create_member(ctx.conn, name)
|
||||
|
||||
# Save the new stuff in the DB
|
||||
logger.debug("Updating fields...")
|
||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="prefix", value=member_prefix)
|
||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="suffix", value=member_suffix)
|
||||
await db.update_member_field(ctx.conn, member_id=existing_member.id, field="avatar_url",
|
||||
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 existing_member.set_proxy_tags(ctx.conn, member_prefix, member_suffix)
|
||||
await existing_member.set_avatar(ctx.conn, member_avatar)
|
||||
await existing_member.set_birthdate(ctx.conn, member_birthdate)
|
||||
await existing_member.set_description(ctx.conn, member_description)
|
||||
|
||||
await ctx.reply_ok(
|
||||
"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.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):
|
||||
member = await ctx.pop_member(
|
||||
error=CommandError("You must pass a member name or ID.", help=help.lookup_member), system_only=False)
|
||||
async def specific_member_root(ctx: CommandContext):
|
||||
member = await ctx.pop_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))
|
||||
|
||||
|
||||
async def new_member(ctx: CommandContext):
|
||||
system = await ctx.ensure_system()
|
||||
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()
|
||||
|
||||
existing_member = await Member.get_member_by_name(ctx.conn, system.id, new_name)
|
||||
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):
|
||||
raise CommandError("Member creation cancelled.")
|
||||
|
||||
@ -37,18 +86,19 @@ async def new_member(ctx: CommandContext):
|
||||
|
||||
async def member_set(ctx: CommandContext):
|
||||
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()
|
||||
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.", help=help.edit_member))
|
||||
new_name = ctx.pop_str(CommandError("You must pass a new member name."))
|
||||
|
||||
# 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)
|
||||
if existing_member:
|
||||
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))
|
||||
if existing_member and existing_member.id != member.id:
|
||||
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):
|
||||
raise CommandError("Member renaming cancelled.")
|
||||
|
||||
@ -56,27 +106,28 @@ async def member_name(ctx: CommandContext):
|
||||
await ctx.reply_ok("Member name updated.")
|
||||
|
||||
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:
|
||||
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():
|
||||
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):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
||||
async def member_description(ctx: CommandContext, member: Member):
|
||||
new_description = ctx.remaining() or None
|
||||
|
||||
await member.set_description(ctx.conn, new_description)
|
||||
await ctx.reply_ok("Member description {}.".format("updated" if new_description else "cleared"))
|
||||
|
||||
|
||||
async def member_avatar(ctx: CommandContext):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
||||
async def member_avatar(ctx: CommandContext, member: Member):
|
||||
new_avatar_url = ctx.remaining() or None
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
async def member_color(ctx: CommandContext):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
||||
async def member_color(ctx: CommandContext, member: Member):
|
||||
new_color = ctx.remaining() or None
|
||||
|
||||
await member.set_color(ctx.conn, new_color)
|
||||
await ctx.reply_ok("Member color {}.".format("updated" if new_color else "cleared"))
|
||||
|
||||
|
||||
async def member_pronouns(ctx: CommandContext):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
||||
async def member_pronouns(ctx: CommandContext, member: Member):
|
||||
new_pronouns = ctx.remaining() or None
|
||||
|
||||
await member.set_pronouns(ctx.conn, new_pronouns)
|
||||
await ctx.reply_ok("Member pronouns {}.".format("updated" if new_pronouns else "cleared"))
|
||||
|
||||
|
||||
async def member_birthdate(ctx: CommandContext):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member))
|
||||
async def member_birthdate(ctx: CommandContext, member: Member):
|
||||
new_birthdate = ctx.remaining() or None
|
||||
|
||||
await member.set_birthdate(ctx.conn, new_birthdate)
|
||||
await ctx.reply_ok("Member birthdate {}.".format("updated" if new_birthdate else "cleared"))
|
||||
|
||||
|
||||
async def member_proxy(ctx: CommandContext):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.member_proxy))
|
||||
|
||||
async def member_proxy(ctx: CommandContext, member: Member):
|
||||
if not ctx.has_next():
|
||||
prefix, suffix = None, None
|
||||
else:
|
||||
# Sanity checking
|
||||
example = ctx.remaining()
|
||||
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:
|
||||
raise CommandError("Example proxy message must contain the string 'text' exactly once.",
|
||||
help=help.member_proxy)
|
||||
raise CommandError("Example proxy message must contain the string 'text' exactly once. For help, type `pk;help proxy`.")
|
||||
|
||||
# Extract prefix and suffix
|
||||
prefix = example[:example.index("text")].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
|
||||
if not prefix:
|
||||
@ -144,13 +184,11 @@ async def member_proxy(ctx: CommandContext):
|
||||
|
||||
async with ctx.conn.transaction():
|
||||
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):
|
||||
await ctx.ensure_system()
|
||||
member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.remove_member))
|
||||
|
||||
async def member_delete(ctx: CommandContext, member: Member):
|
||||
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)
|
||||
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 *
|
||||
|
||||
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):
|
||||
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:
|
||||
mid = int(mid_str)
|
||||
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
|
||||
message = await db.get_message(ctx.conn, mid)
|
||||
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
|
||||
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)
|
||||
await ctx.reply_ok(embed=await embeds.message_card(ctx.client, message))
|
||||
|
@ -1,61 +1,57 @@
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from discord.utils import oauth_url
|
||||
|
||||
import pluralkit.utils
|
||||
from pluralkit.bot import utils, embeds
|
||||
from pluralkit.bot import help
|
||||
from pluralkit.bot.commands import *
|
||||
|
||||
logger = logging.getLogger("pluralkit.commands")
|
||||
|
||||
|
||||
async def show_help(ctx: CommandContext):
|
||||
embed = embeds.status("")
|
||||
embed.title = "PluralKit Help"
|
||||
embed.set_footer(text="By Astrid (Ske#6201; pk;member qoxvy) | GitHub: https://github.com/xSke/PluralKit/")
|
||||
|
||||
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
|
||||
async def help_root(ctx: CommandContext):
|
||||
if ctx.match("commands"):
|
||||
await ctx.reply(help.all_commands)
|
||||
elif ctx.match("proxy"):
|
||||
await ctx.reply(help.proxy_guide)
|
||||
else:
|
||||
raise CommandError("Unknown help page '{}'.".format(category))
|
||||
|
||||
await ctx.reply(embed=embed)
|
||||
await ctx.reply(help.root)
|
||||
|
||||
|
||||
async def invite_link(ctx: CommandContext):
|
||||
client_id = os.environ["CLIENT_ID"]
|
||||
|
||||
permissions = discord.Permissions()
|
||||
|
||||
# So the bot can actually add the webhooks it needs to do the proxy functionality
|
||||
permissions.manage_webhooks = True
|
||||
|
||||
# So the bot can respond with status, error, and success messages
|
||||
permissions.send_messages = True
|
||||
|
||||
# So the bot can delete channels
|
||||
permissions.manage_messages = True
|
||||
|
||||
# So the bot can respond with extended embeds, ex. member cards
|
||||
permissions.embed_links = True
|
||||
|
||||
# So the bot can send images too
|
||||
permissions.attach_files = True
|
||||
|
||||
# (unsure if it needs this, actually, might be necessary for message lookup)
|
||||
permissions.read_message_history = True
|
||||
|
||||
# So the bot can add reactions for confirm/deny prompts
|
||||
permissions.add_reactions = True
|
||||
|
||||
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))
|
||||
|
||||
|
||||
async def export(ctx: CommandContext):
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
members = await db.get_all_members(ctx.conn, system.id)
|
||||
accounts = await db.get_linked_accounts(ctx.conn, system.id)
|
||||
switches = await pluralkit.utils.get_front_history(ctx.conn, system.id, 999999)
|
||||
members = await system.get_members(ctx.conn)
|
||||
accounts = await system.get_linked_account_ids(ctx.conn)
|
||||
switches = await system.get_switches(ctx.conn, 999999)
|
||||
|
||||
data = {
|
||||
"name": system.name,
|
||||
@ -81,10 +77,10 @@ async def export(ctx: CommandContext):
|
||||
"accounts": [str(uid) for uid in accounts],
|
||||
"switches": [
|
||||
{
|
||||
"timestamp": timestamp.isoformat(),
|
||||
"members": [member.hid for member in members]
|
||||
} for timestamp, members in switches
|
||||
]
|
||||
"timestamp": switch.timestamp.isoformat(),
|
||||
"members": [member.hid for member in await switch.fetch_members(ctx.conn)]
|
||||
} for switch in switches
|
||||
] # TODO: messages
|
||||
}
|
||||
|
||||
f = io.BytesIO(json.dumps(data).encode("utf-8"))
|
||||
|
@ -1,7 +1,5 @@
|
||||
from pluralkit.bot.commands import *
|
||||
|
||||
logger = logging.getLogger("pluralkit.commands")
|
||||
|
||||
|
||||
async def set_log(ctx: CommandContext):
|
||||
if not ctx.message.author.guild_permissions.administrator:
|
||||
|
@ -1,5 +1,4 @@
|
||||
import dateparser
|
||||
import humanize
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
@ -7,24 +6,34 @@ import pluralkit.utils
|
||||
from pluralkit.bot import help
|
||||
from pluralkit.bot.commands import *
|
||||
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):
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
if not ctx.has_next():
|
||||
raise CommandError("You must pass at least one member name or ID to register a switch to.",
|
||||
help=help.switch_register)
|
||||
raise CommandError("You must pass at least one member name or ID to register a switch to.")
|
||||
|
||||
members: List[Member] = []
|
||||
for member_name in ctx.remaining().split(" "):
|
||||
# Find the 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)
|
||||
while ctx.has_next():
|
||||
members.append(await ctx.pop_member())
|
||||
|
||||
# Compare requested switch IDs and existing fronter IDs to check for existing switches
|
||||
# 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.")
|
||||
|
||||
# Log the switch
|
||||
async with ctx.conn.transaction():
|
||||
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)
|
||||
await system.add_switch(ctx.conn, members)
|
||||
|
||||
if len(members) == 1:
|
||||
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):
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
# Get current fronters
|
||||
fronters, _ = await pluralkit.utils.get_fronter_ids(ctx.conn, system_id=system.id)
|
||||
if not fronters:
|
||||
switch = await system.get_latest_switch(ctx.conn)
|
||||
if switch and not switch.members:
|
||||
raise CommandError("There's already no one in front.")
|
||||
|
||||
# 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.")
|
||||
|
||||
|
||||
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):
|
||||
system = await ctx.ensure_system()
|
||||
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
|
||||
new_time = dateparser.parse(ctx.remaining(), languages=["en"], settings={
|
||||
@ -76,11 +114,11 @@ async def switch_move(ctx: CommandContext):
|
||||
"RETURN_AS_TIMEZONE_AWARE": False
|
||||
})
|
||||
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
|
||||
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
|
||||
async with ctx.conn.transaction():
|
||||
@ -94,19 +132,25 @@ async def switch_move(ctx: CommandContext):
|
||||
second_last_timestamp, _ = last_two_switches[1]
|
||||
|
||||
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(
|
||||
"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
|
||||
members = ", ".join([member.name for member in last_fronters]) or "nobody"
|
||||
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_relative = humanize.naturaltime(pluralkit.utils.fix_time(new_time))
|
||||
new_relative = display_relative(new_time)
|
||||
|
||||
# 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):
|
||||
raise CommandError("Switch move cancelled.")
|
||||
|
||||
@ -116,6 +160,3 @@ async def switch_move(ctx: CommandContext):
|
||||
# Change the switch in the DB
|
||||
await db.move_last_switch(ctx.conn, system.id, switch_id, new_time)
|
||||
await ctx.reply_ok("Switch moved.")
|
||||
|
||||
|
||||
|
||||
|
@ -3,28 +3,73 @@ import humanize
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pluralkit.bot.embeds
|
||||
import pluralkit.utils
|
||||
from pluralkit.bot import help
|
||||
from pluralkit.bot.commands import *
|
||||
from pluralkit.errors import ExistingSystemError, UnlinkingLastAccountError, PluralKitError, AccountAlreadyLinkedError
|
||||
|
||||
logger = logging.getLogger("pluralkit.commands")
|
||||
from pluralkit.errors import ExistingSystemError, UnlinkingLastAccountError, AccountAlreadyLinkedError
|
||||
from pluralkit.utils import display_relative
|
||||
|
||||
|
||||
async def system_info(ctx: CommandContext):
|
||||
if ctx.has_next():
|
||||
system = await ctx.pop_system()
|
||||
async def system_root(ctx: CommandContext):
|
||||
# Commands that operate without a specified system (usually defaults to the executor's own 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:
|
||||
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))
|
||||
|
||||
|
||||
async def new_system(ctx: CommandContext):
|
||||
system_name = ctx.remaining() or None
|
||||
async def system_new(ctx: CommandContext):
|
||||
new_name = ctx.remaining() or None
|
||||
|
||||
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:
|
||||
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"))
|
||||
|
||||
|
||||
async def system_link(ctx: CommandContext):
|
||||
async def account_link(ctx: CommandContext):
|
||||
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)
|
||||
# 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
|
||||
linkee = await utils.parse_mention(ctx.client, account_name)
|
||||
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
|
||||
account_system = await System.get_by_account(ctx.conn, linkee.id)
|
||||
if account_system:
|
||||
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):
|
||||
raise CommandError("Account link cancelled.")
|
||||
|
||||
@ -120,7 +167,7 @@ async def system_link(ctx: CommandContext):
|
||||
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()
|
||||
|
||||
try:
|
||||
@ -131,24 +178,18 @@ async def system_unlink(ctx: CommandContext):
|
||||
await ctx.reply_ok("Account unlinked.")
|
||||
|
||||
|
||||
async def system_fronter(ctx: CommandContext):
|
||||
if ctx.has_next():
|
||||
system = await ctx.pop_system()
|
||||
else:
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
async def system_fronter(ctx: CommandContext, system: System):
|
||||
embed = await embeds.front_status(await system.get_latest_switch(ctx.conn), ctx.conn)
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
|
||||
async def system_fronthistory(ctx: CommandContext):
|
||||
if ctx.has_next():
|
||||
system = await ctx.pop_system()
|
||||
else:
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
async def system_fronthistory(ctx: CommandContext, system: System):
|
||||
lines = []
|
||||
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):
|
||||
# Special case when no one's fronting
|
||||
if len(members) == 0:
|
||||
@ -158,13 +199,13 @@ async def system_fronthistory(ctx: CommandContext):
|
||||
|
||||
# Make proper date string
|
||||
time_text = timestamp.isoformat(sep=" ", timespec="seconds")
|
||||
rel_text = humanize.naturaltime(pluralkit.utils.fix_time(timestamp))
|
||||
rel_text = display_relative(timestamp)
|
||||
|
||||
delta_text = ""
|
||||
if i > 0:
|
||||
last_switch_time = front_history[i - 1][0]
|
||||
delta_text = ", for {}".format(humanize.naturaldelta(timestamp - last_switch_time))
|
||||
lines.append("**{}** ({}, {}{})".format(name, time_text, rel_text, delta_text))
|
||||
delta_text = ", for {}".format(display_relative(timestamp - last_switch_time))
|
||||
lines.append("**{}** ({}, {} ago{})".format(name, time_text, rel_text, delta_text))
|
||||
|
||||
embed = embeds.status("\n".join(lines) or "(none)")
|
||||
embed.title = "Past switches"
|
||||
@ -183,9 +224,7 @@ async def system_delete(ctx: CommandContext):
|
||||
await ctx.reply_ok("System deleted.")
|
||||
|
||||
|
||||
async def system_frontpercent(ctx: CommandContext):
|
||||
system = await ctx.ensure_system()
|
||||
|
||||
async def system_frontpercent(ctx: CommandContext, system: System):
|
||||
# Parse the time limit (will go this far back)
|
||||
if ctx.remaining():
|
||||
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)",
|
||||
value="{}% ({})".format(percent, humanize.naturaldelta(front_time)))
|
||||
|
||||
embed.set_footer(text="Since {} ({})".format(span_start.isoformat(sep=" ", timespec="seconds"),
|
||||
humanize.naturaltime(pluralkit.utils.fix_time(span_start))))
|
||||
embed.set_footer(text="Since {} ({} ago)".format(span_start.isoformat(sep=" ", timespec="seconds"),
|
||||
display_relative(span_start)))
|
||||
await ctx.reply(embed=embed)
|
||||
|
@ -2,12 +2,12 @@ import discord
|
||||
import humanize
|
||||
from typing import Tuple
|
||||
|
||||
import pluralkit
|
||||
from pluralkit import db
|
||||
from pluralkit.bot.utils import escape
|
||||
from pluralkit.member import Member
|
||||
from pluralkit.switch import Switch
|
||||
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:
|
||||
@ -52,7 +52,8 @@ def status(text: str) -> discord.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.colour = discord.Colour.dark_red()
|
||||
embed.title = truncate_title(message_content)
|
||||
@ -82,7 +83,8 @@ async def system_card(conn, client: discord.Client, system: System) -> discord.E
|
||||
if fronters:
|
||||
names = ", ".join([member.name for member in fronters])
|
||||
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 = []
|
||||
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:
|
||||
embed.add_field(name="Since",
|
||||
value="{} ({})".format(switch.timestamp.isoformat(sep=" ", timespec="seconds"),
|
||||
humanize.naturaltime(pluralkit.utils.fix_time(switch.timestamp))))
|
||||
display_relative(switch.timestamp)))
|
||||
else:
|
||||
embed = error("No switches logged.")
|
||||
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.
|
||||
`pk;help member` - Details on member configuration.
|
||||
`pk;help proxy` - Details on message proxying.
|
||||
`pk;help switch` - Details on switch logging.
|
||||
`pk;help mod` - Details on moderator operations.
|
||||
`pk;help import` - Details on data import from other services.""")
|
||||
getting_started = ("Getting started", """To get started using the bot, try running the following commands:
|
||||
all_commands = """
|
||||
**All 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;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
|
||||
**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
|
||||
**4**. You're done! See the other help pages for more commands.""")
|
||||
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.
|
||||
**3**. `pk;member John proxy [text]` - Set up square brackets as proxy tags
|
||||
**4**. You're done!
|
||||
|
||||
If you don't specify any value, the property will be cleared.""")
|
||||
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 example:
|
||||
`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
|
||||
]
|
||||
}
|
||||
**More information**
|
||||
For a full list of commands, type `pk;help commands`.
|
||||
For a more in-depth explanation of proxying, type `pk;help proxy`.
|
||||
If you're an existing user of the Tupperware proxy bot, type `pk;import` to import your data from there.
|
||||
"""
|
||||
|
@ -1,5 +1,4 @@
|
||||
import discord
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
@ -9,7 +8,6 @@ from pluralkit.bot.channel_logger import ChannelLogger
|
||||
from pluralkit.member import Member
|
||||
from pluralkit.system import System
|
||||
|
||||
logger = logging.getLogger("pluralkit.bot.proxy")
|
||||
|
||||
class ProxyError(Exception):
|
||||
pass
|
||||
@ -21,6 +19,7 @@ def fix_webhook(webhook: discord.Webhook) -> discord.Webhook:
|
||||
webhook._adapter.http = None
|
||||
return webhook
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
created_webhook = await channel.create_webhook(name="PluralKit Proxy Webhook")
|
||||
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)
|
||||
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
|
||||
full_username = "{} {}".format(member.name, system.tag or "").strip()
|
||||
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:
|
||||
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:
|
||||
sent_message = await webhook.send(
|
||||
@ -122,7 +126,8 @@ async def send_proxy_message(conn, original_message: discord.Message, system: Sy
|
||||
try:
|
||||
await original_message.delete()
|
||||
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:
|
||||
@ -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
|
||||
async with conn.transaction():
|
||||
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:
|
||||
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
|
||||
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 re
|
||||
|
||||
import discord
|
||||
from typing import Optional
|
||||
|
||||
from pluralkit import db
|
||||
from pluralkit.system import System
|
||||
from pluralkit.member import Member
|
||||
from pluralkit.system import System
|
||||
|
||||
logger = logging.getLogger("pluralkit.utils")
|
||||
|
||||
@ -21,7 +20,8 @@ def bounds_check_member_name(new_name, system_tag):
|
||||
|
||||
if system_tag:
|
||||
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]:
|
||||
@ -39,18 +39,19 @@ async def parse_mention(client: discord.Client, mention: str) -> Optional[discor
|
||||
except (ValueError, discord.NotFound):
|
||||
return None
|
||||
|
||||
|
||||
def parse_channel_mention(mention: str, server: discord.Guild) -> Optional[discord.TextChannel]:
|
||||
match = re.fullmatch("<#(\\d+)>", mention)
|
||||
if match:
|
||||
return server.get_channel(int(match.group(1)))
|
||||
|
||||
|
||||
try:
|
||||
return server.get_channel(int(mention))
|
||||
except ValueError:
|
||||
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):
|
||||
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:
|
||||
return member
|
||||
|
||||
|
||||
def sanitize(text):
|
||||
# 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))
|
||||
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
|
||||
async def get_server_info(conn, server_id: int):
|
||||
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]):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
|
||||
from typing import List
|
||||
|
||||
@ -7,5 +8,13 @@ from pluralkit.member import Member
|
||||
|
||||
|
||||
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]:
|
||||
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:
|
||||
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:
|
||||
"""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:
|
||||
|
@ -1,19 +1,20 @@
|
||||
import humanize
|
||||
import re
|
||||
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Tuple
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import List, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pluralkit import db
|
||||
from pluralkit.errors import InvalidAvatarURLError
|
||||
|
||||
|
||||
def fix_time(time: datetime):
|
||||
"""Convert a naive datetime from UTC to local time. humanize's methods expect a local naive time and not a time in UTC."""
|
||||
# TODO: replace with methods that call humanize directly, to hide implementation details
|
||||
return time.replace(tzinfo=timezone.utc).astimezone().replace(tzinfo=None)
|
||||
def display_relative(time: Union[datetime, timedelta]) -> str:
|
||||
if isinstance(time, datetime):
|
||||
time = datetime.utcnow() - time
|
||||
return humanize.naturaldelta(time)
|
||||
|
||||
|
||||
async def get_fronter_ids(conn, system_id) -> (List[int], datetime):
|
||||
|
Loading…
Reference in New Issue
Block a user