2020-06-26 03:04:11 -04:00
#!/usr/bin/env python3
import asyncio
import discord
import json
import random
2020-07-08 02:51:51 -04:00
import re
2020-06-26 03:04:11 -04:00
import sys
2020-07-08 02:51:51 -04:00
BLOCKS_FILE = ' blocks.json '
def get_blocks ( ) :
try :
with open ( BLOCKS_FILE ) as f :
return { int ( k ) : v for ( k , v ) in json . load ( f ) . items ( ) }
except FileNotFoundError :
return { }
def add_block ( requester , target ) :
blocks = get_blocks ( )
if requester not in blocks :
blocks [ requester ] = [ ]
if target not in blocks [ requester ] :
blocks [ requester ] . append ( target )
with open ( BLOCKS_FILE , ' w ' ) as f :
json . dump ( blocks , f )
def remove_block ( requester , target ) :
blocks = get_blocks ( )
if requester not in blocks :
return
while target in blocks [ requester ] :
blocks [ requester ] . remove ( target )
if len ( blocks [ requester ] ) == 0 :
del blocks [ requester ]
with open ( BLOCKS_FILE , ' w ' ) as f :
json . dump ( blocks , f )
2020-06-26 03:04:11 -04:00
def matchings ( l ) :
2020-07-08 02:51:51 -04:00
count = len ( l )
if count == 0 :
2020-06-26 03:04:11 -04:00
return [ ]
l = l . copy ( )
random . shuffle ( l )
2020-07-08 02:51:51 -04:00
ids = set ( u . id for u in l )
blocks = get_blocks ( )
block_pairs = set ( )
for r in blocks :
if r in ids :
for t in blocks [ r ] :
if t in ids :
block_pairs . add ( ( min ( r , t ) , max ( r , t ) ) )
disjoint_pairs = [ ]
while True :
paired = set ( u for pair in disjoint_pairs for u in pair )
remaining = set ( p for p in block_pairs if p [ 0 ] not in paired and p [ 1 ] not in paired )
if len ( remaining ) == 0 :
break
disjoint_pairs . append ( remaining . pop ( ) )
circle = [ None ] * ( count - 1 )
center = None
for i in range ( count / / 2 ) :
if len ( disjoint_pairs ) != 0 :
p = disjoint_pairs . pop ( )
2020-07-09 19:31:56 -04:00
[ u0 , u1 ] = [ u for u in l if u . id in p ]
2020-07-08 02:51:51 -04:00
l = [ u for u in l if u . id not in p ]
else :
u0 = l . pop ( )
u1 = l . pop ( )
circle [ i ] = u0
if i != count / / 2 - 1 :
circle [ - ( i + 1 ) ] = u1
else :
center = u1
2020-06-26 03:04:11 -04:00
result = [ ]
2020-07-08 02:51:51 -04:00
for i in range ( len ( circle ) ) :
matching = [ ( center , circle [ i ] ) ]
for j in range ( 1 , ( len ( circle ) - 1 ) / / 2 + 1 ) :
matching . append ( ( circle [ ( i - j ) % len ( circle ) ] , circle [ ( i + j ) % len ( circle ) ] ) )
2020-06-26 03:04:11 -04:00
result . append ( matching )
2020-07-08 02:51:51 -04:00
random . shuffle ( result )
for i in range ( len ( result ) ) :
random . shuffle ( result [ i ] )
2020-07-09 19:31:56 -04:00
for j in range ( len ( result [ i ] ) ) :
2020-07-09 02:42:55 -04:00
if random . randrange ( 2 ) == 0 :
result [ i ] [ j ] = ( result [ i ] [ j ] [ 1 ] , result [ i ] [ j ] [ 0 ] )
2020-07-08 02:51:51 -04:00
result = [ matching for matching in result if all ( ( min ( match [ 0 ] . id , match [ 1 ] . id ) , max ( match [ 0 ] . id , match [ 1 ] . id ) ) not in block_pairs for match in matching ) ]
2020-06-26 03:04:11 -04:00
return result
async def delete_if_possible ( channels ) :
for c in channels :
try :
await c . delete ( )
2020-07-29 21:16:36 -04:00
except discord . DiscordException :
2020-06-26 03:04:11 -04:00
pass
2020-06-26 18:51:22 -04:00
async def countdown ( channel ) :
await asyncio . sleep ( 20 )
await channel . send ( ' 10 ' , tts = True )
await asyncio . sleep ( 5 )
await channel . send ( ' 5 ' , tts = True )
await asyncio . sleep ( 5 )
2020-06-26 03:04:11 -04:00
with open ( ' config.json ' ) as f :
config = json . load ( f )
client = discord . Client ( )
2020-07-08 02:51:51 -04:00
async def handle_guild_message ( message ) :
2020-06-26 03:04:11 -04:00
if not message . content . startswith ( ' !waggle ' ) :
return
if not message . author . guild_permissions . administrator :
await message . channel . send ( ' only administrators can start pollination! ' )
return
argv = message . content . split ( )
if len ( argv ) > 1 :
try :
rounds = int ( message . content . split ( ) [ 1 ] )
except ValueError :
2020-06-26 04:13:03 -04:00
await message . channel . send ( " usage: `!waggle 6` for at most 6 rounds of pollination, `!waggle` for as many rounds as possible until everyone ' s met everyone else " )
2020-06-26 03:04:11 -04:00
return
else :
rounds = None
voice_state = message . author . voice
if voice_state is None or voice_state . channel is None :
await message . channel . send ( ' you need to be in the main gathering voice channel to start pollination ' )
return
main_voice_channel = voice_state . channel
category = message . channel . category
if category is None :
await message . channel . send ( " please re-send this message in a channel in the category where you ' d like me to create voice channels " )
return
participants = main_voice_channel . members . copy ( )
if len ( participants ) % 2 != 0 :
participants . remove ( message . author )
await message . channel . send ( ' leaving out {} so we have an even number of participants ' . format ( message . author . mention ) )
mention_all = ' ' . join ( u . mention for u in participants )
schedule = matchings ( participants )
if rounds :
schedule = schedule [ : rounds ]
2020-07-08 02:51:51 -04:00
if len ( schedule ) == 0 :
await message . channel . send ( " there aren ' t enough people for pollination right now :( " )
return
2020-06-26 03:04:11 -04:00
pollination_channels = [ ]
channel_names = config [ ' channel_names ' ] . copy ( )
exception_count = 0
2020-06-26 17:51:49 -04:00
while len ( pollination_channels ) < len ( participants ) / / 2 :
2020-06-26 03:04:11 -04:00
if len ( channel_names ) > 0 :
name = channel_names . pop ( 0 )
else :
name = ' pollination- {} ' . format ( random . randrange ( 10000 ) )
try :
pollination_channels . append ( await category . create_voice_channel ( name ) )
exception_count = 0
except discord . Forbidden :
await message . channel . send ( " looks like I ' m not allowed to create voice channels :( " )
await delete_if_possible ( pollination_channels )
return
2020-07-29 21:16:36 -04:00
except discord . DiscordException as e :
2020-06-26 03:04:11 -04:00
exception_count + = 1
if exception_count > 20 :
await message . channel . send ( " I ' m trying to create voice channels, but something ' s wrong: {} " . format ( e ) )
await delete_if_possible ( pollination_channels )
return
continue
2020-06-26 18:51:22 -04:00
await message . channel . send ( " {} Welcome to pollination! You ' ll be randomly paired up, moved into separate voice channels, spend some time chatting, and then move to a new channel and meet somebody else. Each round will last 5 minutes, and there ' ll be {} rounds total. I ' ll announce when your time is nearly up so you can wrap up your conversations, and I ' ll automatically move you to a different voice channel at the start of each new round. If you keep this text channel open, you ' ll get audible countdowns via text-to-speech at the end of each round. **Feel free to leave at any time if you need to. You won ' t get moved into new voice channels if you disconnect from voice.** If you find yourself alone in a channel, the person you were paired with for that round may have left; just relax and take a break for 5 minutes. Have fun! " . format ( mention_all , len ( schedule ) ) )
2020-06-26 03:37:20 -04:00
2020-06-26 03:04:11 -04:00
for matching in schedule :
2020-06-26 03:37:20 -04:00
announcement = ' Next round starting in 30 seconds: \n '
2020-06-26 03:04:11 -04:00
announcement + = ' \n ' . join ( ' {0} and {1} in {2} ' . format ( a . mention , b . mention , c . name ) for ( ( a , b ) , c ) in zip ( matching , pollination_channels ) )
await message . channel . send ( announcement )
2020-06-26 18:51:22 -04:00
await countdown ( message . channel )
2020-06-26 03:04:11 -04:00
for ( ( a , b ) , c ) in zip ( matching , pollination_channels ) :
try :
await a . move_to ( c )
2020-07-29 21:16:36 -04:00
except discord . DiscordException as e :
2020-06-26 03:37:20 -04:00
print ( ' failed to move participant {} : {} ' . format ( a , e ) , file = sys . stderr )
2020-06-26 03:04:11 -04:00
try :
await b . move_to ( c )
2020-07-29 21:16:36 -04:00
except discord . DiscordException as e :
2020-06-26 03:37:20 -04:00
print ( ' failed to move participant {} : {} ' . format ( b , e ) , file = sys . stderr )
2020-06-26 03:04:11 -04:00
await asyncio . sleep ( 60 * 3 )
2020-06-26 03:37:20 -04:00
await message . channel . send ( ' Round ending in 2 minutes {} ' . format ( mention_all ) )
2020-06-26 03:04:11 -04:00
await asyncio . sleep ( 60 )
2020-06-26 03:37:20 -04:00
await message . channel . send ( ' Round ending in 1 minute {} ' . format ( mention_all ) )
2020-06-26 03:04:11 -04:00
await asyncio . sleep ( 30 )
2020-06-26 03:37:20 -04:00
await message . channel . send ( ' Returning to main channel in 30 seconds {} ' . format ( mention_all ) )
2020-06-26 18:51:22 -04:00
await countdown ( message . channel )
2020-06-26 03:04:11 -04:00
for u in participants :
try :
await u . move_to ( main_voice_channel )
2020-07-29 21:16:36 -04:00
except discord . DiscordException as e :
2020-06-26 03:37:20 -04:00
print ( ' failed to move participant {} : {} ' . format ( u , e ) , file = sys . stderr )
2020-06-26 03:04:11 -04:00
await delete_if_possible ( pollination_channels )
2020-07-08 02:51:51 -04:00
async def handle_dm ( message ) :
requester = message . author . id
match = re . search ( ' @([^#]+)#([0-9a-f]+) ' , message . content )
if match :
handle = match . group ( 0 )
name = match . group ( 1 )
discriminator = match . group ( 2 )
matching_users = [ u for u in client . users if u . name == name and u . discriminator == discriminator ]
if len ( matching_users ) == 0 :
2020-07-09 02:56:07 -04:00
await message . channel . send ( " sorry, I can ' t find user {} , it looks like they ' re not in any of the discords I ' m in " . format ( handle ) )
2020-07-08 02:51:51 -04:00
else :
target = matching_users [ 0 ] . id
if target in get_blocks ( ) . get ( requester , [ ] ) :
await message . channel . send ( " removing {} from your list of blocked users for pollination " . format ( handle ) )
remove_block ( requester , target )
else :
await message . channel . send ( " adding {} to your list of blocked users for pollination " . format ( handle ) )
add_block ( requester , target )
blocks = get_blocks ( ) . get ( requester , [ ] )
if len ( blocks ) == 0 :
await message . channel . send ( " you currently don ' t have anyone blocked for pollination " )
else :
await message . channel . send ( " your current list of blocked users for pollination is: \n {} " . format ( ' \n ' . join ( ' @ {} # {} ' . format ( u . name , u . discriminator ) for u in [ client . get_user ( uid ) for uid in blocks ] if u is not None ) ) )
await message . channel . send ( " to block or unblock a user, DM me their handle, like @creep#0000. to see this message and your list of blocked users, DM me something random. buzz buzz! " )
@client.event
async def on_ready ( ) :
print ( ' logged in as {0.user} ' . format ( client ) , file = sys . stderr )
@client.event
async def on_message ( message ) :
if message . author == client . user :
return
if isinstance ( message . channel , discord . TextChannel ) :
await handle_guild_message ( message )
elif isinstance ( message . channel , discord . DMChannel ) :
await handle_dm ( message )
2020-06-26 03:04:11 -04:00
client . run ( config [ ' token ' ] )