2018-08-02 11:10:09 +02:00
import dateparser
2018-07-24 22:47:57 +02:00
import humanize
2018-09-07 17:34:38 +02:00
from datetime import datetime
from urllib . parse import urlparse
2018-07-24 22:47:57 +02:00
2018-08-02 00:36:50 +02:00
import pluralkit . utils
2018-09-07 17:34:38 +02:00
from pluralkit . bot import help
2018-07-24 22:47:57 +02:00
from pluralkit . bot . commands import *
logger = logging . getLogger ( " pluralkit.commands " )
2018-09-07 17:34:38 +02:00
async def system_info ( ctx : CommandContext ) :
if ctx . has_next ( ) :
system = await ctx . pop_system ( )
else :
system = await ctx . ensure_system ( )
2018-08-02 11:10:09 +02:00
2018-07-24 22:47:57 +02:00
await ctx . reply ( embed = await utils . generate_system_info_card ( ctx . conn , ctx . client , system ) )
2018-09-07 17:34:38 +02:00
async def new_system ( ctx : CommandContext ) :
system = await ctx . get_system ( )
if system :
return CommandError (
" You already have a system registered. To delete your system, use `pk;system delete`, or to unlink your system from this account, use `pk;system unlink`. " )
system_name = ctx . remaining ( ) or None
2018-07-24 22:47:57 +02:00
async with ctx . conn . transaction ( ) :
# TODO: figure out what to do if this errors out on collision on generate_hid
hid = utils . generate_hid ( )
system = await db . create_system ( ctx . conn , system_name = system_name , system_hid = hid )
# Link account
await db . link_account ( ctx . conn , system_id = system . id , account_id = ctx . message . author . id )
2018-09-07 17:34:38 +02:00
return CommandSuccess ( " System registered! To begin adding members, use `pk;member new <name>`. " )
2018-07-24 22:47:57 +02:00
2018-09-07 17:34:38 +02:00
async def system_set ( ctx : CommandContext ) :
system = await ctx . ensure_system ( )
prop = ctx . pop_str ( CommandError ( " You must pass a property name to set. " , help = help . edit_system ) )
2018-07-24 22:47:57 +02:00
allowed_properties = [ " name " , " description " , " tag " , " avatar " ]
db_properties = {
" name " : " name " ,
" description " : " description " ,
" tag " : " tag " ,
" avatar " : " avatar_url "
}
if prop not in allowed_properties :
2018-09-07 17:34:38 +02:00
return CommandError (
" Unknown property {} . Allowed properties are {} . " . format ( prop , " , " . join ( allowed_properties ) ) ,
help = help . edit_system )
2018-07-24 22:47:57 +02:00
2018-09-07 17:34:38 +02:00
if ctx . has_next ( ) :
value = ctx . remaining ( )
2018-07-24 22:47:57 +02:00
# Sanity checking
2018-09-07 17:40:02 +02:00
if prop == " description " :
if len ( value ) > 1024 :
return CommandError ( " You can ' t have a description longer than 1024 characters. " )
2018-07-24 22:47:57 +02:00
if prop == " tag " :
if len ( value ) > 32 :
2018-09-07 17:34:38 +02:00
return CommandError ( " You can ' t have a system tag longer than 32 characters. " )
2018-07-24 22:47:57 +02:00
2018-09-07 17:57:12 +02:00
if re . search ( " <a?: \ w+: \ d+> " , value ) :
return CommandError ( " Due to a Discord limitation, custom emojis aren ' t supported. Please use a standard emoji instead. " )
2018-07-24 22:47:57 +02:00
# Make sure there are no members which would make the combined length exceed 32
2018-09-07 17:34:38 +02:00
members_exceeding = await db . get_members_exceeding ( ctx . conn , system_id = system . id ,
length = 32 - len ( value ) - 1 )
2018-07-24 22:47:57 +02:00
if len ( members_exceeding ) > 0 :
# If so, error out and warn
member_names = " , " . join ( [ member . name
2018-09-07 17:34:38 +02:00
for member in members_exceeding ] )
2018-07-24 22:47:57 +02:00
logger . debug ( " Members exceeding combined length with tag ' {} ' : {} " . format ( value , member_names ) )
2018-09-07 17:34:38 +02:00
return CommandError (
" The maximum length of a name plus the system tag is 32 characters. The following members would exceed the limit: {} . Please reduce the length of the tag, or rename the members. " . format (
member_names ) )
2018-07-24 22:47:57 +02:00
if prop == " avatar " :
user = await utils . parse_mention ( ctx . client , value )
if user :
# Set the avatar to the mentioned user's avatar
# Discord doesn't like webp, but also hosts png alternatives
value = user . avatar_url . replace ( " .webp " , " .png " )
else :
# Validate URL
u = urlparse ( value )
if u . scheme in [ " http " , " https " ] and u . netloc and u . path :
value = value
else :
2018-09-07 17:34:38 +02:00
return CommandError ( " Invalid image URL. " )
2018-07-24 22:47:57 +02:00
else :
# Clear from DB
value = None
db_prop = db_properties [ prop ]
2018-09-07 17:34:38 +02:00
await db . update_system_field ( ctx . conn , system_id = system . id , field = db_prop , value = value )
2018-08-02 11:10:09 +02:00
2018-09-07 17:34:38 +02:00
response = CommandSuccess ( " {} system {} . " . format ( " Updated " if value else " Cleared " , prop ) )
2018-09-07 21:33:27 +02:00
#if prop == "avatar" and value:
# response.set_image(url=value)
2018-07-24 22:47:57 +02:00
return response
2018-09-07 17:34:38 +02:00
async def system_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 ) )
2018-07-24 22:47:57 +02:00
# Find account to link
2018-09-07 17:34:38 +02:00
linkee = await utils . parse_mention ( ctx . client , account_name )
2018-07-24 22:47:57 +02:00
if not linkee :
2018-09-07 17:34:38 +02:00
return CommandError ( " Account not found. " )
2018-07-24 22:47:57 +02:00
# Make sure account doesn't already have a system
account_system = await db . get_system_by_account ( ctx . conn , linkee . id )
if account_system :
2018-09-07 17:34:38 +02:00
return CommandError ( " The mentioned account is already linked to a system (` {} `) " . format ( account_system . hid ) )
2018-07-24 22:47:57 +02:00
# Send confirmation message
2018-09-07 17:34:38 +02:00
msg = await ctx . reply (
" {} , please confirm the link by clicking the ✅ reaction on this message. " . format ( linkee . mention ) )
2018-07-24 22:47:57 +02:00
await ctx . client . add_reaction ( msg , " ✅ " )
await ctx . client . add_reaction ( msg , " ❌ " )
2018-09-07 23:21:12 +02:00
reaction = await ctx . client . wait_for_reaction ( emoji = [ " ✅ " , " ❌ " ] , message = msg , user = linkee , timeout = 60.0 * 5 )
2018-07-24 22:47:57 +02:00
# If account to be linked confirms...
if not reaction :
2018-09-07 17:34:38 +02:00
return CommandError ( " Account link timed out. " )
2018-07-24 22:47:57 +02:00
if not reaction . reaction . emoji == " ✅ " :
2018-09-07 17:34:38 +02:00
return CommandError ( " Account link cancelled. " )
2018-07-24 22:47:57 +02:00
2018-09-07 17:34:38 +02:00
await db . link_account ( ctx . conn , system_id = system . id , account_id = linkee . id )
return CommandSuccess ( " Account linked to system. " )
async def system_unlink ( ctx : CommandContext ) :
system = await ctx . ensure_system ( )
2018-07-24 22:47:57 +02:00
# Make sure you can't unlink every account
2018-09-07 17:34:38 +02:00
linked_accounts = await db . get_linked_accounts ( ctx . conn , system_id = system . id )
2018-07-24 22:47:57 +02:00
if len ( linked_accounts ) == 1 :
2018-09-07 17:34:38 +02:00
return CommandError ( " This is the only account on your system, so you can ' t unlink it. " )
2018-07-24 22:47:57 +02:00
2018-09-07 17:34:38 +02:00
await db . unlink_account ( ctx . conn , system_id = system . id , account_id = ctx . message . author . id )
return CommandSuccess ( " Account unlinked. " )
2018-07-24 22:47:57 +02:00
2018-08-02 11:10:09 +02:00
2018-09-07 17:34:38 +02:00
async def system_fronter ( ctx : CommandContext ) :
if ctx . has_next ( ) :
system = await ctx . pop_system ( )
else :
system = await ctx . ensure_system ( )
2018-08-02 11:10:09 +02:00
2018-08-02 00:36:50 +02:00
fronters , timestamp = await pluralkit . utils . get_fronters ( ctx . conn , system_id = system . id )
2018-07-24 22:47:57 +02:00
fronter_names = [ member . name for member in fronters ]
2018-09-07 17:34:38 +02:00
embed = embeds . status ( " " )
2018-07-24 22:47:57 +02:00
if len ( fronter_names ) == 0 :
embed . add_field ( name = " Current fronter " , value = " (no fronter) " )
elif len ( fronter_names ) == 1 :
embed . add_field ( name = " Current fronter " , value = fronter_names [ 0 ] )
else :
embed . add_field ( name = " Current fronters " , value = " , " . join ( fronter_names ) )
if timestamp :
2018-09-07 17:34:38 +02:00
embed . add_field ( name = " Since " , value = " {} ( {} ) " . format ( timestamp . isoformat ( sep = " " , timespec = " seconds " ) ,
humanize . naturaltime ( pluralkit . utils . fix_time ( timestamp ) ) ) )
await ctx . reply ( embed = embed )
2018-08-02 11:10:09 +02:00
2018-09-07 17:34:38 +02:00
async def system_fronthistory ( ctx : CommandContext ) :
if ctx . has_next ( ) :
system = await ctx . pop_system ( )
else :
system = await ctx . ensure_system ( )
2018-08-02 11:10:09 +02:00
2018-07-24 22:47:57 +02:00
lines = [ ]
2018-08-02 00:36:50 +02:00
front_history = await pluralkit . utils . get_front_history ( ctx . conn , system . id , count = 10 )
2018-07-24 22:47:57 +02:00
for i , ( timestamp , members ) in enumerate ( front_history ) :
# Special case when no one's fronting
if len ( members ) == 0 :
name = " (no fronter) "
else :
name = " , " . join ( [ member . name for member in members ] )
# Make proper date string
time_text = timestamp . isoformat ( sep = " " , timespec = " seconds " )
2018-09-07 17:34:38 +02:00
rel_text = humanize . naturaltime ( pluralkit . utils . fix_time ( timestamp ) )
2018-07-24 22:47:57 +02:00
delta_text = " "
if i > 0 :
2018-09-07 17:34:38 +02:00
last_switch_time = front_history [ i - 1 ] [ 0 ]
2018-07-24 22:47:57 +02:00
delta_text = " , for {} " . format ( humanize . naturaldelta ( timestamp - last_switch_time ) )
lines . append ( " ** {} ** ( {} , {} {} ) " . format ( name , time_text , rel_text , delta_text ) )
2018-09-07 17:34:38 +02:00
embed = embeds . status ( " \n " . join ( lines ) or " (none) " )
2018-07-24 22:47:57 +02:00
embed . title = " Past switches "
2018-09-07 17:34:38 +02:00
await ctx . reply ( embed = embed )
2018-07-24 22:47:57 +02:00
2018-09-07 17:34:38 +02:00
async def system_delete ( ctx : CommandContext ) :
system = await ctx . ensure_system ( )
2018-09-08 13:48:18 +02:00
delete_confirm_msg = " Are you sure you want to delete your system? If so, reply to this message with the system ' s ID (` {} `). " . format ( system . hid )
if not await ctx . confirm_text ( ctx . message . author , ctx . message . channel , system . hid , delete_confirm_msg ) :
2018-09-07 17:34:38 +02:00
return CommandError ( " System deletion cancelled. " )
2018-09-08 13:48:18 +02:00
await db . remove_system ( ctx . conn , system_id = system . id )
return CommandSuccess ( " System deleted. " )
2018-08-02 11:10:09 +02:00
2018-09-07 17:34:38 +02:00
async def system_frontpercent ( ctx : CommandContext ) :
system = await ctx . ensure_system ( )
2018-08-02 11:10:09 +02:00
# Parse the time limit (will go this far back)
2018-09-07 17:34:38 +02:00
before = dateparser . parse ( ctx . remaining ( ) , languages = [ " en " ] , settings = {
2018-08-02 11:10:09 +02:00
" TO_TIMEZONE " : " UTC " ,
" RETURN_AS_TIMEZONE_AWARE " : False
} )
# If time is in the future, just kinda discard
if before and before > datetime . utcnow ( ) :
before = None
# Fetch list of switches
2018-09-07 17:34:38 +02:00
all_switches = await pluralkit . utils . get_front_history ( ctx . conn , system . id , 99999 )
2018-08-02 11:10:09 +02:00
if not all_switches :
2018-09-07 17:34:38 +02:00
return CommandError ( " No switches registered to this system. " )
2018-08-02 11:10:09 +02:00
# Cull the switches *ending* before the limit, if given
# We'll need to find the first switch starting before the limit, then cut off every switch *before* that
if before :
for last_stamp , _ in all_switches :
if last_stamp < before :
break
all_switches = [ ( stamp , members ) for stamp , members in all_switches if stamp > = last_stamp ]
start_times = [ stamp for stamp , _ in all_switches ]
end_times = [ datetime . utcnow ( ) ] + start_times
switch_members = [ members for _ , members in all_switches ]
# Gonna save a list of members by ID for future lookup too
members_by_id = { }
# Using the ID as a key here because it's a simple number that can be hashed and used as a key
member_times = { }
for start_time , end_time , members in zip ( start_times , end_times , switch_members ) :
# Cut off parts of the switch that occurs before the time limit (will only happen if this is the last switch)
if before and start_time < before :
start_time = before
# Calculate length of the switch
switch_length = end_time - start_time
2018-09-07 17:34:38 +02:00
def add_switch ( id , length ) :
if id not in member_times :
member_times [ id ] = length
2018-08-02 11:10:09 +02:00
else :
2018-09-07 17:34:38 +02:00
member_times [ id ] + = length
2018-08-02 11:10:09 +02:00
for member in members :
# Add the switch length to the currently registered time for that member
add_switch ( member . id , switch_length )
# Also save the member in the ID map for future reference
members_by_id [ member . id ] = member
# Also register a no-fronter switch with the key None
if not members :
add_switch ( None , switch_length )
# Find the total timespan of the range
span_start = max ( start_times [ - 1 ] , before ) if before else start_times [ - 1 ]
total_time = datetime . utcnow ( ) - span_start
2018-09-01 19:41:35 +02:00
embed = embeds . status ( " " )
2018-08-02 11:10:09 +02:00
for member_id , front_time in sorted ( member_times . items ( ) , key = lambda x : x [ 1 ] , reverse = True ) :
member = members_by_id [ member_id ] if member_id else None
# Calculate percent
fraction = front_time / total_time
percent = int ( fraction * 100 )
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 " ) ) )
2018-09-07 17:34:38 +02:00
await ctx . reply ( embed = embed )