2020-10-11 13:40:39 +00:00
from discord . ext import commands
import discord
import logging
import asyncio
2021-04-05 18:08:37 +00:00
import re
2020-10-11 13:40:39 +00:00
import hashlib
2022-01-18 23:56:11 +00:00
from datetime import datetime , timedelta
2021-04-05 18:08:37 +00:00
import os
import pydot
import tempfile
2022-01-18 23:56:11 +00:00
import collections
import aiohttp
2020-10-11 13:40:39 +00:00
import util
2021-02-25 17:48:06 +00:00
import eventbus
2020-10-11 13:40:39 +00:00
# Generate a "phone" address
# Not actually for phones
def generate_address ( ctx ) :
h = hashlib . blake2b ( str ( ctx . guild . id ) . encode ( " utf-8 " ) ) . digest ( )
words = open ( " wordlist-8192.txt " ) . readlines ( )
out = " "
for i in range ( 3 ) :
out + = words [ int . from_bytes ( h [ i * 2 : i * 2 + 3 ] , " little " ) % 8192 ] . strip ( ) . title ( )
return out
2021-04-05 18:08:37 +00:00
def parse_formatting ( bot , text ) :
def parse_match ( m ) :
try :
target = int ( m . group ( 2 ) )
except ValueError : return m . string
if m . group ( 1 ) == " @ " : # user ping
user = bot . get_user ( target )
if user : return { " type " : " user_mention " , " name " : user . name , " id " : target }
return f " @ { target } "
else : # channel "ping"
channel = bot . get_channel ( target )
if channel : return { " type " : " channel_mention " , " name " : channel . name , " id " : target }
return f " # { target } "
remaining = text
out = [ ]
while match := re . search ( r " <([@#])!?([0-9]+)> " , remaining ) :
start , end = match . span ( )
out . append ( remaining [ : start ] )
out . append ( parse_match ( match ) )
remaining = remaining [ end : ]
out . append ( remaining )
return list ( filter ( lambda x : x != " " , out ) )
def render_formatting ( dest_channel , message ) :
out = " "
for seg in message :
if isinstance ( seg , str ) :
out + = seg
else :
kind = seg [ " type " ]
# TODO: use python 3.10 pattern matching
if kind == " user_mention " :
member = dest_channel . guild . get_member ( seg [ " id " ] )
if member != None : out + = f " <@ { member . id } > "
else : out + = f " @ { seg [ ' name ' ] } "
elif kind == " channel_mention " : # these appear to be clickable across servers/guilds
out + = f " <# { seg [ ' id ' ] } > "
else : logging . warn ( " Unrecognized message seg %s " , kind )
return out
class Telephone ( commands . Cog ) :
# Discord event bus link
def __init__ ( self , bot ) :
self . webhooks = { }
self . bot = bot
self . unlisten = eventbus . add_listener ( " discord " , self . on_bridge_message )
2021-07-17 21:09:17 +00:00
self . webhook_queue = asyncio . Queue ( 50 )
self . webhook_queue_handler_task = asyncio . create_task ( self . send_webhooks ( ) )
async def send_webhooks ( self ) :
while True :
webhook , content , username , avatar_url = await self . webhook_queue . get ( )
2022-01-01 21:28:40 +00:00
wh_obj = discord . Webhook . from_url ( webhook , session = self . bot . http . _HTTPClient__session )
2021-07-28 15:52:46 +00:00
try :
await wh_obj . send ( content = content , username = username , avatar_url = avatar_url , allowed_mentions = discord . AllowedMentions ( everyone = False , roles = False , users = False ) )
except :
logging . exception ( " Webhook send on %s failed " )
2021-04-05 18:08:37 +00:00
async def initial_load_webhooks ( self ) :
rows = await self . bot . database . execute_fetchall ( " SELECT * FROM discord_webhooks " )
for row in rows :
self . webhooks [ row [ " channel_id " ] ] = row [ " webhook " ]
logging . info ( " Loaded %d webhooks " , len ( rows ) )
2021-07-28 19:30:37 +00:00
async def on_bridge_message ( self , channel_id , msg : eventbus . Message ) :
2021-04-05 18:08:37 +00:00
channel = self . bot . get_channel ( channel_id )
if channel :
webhook = self . webhooks . get ( channel_id )
2021-07-28 19:30:37 +00:00
attachments_text = " \n " . join ( f " { at . filename } : { at . proxy_url } " for at in msg . attachments )
async def send_raw ( text ) :
if webhook :
try :
self . webhook_queue . put_nowait ( ( webhook , text , msg . author . name , msg . author . avatar_url ) )
except asyncio . QueueFull :
text = f " < { msg . author . name } > { text } "
await channel . send ( text [ : 2000 ] , allowed_mentions = discord . AllowedMentions ( everyone = False , roles = False , users = False ) )
else :
text = f " < { msg . author . name } > { text } "
2021-07-17 21:09:17 +00:00
await channel . send ( text [ : 2000 ] , allowed_mentions = discord . AllowedMentions ( everyone = False , roles = False , users = False ) )
2021-07-28 19:30:37 +00:00
await send_raw ( render_formatting ( channel , msg . message ) [ : 2000 ] )
if attachments_text : await send_raw ( attachments_text )
2021-04-05 18:08:37 +00:00
else :
logging . warning ( " Channel %d not found " , channel_id )
2020-10-11 13:40:39 +00:00
2021-04-05 18:08:37 +00:00
@commands.Cog.listener ( " on_message " )
async def send_to_bridge ( self , msg ) :
# discard webhooks and bridge messages (hackily, admittedly, not sure how else to do this)
2021-07-28 19:30:37 +00:00
if msg . content == " " and len ( msg . attachments ) == 0 : return
2022-01-01 21:28:40 +00:00
if ( msg . author == self . bot . user and ( len ( msg . content ) > 0 and msg . content [ 0 ] == " < " ) ) or msg . author . discriminator == " 0000 " : return
2021-04-05 18:08:37 +00:00
channel_id = msg . channel . id
2021-10-28 11:55:40 +00:00
reply = None
2021-11-09 08:11:15 +00:00
if msg . reference :
if isinstance ( msg . reference . resolved , discord . DeletedReferencedMessage ) :
replying_to = None
elif msg . reference . resolved :
replying_to = msg . reference . resolved
elif msg . reference . cached_message :
replying_to = msg . reference . cached_message
else :
try :
replying_to = await self . bot . get_guild ( msg . reference . guild_id ) . get_channel ( msg . reference . channel_id ) . fetch_message ( msg . reference . message_id )
2022-11-28 16:02:40 +00:00
except ( discord . HTTPException , AttributeError ) :
2021-11-09 08:11:15 +00:00
replying_to = None
if replying_to :
2022-01-01 21:28:40 +00:00
reply = ( eventbus . AuthorInfo ( replying_to . author . name , replying_to . author . id , str ( replying_to . author . display_avatar . url ) , replying_to . author . bot ) , parse_formatting ( self . bot , replying_to . content ) )
2021-11-09 08:11:15 +00:00
else :
reply = ( None , None )
2022-01-01 21:28:40 +00:00
msg = eventbus . Message ( eventbus . AuthorInfo ( msg . author . name , msg . author . id , str ( msg . author . display_avatar . url ) , msg . author . bot ) ,
2021-10-28 11:55:40 +00:00
parse_formatting ( self . bot , msg . content ) , ( " discord " , channel_id ) , msg . id , [ at for at in msg . attachments if not at . is_spoiler ( ) ] , reply = reply )
2021-04-05 18:08:37 +00:00
await eventbus . push ( msg )
2020-10-11 13:40:39 +00:00
2021-04-05 18:08:37 +00:00
def cog_unload ( self ) :
self . unlisten ( )
2021-07-17 21:09:17 +00:00
self . webhook_queue_handler_task . cancel ( )
2021-04-05 18:08:37 +00:00
# ++tel commands
@commands.group ( name = " apiotelephone " , aliases = [ " tel " , " tele " , " telephone " , " apiotel " ] , brief = " ApioTelephone lets you ' call ' other servers. " )
async def telephone ( self , ctx ) :
2021-04-13 15:34:36 +00:00
""" Call other (participating) servers with ApioTelephone! To configure a channel for telephony, use the setup command (requires Manage Channels).
2021-04-05 18:08:37 +00:00
It ' s recommended that you give the bot Manage Webhooks permissions in this channel so that it can use webhook calls mode.
To place a call , use dial [ number ] - the other end has to accept the call .
When you want to end a call , use hangup .
"""
pass
async def get_channel_config ( self , channel ) :
return await self . bot . database . execute_fetchone ( " SELECT * FROM telephone_config WHERE channel_id = ? " , ( channel , ) )
async def get_addr_config ( self , addr ) :
return await self . bot . database . execute_fetchone ( " SELECT * FROM telephone_config WHERE id = ? COLLATE NOCASE " , ( addr , ) )
2020-10-11 13:40:39 +00:00
2021-02-25 17:48:06 +00:00
@telephone.command ( brief = " Link to other channels " , help = """ Connect to another channel on Discord or any supported bridges.
Virtual channels also exist .
""" )
2021-07-28 19:30:37 +00:00
@commands.check ( util . extpriv_check )
2021-04-05 18:08:37 +00:00
async def link ( self , ctx , target_type , target_id , bidirectional : bool = True ) :
2021-03-25 17:56:29 +00:00
target_id = util . extract_codeblock ( target_id )
2021-02-25 17:48:06 +00:00
try :
target_id = int ( target_id )
except ValueError : pass
2021-04-05 18:08:37 +00:00
await eventbus . add_bridge_link ( self . bot . database , ( " discord " , ctx . channel . id ) , ( target_type , target_id ) , " manual " , bidirectional )
2021-02-25 17:48:06 +00:00
await ctx . send ( f " Link established. " )
pass
@telephone.command ( brief = " Undo link commands. " )
2022-01-18 21:34:34 +00:00
@commands.check ( util . server_mod_check )
2021-04-05 18:08:37 +00:00
async def unlink ( self , ctx , target_type , target_id , bidirectional : bool = True ) :
2021-03-25 17:56:29 +00:00
target_id = util . extract_codeblock ( target_id )
2021-02-25 17:48:06 +00:00
try :
target_id = int ( target_id )
except ValueError : pass
2021-04-05 18:08:37 +00:00
await eventbus . remove_bridge_link ( self . bot . database , ( " discord " , ctx . channel . id ) , ( target_type , target_id ) , bidirectional )
2021-02-25 17:48:06 +00:00
await ctx . send ( f " Successfully deleted. " )
pass
2022-01-18 23:56:11 +00:00
2022-06-16 17:56:26 +00:00
async def find_recent ( self , chs , query ) :
2022-01-18 23:56:11 +00:00
one_week = timedelta ( seconds = 60 * 60 * 24 * 7 )
2022-06-16 17:56:26 +00:00
one_week_ago = datetime . now ( ) - one_week
2022-01-18 23:56:11 +00:00
2022-06-16 17:56:26 +00:00
for ch in chs :
yield True , ch
2022-01-18 23:56:11 +00:00
async for msg in ch . history ( limit = None , after = one_week_ago ) :
if query in msg . content . lower ( ) :
2022-06-16 17:56:26 +00:00
yield False , ch , msg
2022-01-18 23:56:11 +00:00
2022-06-16 17:56:26 +00:00
@telephone.command ( brief = " Find recent messages in channels linked to this " )
2022-01-18 23:56:11 +00:00
@commands.check ( util . extpriv_check )
async def searchrecent ( self , ctx , ch : discord . TextChannel , * , query ) :
author = ctx . author
chs = [ ]
2022-06-16 17:56:26 +00:00
for dest in eventbus . find_all_destinations ( ( " discord " , ch . id ) ) :
if dest [ 0 ] == " discord " :
2022-01-18 23:56:11 +00:00
chs . append ( self . bot . get_channel ( dest [ 1 ] ) )
2022-06-16 17:56:26 +00:00
found = await self . find_recent ( chs , query )
2022-01-18 23:56:11 +00:00
out = " "
2022-06-16 17:56:26 +00:00
async for ch , ms in found . items ( ) :
2022-01-18 23:56:11 +00:00
out + = f " { ch . mention } (`# { ch . name } ` in ` { ch . guild . name } `) \n "
for m in ms :
u = m . author . name if m . author else None
w = " [WH] " if m . webhook_id else " "
out + = f " - { m . content [ : 20 ] } @ { m . created_at } by { u } { w } \n "
for c in util . chunks ( out , 2000 ) :
await author . send ( c )
return found
2022-06-16 17:56:26 +00:00
@telephone.command ( brief = " Delete recent messages in channels linked to this " )
2022-01-18 23:56:11 +00:00
@commands.check ( util . extpriv_check )
async def delrecent ( self , ctx , ch : discord . TextChannel , * , query ) :
author = ctx . author
found = await self . searchrecent ( ctx , ch , query = query )
await author . send ( " please say ' GO ' to confirm or wait 10 seconds to not confirm " )
try :
await self . bot . wait_for ( ' message ' , check = lambda m : m . author == ctx . author and m . content == " GO " and m . channel == ctx . author . dm_channel , timeout = 10 )
except asyncio . TimeoutError :
await author . send ( " timed out " )
return
async def try_delete ( msg , session ) :
if msg . webhook_id is not None :
# note: assumes there is only one webhook we control per channel
# i think that's the case
wh_url = await self . bot . database . execute_fetchone ( " SELECT webhook FROM discord_webhooks WHERE channel_id = ? " , ( msg . channel . id , ) )
if wh_url is None :
await author . send ( f " no access to webhook: { msg . id } { msg . channel . mention } { msg . jump_url } " )
return
wh_url = wh_url [ ' webhook ' ]
wh = discord . Webhook . from_url ( wh_url , session = session )
await wh . delete_message ( msg . id )
else :
try :
await msg . delete ( )
except discord . errors . Forbidden :
await author . send ( f " !!! couldn ' t delete msg { msg . id } in { msg . channel . mention } " )
msgs = [ ]
for q in found . values ( ) :
msgs . extend ( q )
async with aiohttp . ClientSession ( ) as session :
await asyncio . gather ( * ( try_delete ( msg , session ) for msg in msgs ) )
await author . send ( " done " )
2021-02-25 17:48:06 +00:00
@telephone.command ( brief = " Generate a webhook " )
2021-07-28 21:08:57 +00:00
@commands.check ( util . server_mod_check )
2021-04-05 18:08:37 +00:00
async def init_webhook ( self , ctx ) :
2021-02-25 17:48:06 +00:00
webhook = ( await ctx . channel . create_webhook ( name = " ABR webhook " , reason = f " requested by { ctx . author . name } " ) ) . url
2021-04-05 18:08:37 +00:00
await self . bot . database . execute ( " INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?) " , ( ctx . channel . id , webhook ) )
await self . bot . database . commit ( )
self . webhooks [ ctx . channel . id ] = webhook
2021-02-25 17:48:06 +00:00
await ctx . send ( " Done. " )
2020-10-11 13:40:39 +00:00
@telephone.command ( )
2021-01-14 09:38:32 +00:00
@commands.check ( util . server_mod_check )
2021-04-05 18:08:37 +00:00
async def setup ( self , ctx ) :
2020-10-11 13:40:39 +00:00
num = generate_address ( ctx )
await ctx . send ( f " Your address is { num } . " )
2021-04-05 18:08:37 +00:00
info = await self . get_addr_config ( num )
2020-10-11 13:40:39 +00:00
webhook = None
if info : webhook = info [ " webhook " ]
if not info or not webhook :
try :
webhook = ( await ctx . channel . create_webhook ( name = " incoming message display " , reason = " configure for apiotelephone " ) ) . url
2021-04-05 18:08:37 +00:00
await self . bot . database . execute ( " INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?) " , ( ctx . channel . id , webhook ) )
2020-10-11 13:40:39 +00:00
await ctx . send ( " Created webhook. " )
except discord . Forbidden as f :
2021-03-25 17:56:29 +00:00
logging . warn ( " Could not create webhook in # %s %s " , ctx . channel . name , ctx . guild . name , exc_info = f )
2020-10-11 13:40:39 +00:00
await ctx . send ( " Webhook creation failed - please ensure permissions are available. This is not necessary but is recommended. " )
2023-07-19 13:51:52 +00:00
await self . bot . database . execute ( " INSERT OR REPLACE INTO telephone_config VALUES (?, ?, ?, ?, 0) " , ( num , ctx . guild . id , ctx . channel . id , webhook ) )
2021-04-05 18:08:37 +00:00
await self . bot . database . commit ( )
2020-10-11 13:40:39 +00:00
await ctx . send ( " Configured. " )
2021-04-14 21:44:16 +00:00
@telephone.command ( aliases = [ " rcall " ] , brief = " Dial another telephone channel. " )
async def rdial ( self , ctx ) :
# TODO: this is not very performant
2023-07-19 13:51:52 +00:00
random = ( await self . bot . database . execute_fetchone ( " SELECT id FROM telephone_config WHERE disabled = 0 ORDER BY RANDOM() " ) ) [ " id " ]
2021-04-14 21:44:16 +00:00
await self . dial ( ctx , random )
2021-02-25 17:48:06 +00:00
@telephone.command ( aliases = [ " call " ] , brief = " Dial another telephone channel. " )
2021-04-05 18:08:37 +00:00
async def dial ( self , ctx , address ) :
2020-10-11 13:40:39 +00:00
# basic checks - ensure this is a phone channel and has no other open calls
2021-04-05 18:08:37 +00:00
channel_info = await self . get_channel_config ( ctx . channel . id )
2020-10-11 13:40:39 +00:00
if not channel_info : return await ctx . send ( embed = util . error_embed ( " Not in a phone channel. " ) )
originating_address = channel_info [ " id " ]
if address == originating_address : return await ctx . send ( embed = util . error_embed ( " A channel cannot dial itself. That means *you*, Gibson. " ) )
2021-04-05 18:08:37 +00:00
recv_info = await self . get_addr_config ( address )
2020-10-11 13:40:39 +00:00
if not recv_info : return await ctx . send ( embed = util . error_embed ( " Destination address not found. Please check for typos and/or antimemes. " ) )
2022-01-18 23:56:11 +00:00
2021-04-05 18:08:37 +00:00
current_call = await self . bot . database . execute_fetchone ( " SELECT * FROM calls WHERE from_id = ? " , ( originating_address , ) )
2020-10-11 13:40:39 +00:00
if current_call : return await ctx . send ( embed = util . error_embed ( f " A call is already open (to { current_call [ ' to_id ' ] } ) from this channel. Currently, only one outgoing call is permitted at a time. " ) )
# post embed in the receiving channel prompting people to accept/decline call
2021-04-05 18:08:37 +00:00
recv_channel = self . bot . get_channel ( recv_info [ " channel_id " ] )
2023-07-19 13:51:52 +00:00
if recv_channel is None :
await self . bot . database . execute ( " UPDATE telephone_config SET disabled = 1 WHERE id = ? " , ( address , ) )
return await ctx . send ( embed = util . error_embed ( " Target channel no longer exists. " ) )
2020-10-11 13:40:39 +00:00
_ , call_message = await asyncio . gather (
ctx . send ( embed = util . info_embed ( " Outgoing call " , f " Dialing { address } ... " ) ) ,
recv_channel . send ( embed = util . info_embed ( " Incoming call " ,
f " Call from { originating_address } . Click :white_check_mark: to accept or :negative_squared_cross_mark: to decline. " ) )
)
# add clickable reactions to it
await asyncio . gather (
call_message . add_reaction ( " ✅ " ) ,
call_message . add_reaction ( " ❎ " )
)
2021-04-05 18:08:37 +00:00
def check ( re , u ) : return ( str ( re . emoji ) == " ✅ " or str ( re . emoji ) == " ❎ " ) and u != self . bot . user
2020-10-11 13:40:39 +00:00
2020-10-27 16:35:56 +00:00
reaction = None
2020-10-11 13:40:39 +00:00
# wait until someone clicks the reactions, or time out and say so
try :
2021-04-05 18:08:37 +00:00
reaction , user = await self . bot . wait_for ( " reaction_add " , timeout = util . config [ " call_timeout " ] , check = check )
2020-10-11 13:40:39 +00:00
except asyncio . TimeoutError :
await asyncio . gather (
ctx . send ( embed = util . error_embed ( " Timed out " , " Outgoing call timed out - the other end did not pick up. " ) ) ,
recv_channel . send ( embed = util . error_embed ( " Timed out " , " Call timed out - no response in time " ) )
)
2021-04-05 18:08:37 +00:00
await asyncio . gather ( call_message . remove_reaction ( " ✅ " , self . bot . user ) , call_message . remove_reaction ( " ❎ " , self . bot . user ) )
2020-10-27 16:35:56 +00:00
em = str ( reaction . emoji ) if reaction else " ❎ "
2020-10-11 13:40:39 +00:00
if em == " ✅ " : # accept call
2021-04-05 18:08:37 +00:00
await self . bot . database . execute ( " INSERT INTO calls VALUES (?, ?, ?) " , ( originating_address , address , util . timestamp ( ) ) )
await self . bot . database . commit ( )
await eventbus . add_bridge_link ( self . bot . database , ( " discord " , ctx . channel . id ) , ( " discord " , recv_channel . id ) , " telephone " )
2020-10-11 13:40:39 +00:00
await asyncio . gather (
ctx . send ( embed = util . info_embed ( " Outgoing call " , " Call accepted and connected. " ) ) ,
recv_channel . send ( embed = util . info_embed ( " Incoming call " , " Call accepted and connected. " ) )
)
elif em == " ❎ " : # drop call
await ctx . send ( embed = util . error_embed ( " Your call was declined. " , " Call declined " ) )
2021-02-25 17:48:06 +00:00
@telephone.command ( aliases = [ " disconnect " , " quit " ] , brief = " Disconnect latest call. " )
2021-04-05 18:08:37 +00:00
async def hangup ( self , ctx ) :
channel_info = await self . get_channel_config ( ctx . channel . id )
2020-10-11 13:40:39 +00:00
addr = channel_info [ " id " ]
if not channel_info : return await ctx . send ( embed = util . error_embed ( " Not in a phone channel. " ) )
2021-04-05 18:08:37 +00:00
from_here = await self . bot . database . execute_fetchone ( " SELECT * FROM calls WHERE from_id = ? " , ( addr , ) )
to_here = await self . bot . database . execute_fetchone ( " SELECT * FROM calls WHERE to_id = ? " , ( addr , ) )
2020-10-11 13:40:39 +00:00
if ( not to_here ) and ( not from_here ) : return await ctx . send ( embed = util . error_embed ( " No calls are active. " ) )
other = None
if from_here :
other = from_here [ " to_id " ]
2021-04-05 18:08:37 +00:00
await self . bot . database . execute ( " DELETE FROM calls WHERE from_id = ? AND to_id = ? " , ( addr , other ) )
2020-10-11 13:40:39 +00:00
elif to_here :
other = to_here [ " from_id " ]
2021-04-05 18:08:37 +00:00
await self . bot . database . execute ( " DELETE FROM calls WHERE to_id = ? AND from_id = ? " , ( addr , other ) )
await self . bot . database . commit ( )
other_channel = ( await self . get_addr_config ( other ) ) [ " channel_id " ]
await eventbus . remove_bridge_link ( self . bot . database , ( " discord " , other_channel ) , ( " discord " , ctx . channel . id ) )
2020-10-11 13:40:39 +00:00
await asyncio . gather (
ctx . send ( embed = util . info_embed ( " Hung up " , f " Call to { other } disconnected. " ) ) ,
2021-04-05 18:08:37 +00:00
self . bot . get_channel ( other_channel ) . send ( embed = util . info_embed ( " Hung up " , f " Call to { addr } disconnected. " ) )
2020-10-11 13:40:39 +00:00
)
2021-02-25 17:48:06 +00:00
@telephone.command ( aliases = [ " status " ] , brief = " List inbound/outbound calls. " )
2021-04-05 18:08:37 +00:00
async def info ( self , ctx ) :
channel_info = await self . get_channel_config ( ctx . channel . id )
2020-10-11 13:40:39 +00:00
if not channel_info : return await ctx . send ( embed = util . info_embed ( " Phone status " , " Not a phone channel " ) )
addr = channel_info [ ' id ' ]
title = f " { addr } status "
fields = [ ]
now = datetime . utcnow ( )
def delta ( ts ) :
return util . format_timedelta ( datetime . utcfromtimestamp ( ts ) , now )
2021-04-05 18:08:37 +00:00
incoming = await self . bot . database . execute_fetchall ( " SELECT * FROM calls WHERE to_id = ? " , ( addr , ) )
2020-10-11 13:40:39 +00:00
fields . extend ( map ( lambda x : [ " Incoming call " , f " From { x [ ' from_id ' ] } - for { delta ( x [ ' start_time ' ] ) } " ] , incoming ) )
2021-04-05 18:08:37 +00:00
outgoing = await self . bot . database . execute_fetchall ( " SELECT * FROM calls WHERE from_id = ? " , ( addr , ) )
2020-10-11 15:26:29 +00:00
fields . extend ( map ( lambda x : [ " Outgoing call " , f " To { x [ ' to_id ' ] } - for { delta ( x [ ' start_time ' ] ) } " ] , outgoing ) )
2020-10-27 16:35:56 +00:00
await ctx . send ( embed = util . info_embed ( title , f " Connected: { len ( incoming ) + len ( outgoing ) } " , fields ) )
2021-04-05 18:08:37 +00:00
@telephone.command ( brief = " Dump links out of current channel. " )
async def graph ( self , ctx ) :
2021-04-13 15:34:36 +00:00
graph = pydot . Dot ( " linkgraph " , ratio = " fill " )
2021-04-05 18:08:37 +00:00
seen = set ( )
seen_edges = set ( )
def node_name ( x ) :
if x [ 0 ] == " discord " :
chan = self . bot . get_channel ( x [ 1 ] )
2021-04-13 15:34:36 +00:00
if chan and getattr ( chan , " name " , False ) :
2021-04-05 18:08:37 +00:00
out = " # " + chan . name
if chan . guild :
out = chan . guild . name + " / " + out
return " discord/ " + out
else :
return f " { x [ 0 ] } / { x [ 1 ] } "
return f " { x [ 0 ] } / { x [ 1 ] } "
todo = [ ( " discord " , ctx . channel . id ) ]
while todo :
current = todo . pop ( 0 )
graph . add_node ( pydot . Node ( node_name ( current ) , fontname = " monospace " ) )
for adjacent in eventbus . links [ current ] :
if adjacent not in seen :
todo . append ( adjacent )
edge = ( current , adjacent )
if edge not in seen_edges :
graph . add_edge ( pydot . Edge ( node_name ( current ) , node_name ( adjacent ) ) )
seen_edges . add ( edge )
seen . add ( current )
( handle , tmppath ) = tempfile . mkstemp ( " .png " , " graphviz " )
2023-07-19 13:51:52 +00:00
graph . write_png ( tmppath , prog = " neato " )
2021-04-05 18:08:37 +00:00
try :
await ctx . send ( file = discord . File ( handle , filename = " out.png " ) )
finally :
os . unlink ( tmppath )
def setup ( bot ) :
cog = Telephone ( bot )
bot . add_cog ( cog )
2022-01-18 23:56:11 +00:00
asyncio . create_task ( cog . initial_load_webhooks ( ) )