mirror of
https://github.com/osmarks/autobotrobot
synced 2025-02-12 06:40:10 +00:00
telephony semioverhaul
This commit is contained in:
parent
e525f8da10
commit
4d5ed52f7f
@ -97,6 +97,9 @@ UPDATE user_data SET guild_id = '_global' WHERE rowid IN
|
|||||||
(SELECT rowid FROM user_data d WHERE d.key = user_data.key AND d.user_id = user_data.user_id ORDER BY rowid DESC LIMIT 1)
|
(SELECT rowid FROM user_data d WHERE d.key = user_data.key AND d.user_id = user_data.user_id ORDER BY rowid DESC LIMIT 1)
|
||||||
FROM user_data WHERE guild_id IS NULL GROUP BY user_id, key);
|
FROM user_data WHERE guild_id IS NULL GROUP BY user_id, key);
|
||||||
DELETE FROM user_data WHERE guild_id IS NULL;
|
DELETE FROM user_data WHERE guild_id IS NULL;
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
ALTER TABLE links ADD COLUMN cause TEXT;
|
||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import eventbus
|
|
||||||
import discord
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import discord.ext.commands as commands
|
|
||||||
|
|
||||||
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: 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 DiscordLink(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.webhooks = {}
|
|
||||||
self.bot = bot
|
|
||||||
self.unlisten = eventbus.add_listener("discord", self.on_bridge_message)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
async def on_bridge_message(self, channel_id, msg):
|
|
||||||
channel = self.bot.get_channel(channel_id)
|
|
||||||
if channel:
|
|
||||||
webhook = self.webhooks.get(channel_id)
|
|
||||||
if webhook:
|
|
||||||
wh_obj = discord.Webhook.from_url(webhook, adapter=discord.AsyncWebhookAdapter(self.bot.http._HTTPClient__session))
|
|
||||||
await wh_obj.send(
|
|
||||||
content=render_formatting(channel, msg.message)[:2000], username=msg.author.name, avatar_url=msg.author.avatar_url,
|
|
||||||
allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False))
|
|
||||||
else:
|
|
||||||
text = f"<{msg.author.name}> {render_formatting(channel, msg.message)}"
|
|
||||||
await channel.send(text[:2000], allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False))
|
|
||||||
else:
|
|
||||||
logging.warning("Channel %d not found", channel_id)
|
|
||||||
|
|
||||||
@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)
|
|
||||||
if msg.content == "": return
|
|
||||||
if (msg.author == self.bot.user and msg.content[0] == "<") or msg.author.discriminator == "0000": return
|
|
||||||
channel_id = msg.channel.id
|
|
||||||
msg = eventbus.Message(eventbus.AuthorInfo(msg.author.name, msg.author.id, str(msg.author.avatar_url), msg.author.bot), parse_formatting(self.bot, msg.content), ("discord", channel_id), msg.id)
|
|
||||||
await eventbus.push(msg)
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
self.unlisten()
|
|
||||||
|
|
||||||
def setup(bot):
|
|
||||||
cog = DiscordLink(bot)
|
|
||||||
bot.add_cog(cog)
|
|
||||||
asyncio.create_task(cog.initial_load_webhooks())
|
|
@ -79,20 +79,20 @@ def add_listener(s, l):
|
|||||||
listeners[s].add(l)
|
listeners[s].add(l)
|
||||||
return lambda: listeners[s].remove(l)
|
return lambda: listeners[s].remove(l)
|
||||||
|
|
||||||
async def add_bridge_link(db, c1, c2):
|
async def add_bridge_link(db, c1, c2, cause=None, bidirectional=True):
|
||||||
logging.info("Bridging %s and %s", repr(c1), repr(c2))
|
logging.info("Bridging %s and %s (bidirectional: %s)", repr(c1), repr(c2), bidirectional)
|
||||||
links[c1].add(c2)
|
links[c1].add(c2)
|
||||||
links[c2].add(c1)
|
if bidirectional: links[c2].add(c1)
|
||||||
await db.execute("INSERT INTO links VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", (c1[0], c1[1], c2[0], c2[1], util.timestamp()))
|
await db.execute("INSERT INTO links VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", (c1[0], c1[1], c2[0], c2[1], util.timestamp(), cause))
|
||||||
await db.execute("INSERT INTO links VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", (c2[0], c2[1], c1[0], c1[1], util.timestamp()))
|
if bidirectional: await db.execute("INSERT INTO links VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING", (c2[0], c2[1], c1[0], c1[1], util.timestamp(), cause))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def remove_bridge_link(db, c1, c2):
|
async def remove_bridge_link(db, c1, c2, bidirectional=True):
|
||||||
logging.info("Unbridging %s and %s", repr(c1), repr(c2))
|
logging.info("Unbridging %s and %s (bidirectional: %s)", repr(c1), repr(c2), bidirectional)
|
||||||
links[c1].remove(c2)
|
links[c1].remove(c2)
|
||||||
links[c2].remove(c1)
|
if bidirectional: links[c2].remove(c1)
|
||||||
await db.execute("DELETE FROM links WHERE (to_type = ? AND to_id = ?) AND (from_type = ? AND from_id = ?)", (c1[0], c1[1], c2[0], c2[1]))
|
await db.execute("DELETE FROM links WHERE (to_type = ? AND to_id = ?) AND (from_type = ? AND from_id = ?)", (c1[0], c1[1], c2[0], c2[1]))
|
||||||
await db.execute("DELETE FROM links WHERE (to_type = ? AND to_id = ?) AND (from_type = ? AND from_id = ?)", (c2[0], c2[1], c1[0], c1[1]))
|
if bidirectional: await db.execute("DELETE FROM links WHERE (to_type = ? AND to_id = ?) AND (from_type = ? AND from_id = ?)", (c2[0], c2[1], c1[0], c1[1]))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
async def initial_load(db):
|
async def initial_load(db):
|
||||||
|
@ -28,7 +28,7 @@ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(mess
|
|||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.members = True
|
intents.members = True
|
||||||
|
|
||||||
bot = commands.Bot(command_prefix=config["prefix"], description="AutoBotRobot, the most useless bot in the known universe." + util.config.get("description_suffix", ""),
|
bot = commands.Bot(command_prefix=commands.when_mentioned_or(config["prefix"]), description="AutoBotRobot, the most useless bot in the known universe." + util.config.get("description_suffix", ""),
|
||||||
case_insensitive=True, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=True), intents=intents)
|
case_insensitive=True, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=True), intents=intents)
|
||||||
bot._skip_check = lambda x, y: False
|
bot._skip_check = lambda x, y: False
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ async def andrew_bad(ctx):
|
|||||||
async def on_ready():
|
async def on_ready():
|
||||||
logging.info("Connected as " + bot.user.name)
|
logging.info("Connected as " + bot.user.name)
|
||||||
await bot.change_presence(status=discord.Status.online,
|
await bot.change_presence(status=discord.Status.online,
|
||||||
activity=discord.Activity(name=f"{bot.command_prefix}help", type=discord.ActivityType.listening))
|
activity=discord.Activity(name=f"{config['prefix']}help", type=discord.ActivityType.listening))
|
||||||
|
|
||||||
visible_users = prometheus_client.Gauge("abr_visible_users", "Users the bot can see")
|
visible_users = prometheus_client.Gauge("abr_visible_users", "Users the bot can see")
|
||||||
def get_visible_users():
|
def get_visible_users():
|
||||||
|
227
src/telephone.py
227
src/telephone.py
@ -2,8 +2,12 @@ from discord.ext import commands
|
|||||||
import discord
|
import discord
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
import pydot
|
||||||
|
import tempfile
|
||||||
|
|
||||||
import util
|
import util
|
||||||
import eventbus
|
import eventbus
|
||||||
@ -18,88 +22,172 @@ def generate_address(ctx):
|
|||||||
out += words[int.from_bytes(h[i * 2:i * 2 + 3], "little") % 8192].strip().title()
|
out += words[int.from_bytes(h[i * 2:i * 2 + 3], "little") % 8192].strip().title()
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def setup(bot):
|
def parse_formatting(bot, text):
|
||||||
@bot.group(name="apiotelephone", aliases=["tel", "tele", "telephone", "apiotel"], brief="ApioTelephone lets you 'call' other servers.", help=f"""
|
def parse_match(m):
|
||||||
Call other (participating) servers with ApioTelephone! To configure a channel for telephony, do `{bot.command_prefix}tel setup` (requires Manage Channels).
|
try:
|
||||||
It's recommended that you give the bot Manage Webhooks permissions in this channel so that it can use webhook calls mode.
|
target = int(m.group(2))
|
||||||
To place a call, do `{bot.command_prefix}tel dial [number]` - the other end has to accept the call.
|
except ValueError: return m.string
|
||||||
When you want to end a call, do {bot.command_prefix}tel disconnect.
|
if m.group(1) == "@": # user ping
|
||||||
""")
|
user = bot.get_user(target)
|
||||||
async def telephone(ctx): pass
|
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))
|
||||||
|
|
||||||
async def get_channel_config(channel):
|
def render_formatting(dest_channel, message):
|
||||||
return await bot.database.execute_fetchone("SELECT * FROM telephone_config WHERE channel_id = ?", (channel,))
|
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
|
||||||
|
|
||||||
async def get_addr_config(addr):
|
class Telephone(commands.Cog):
|
||||||
return await bot.database.execute_fetchone("SELECT * FROM telephone_config WHERE id = ?", (addr,))
|
# Discord event bus link
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.webhooks = {}
|
||||||
|
self.bot = bot
|
||||||
|
self.unlisten = eventbus.add_listener("discord", self.on_bridge_message)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
async def on_bridge_message(self, channel_id, msg):
|
||||||
|
channel = self.bot.get_channel(channel_id)
|
||||||
|
if channel:
|
||||||
|
webhook = self.webhooks.get(channel_id)
|
||||||
|
if webhook:
|
||||||
|
wh_obj = discord.Webhook.from_url(webhook, adapter=discord.AsyncWebhookAdapter(self.bot.http._HTTPClient__session))
|
||||||
|
await wh_obj.send(
|
||||||
|
content=render_formatting(channel, msg.message)[:2000], username=msg.author.name, avatar_url=msg.author.avatar_url,
|
||||||
|
allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False))
|
||||||
|
else:
|
||||||
|
text = f"<{msg.author.name}> {render_formatting(channel, msg.message)}"
|
||||||
|
await channel.send(text[:2000], allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False))
|
||||||
|
else:
|
||||||
|
logging.warning("Channel %d not found", channel_id)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
if msg.content == "": return
|
||||||
|
if (msg.author == self.bot.user and msg.content[0] == "<") or msg.author.discriminator == "0000": return
|
||||||
|
channel_id = msg.channel.id
|
||||||
|
msg = eventbus.Message(eventbus.AuthorInfo(msg.author.name, msg.author.id, str(msg.author.avatar_url), msg.author.bot), parse_formatting(self.bot, msg.content), ("discord", channel_id), msg.id)
|
||||||
|
await eventbus.push(msg)
|
||||||
|
|
||||||
|
def cog_unload(self):
|
||||||
|
self.unlisten()
|
||||||
|
|
||||||
|
# ++tel commands
|
||||||
|
|
||||||
|
@commands.group(name="apiotelephone", aliases=["tel", "tele", "telephone", "apiotel"], brief="ApioTelephone lets you 'call' other servers.")
|
||||||
|
async def telephone(self, ctx):
|
||||||
|
f"""Call other (participating) servers with ApioTelephone! To configure a channel for telephony, use the setup command (requires Manage Channels).
|
||||||
|
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,))
|
||||||
|
|
||||||
@telephone.command(brief="Link to other channels", help="""Connect to another channel on Discord or any supported bridges.
|
@telephone.command(brief="Link to other channels", help="""Connect to another channel on Discord or any supported bridges.
|
||||||
Virtual channels also exist.
|
Virtual channels also exist.
|
||||||
""")
|
""")
|
||||||
@commands.check(util.admin_check)
|
@commands.check(util.admin_check)
|
||||||
async def link(ctx, target_type, target_id):
|
async def link(self, ctx, target_type, target_id, bidirectional: bool = True):
|
||||||
target_id = util.extract_codeblock(target_id)
|
target_id = util.extract_codeblock(target_id)
|
||||||
try:
|
try:
|
||||||
target_id = int(target_id)
|
target_id = int(target_id)
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
await eventbus.add_bridge_link(bot.database, ("discord", ctx.channel.id), (target_type, target_id))
|
await eventbus.add_bridge_link(self.bot.database, ("discord", ctx.channel.id), (target_type, target_id), "manual", bidirectional)
|
||||||
await ctx.send(f"Link established.")
|
await ctx.send(f"Link established.")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@telephone.command(brief="Undo link commands.")
|
@telephone.command(brief="Undo link commands.")
|
||||||
@commands.check(util.admin_check)
|
@commands.check(util.admin_check)
|
||||||
async def unlink(ctx, target_type, target_id):
|
async def unlink(self, ctx, target_type, target_id, bidirectional: bool = True):
|
||||||
target_id = util.extract_codeblock(target_id)
|
target_id = util.extract_codeblock(target_id)
|
||||||
try:
|
try:
|
||||||
target_id = int(target_id)
|
target_id = int(target_id)
|
||||||
except ValueError: pass
|
except ValueError: pass
|
||||||
await eventbus.remove_bridge_link(bot.database, ("discord", ctx.channel.id), (target_type, target_id))
|
await eventbus.remove_bridge_link(self.bot.database, ("discord", ctx.channel.id), (target_type, target_id), bidirectional)
|
||||||
await ctx.send(f"Successfully deleted.")
|
await ctx.send(f"Successfully deleted.")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@telephone.command(brief="Generate a webhook")
|
@telephone.command(brief="Generate a webhook")
|
||||||
@commands.check(util.admin_check)
|
@commands.check(util.admin_check)
|
||||||
async def init_webhook(ctx):
|
async def init_webhook(self, ctx):
|
||||||
webhook = (await ctx.channel.create_webhook(name="ABR webhook", reason=f"requested by {ctx.author.name}")).url
|
webhook = (await ctx.channel.create_webhook(name="ABR webhook", reason=f"requested by {ctx.author.name}")).url
|
||||||
await bot.database.execute("INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?)", (ctx.channel.id, webhook))
|
await self.bot.database.execute("INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?)", (ctx.channel.id, webhook))
|
||||||
await bot.database.commit()
|
await self.bot.database.commit()
|
||||||
|
self.webhooks[ctx.channel.id] = webhook
|
||||||
await ctx.send("Done.")
|
await ctx.send("Done.")
|
||||||
|
|
||||||
@telephone.command()
|
@telephone.command()
|
||||||
@commands.check(util.server_mod_check)
|
@commands.check(util.server_mod_check)
|
||||||
async def setup(ctx):
|
async def setup(self, ctx):
|
||||||
num = generate_address(ctx)
|
num = generate_address(ctx)
|
||||||
await ctx.send(f"Your address is {num}.")
|
await ctx.send(f"Your address is {num}.")
|
||||||
info = await get_addr_config(num)
|
info = await self.get_addr_config(num)
|
||||||
webhook = None
|
webhook = None
|
||||||
if info: webhook = info["webhook"]
|
if info: webhook = info["webhook"]
|
||||||
if not info or not webhook:
|
if not info or not webhook:
|
||||||
try:
|
try:
|
||||||
webhook = (await ctx.channel.create_webhook(name="incoming message display", reason="configure for apiotelephone")).url
|
webhook = (await ctx.channel.create_webhook(name="incoming message display", reason="configure for apiotelephone")).url
|
||||||
await bot.database.execute("INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?)", (ctx.channel.id, webhook))
|
await self.bot.database.execute("INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?)", (ctx.channel.id, webhook))
|
||||||
await ctx.send("Created webhook.")
|
await ctx.send("Created webhook.")
|
||||||
except discord.Forbidden as f:
|
except discord.Forbidden as f:
|
||||||
logging.warn("Could not create webhook in #%s %s", ctx.channel.name, ctx.guild.name, exc_info=f)
|
logging.warn("Could not create webhook in #%s %s", ctx.channel.name, ctx.guild.name, exc_info=f)
|
||||||
await ctx.send("Webhook creation failed - please ensure permissions are available. This is not necessary but is recommended.")
|
await ctx.send("Webhook creation failed - please ensure permissions are available. This is not necessary but is recommended.")
|
||||||
await bot.database.execute("INSERT OR REPLACE INTO telephone_config VALUES (?, ?, ?, ?)", (num, ctx.guild.id, ctx.channel.id, webhook))
|
await self.bot.database.execute("INSERT OR REPLACE INTO telephone_config VALUES (?, ?, ?, ?)", (num, ctx.guild.id, ctx.channel.id, webhook))
|
||||||
await bot.database.commit()
|
await self.bot.database.commit()
|
||||||
await ctx.send("Configured.")
|
await ctx.send("Configured.")
|
||||||
|
|
||||||
@telephone.command(aliases=["call"], brief="Dial another telephone channel.")
|
@telephone.command(aliases=["call"], brief="Dial another telephone channel.")
|
||||||
async def dial(ctx, address):
|
async def dial(self, ctx, address):
|
||||||
# basic checks - ensure this is a phone channel and has no other open calls
|
# basic checks - ensure this is a phone channel and has no other open calls
|
||||||
channel_info = await get_channel_config(ctx.channel.id)
|
channel_info = await self.get_channel_config(ctx.channel.id)
|
||||||
if not channel_info: return await ctx.send(embed=util.error_embed("Not in a phone channel."))
|
if not channel_info: return await ctx.send(embed=util.error_embed("Not in a phone channel."))
|
||||||
originating_address = channel_info["id"]
|
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."))
|
if address == originating_address: return await ctx.send(embed=util.error_embed("A channel cannot dial itself. That means *you*, Gibson."))
|
||||||
recv_info = await get_addr_config(address)
|
recv_info = await self.get_addr_config(address)
|
||||||
if not recv_info: return await ctx.send(embed=util.error_embed("Destination address not found. Please check for typos and/or antimemes."))
|
if not recv_info: return await ctx.send(embed=util.error_embed("Destination address not found. Please check for typos and/or antimemes."))
|
||||||
|
|
||||||
current_call = await bot.database.execute_fetchone("SELECT * FROM calls WHERE from_id = ?", (originating_address,))
|
current_call = await self.bot.database.execute_fetchone("SELECT * FROM calls WHERE from_id = ?", (originating_address,))
|
||||||
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."))
|
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
|
# post embed in the receiving channel prompting people to accept/decline call
|
||||||
recv_channel = bot.get_channel(recv_info["channel_id"])
|
recv_channel = self.bot.get_channel(recv_info["channel_id"])
|
||||||
_, call_message = await asyncio.gather(
|
_, call_message = await asyncio.gather(
|
||||||
ctx.send(embed=util.info_embed("Outgoing call", f"Dialing {address}...")),
|
ctx.send(embed=util.info_embed("Outgoing call", f"Dialing {address}...")),
|
||||||
recv_channel.send(embed=util.info_embed("Incoming call",
|
recv_channel.send(embed=util.info_embed("Incoming call",
|
||||||
@ -111,24 +199,24 @@ def setup(bot):
|
|||||||
call_message.add_reaction("❎")
|
call_message.add_reaction("❎")
|
||||||
)
|
)
|
||||||
|
|
||||||
def check(re, u): return (str(re.emoji) == "✅" or str(re.emoji) == "❎") and u != bot.user
|
def check(re, u): return (str(re.emoji) == "✅" or str(re.emoji) == "❎") and u != self.bot.user
|
||||||
|
|
||||||
reaction = None
|
reaction = None
|
||||||
# wait until someone clicks the reactions, or time out and say so
|
# wait until someone clicks the reactions, or time out and say so
|
||||||
try:
|
try:
|
||||||
reaction, user = await bot.wait_for("reaction_add", timeout=util.config["call_timeout"], check=check)
|
reaction, user = await self.bot.wait_for("reaction_add", timeout=util.config["call_timeout"], check=check)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
ctx.send(embed=util.error_embed("Timed out", "Outgoing call timed out - the other end did not pick up.")),
|
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"))
|
recv_channel.send(embed=util.error_embed("Timed out", "Call timed out - no response in time"))
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.gather(call_message.remove_reaction("✅", bot.user), call_message.remove_reaction("❎", bot.user))
|
await asyncio.gather(call_message.remove_reaction("✅", self.bot.user), call_message.remove_reaction("❎", self.bot.user))
|
||||||
em = str(reaction.emoji) if reaction else "❎"
|
em = str(reaction.emoji) if reaction else "❎"
|
||||||
if em == "✅": # accept call
|
if em == "✅": # accept call
|
||||||
await bot.database.execute("INSERT INTO calls VALUES (?, ?, ?)", (originating_address, address, util.timestamp()))
|
await self.bot.database.execute("INSERT INTO calls VALUES (?, ?, ?)", (originating_address, address, util.timestamp()))
|
||||||
await bot.database.commit()
|
await self.bot.database.commit()
|
||||||
await eventbus.add_bridge_link(bot.database, ("discord", ctx.channel.id), ("discord", recv_channel.id))
|
await eventbus.add_bridge_link(self.bot.database, ("discord", ctx.channel.id), ("discord", recv_channel.id), "telephone")
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
ctx.send(embed=util.info_embed("Outgoing call", "Call accepted and connected.")),
|
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."))
|
recv_channel.send(embed=util.info_embed("Incoming call", "Call accepted and connected."))
|
||||||
@ -137,33 +225,33 @@ def setup(bot):
|
|||||||
await ctx.send(embed=util.error_embed("Your call was declined.", "Call declined"))
|
await ctx.send(embed=util.error_embed("Your call was declined.", "Call declined"))
|
||||||
|
|
||||||
@telephone.command(aliases=["disconnect", "quit"], brief="Disconnect latest call.")
|
@telephone.command(aliases=["disconnect", "quit"], brief="Disconnect latest call.")
|
||||||
async def hangup(ctx):
|
async def hangup(self, ctx):
|
||||||
channel_info = await get_channel_config(ctx.channel.id)
|
channel_info = await self.get_channel_config(ctx.channel.id)
|
||||||
addr = channel_info["id"]
|
addr = channel_info["id"]
|
||||||
if not channel_info: return await ctx.send(embed=util.error_embed("Not in a phone channel."))
|
if not channel_info: return await ctx.send(embed=util.error_embed("Not in a phone channel."))
|
||||||
from_here = await bot.database.execute_fetchone("SELECT * FROM calls WHERE from_id = ?", (addr,))
|
from_here = await self.bot.database.execute_fetchone("SELECT * FROM calls WHERE from_id = ?", (addr,))
|
||||||
to_here = await bot.database.execute_fetchone("SELECT * FROM calls WHERE to_id = ?", (addr,))
|
to_here = await self.bot.database.execute_fetchone("SELECT * FROM calls WHERE to_id = ?", (addr,))
|
||||||
if (not to_here) and (not from_here): return await ctx.send(embed=util.error_embed("No calls are active."))
|
if (not to_here) and (not from_here): return await ctx.send(embed=util.error_embed("No calls are active."))
|
||||||
|
|
||||||
other = None
|
other = None
|
||||||
if from_here:
|
if from_here:
|
||||||
other = from_here["to_id"]
|
other = from_here["to_id"]
|
||||||
await bot.database.execute("DELETE FROM calls WHERE from_id = ? AND to_id = ?", (addr, other))
|
await self.bot.database.execute("DELETE FROM calls WHERE from_id = ? AND to_id = ?", (addr, other))
|
||||||
elif to_here:
|
elif to_here:
|
||||||
other = to_here["from_id"]
|
other = to_here["from_id"]
|
||||||
await bot.database.execute("DELETE FROM calls WHERE to_id = ? AND from_id = ?", (addr, other))
|
await self.bot.database.execute("DELETE FROM calls WHERE to_id = ? AND from_id = ?", (addr, other))
|
||||||
await bot.database.commit()
|
await self.bot.database.commit()
|
||||||
other_channel = (await get_addr_config(other))["channel_id"]
|
other_channel = (await self.get_addr_config(other))["channel_id"]
|
||||||
await eventbus.remove_bridge_link(bot.database, ("discord", other_channel), ("discord", ctx.channel.id))
|
await eventbus.remove_bridge_link(self.bot.database, ("discord", other_channel), ("discord", ctx.channel.id))
|
||||||
|
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
ctx.send(embed=util.info_embed("Hung up", f"Call to {other} disconnected.")),
|
ctx.send(embed=util.info_embed("Hung up", f"Call to {other} disconnected.")),
|
||||||
bot.get_channel(other_channel).send(embed=util.info_embed("Hung up", f"Call to {addr} disconnected."))
|
self.bot.get_channel(other_channel).send(embed=util.info_embed("Hung up", f"Call to {addr} disconnected."))
|
||||||
)
|
)
|
||||||
|
|
||||||
@telephone.command(aliases=["status"], brief="List inbound/outbound calls.")
|
@telephone.command(aliases=["status"], brief="List inbound/outbound calls.")
|
||||||
async def info(ctx):
|
async def info(self, ctx):
|
||||||
channel_info = await get_channel_config(ctx.channel.id)
|
channel_info = await self.get_channel_config(ctx.channel.id)
|
||||||
if not channel_info: return await ctx.send(embed=util.info_embed("Phone status", "Not a phone channel"))
|
if not channel_info: return await ctx.send(embed=util.info_embed("Phone status", "Not a phone channel"))
|
||||||
addr = channel_info['id']
|
addr = channel_info['id']
|
||||||
title = f"{addr} status"
|
title = f"{addr} status"
|
||||||
@ -174,8 +262,49 @@ def setup(bot):
|
|||||||
def delta(ts):
|
def delta(ts):
|
||||||
return util.format_timedelta(datetime.utcfromtimestamp(ts), now)
|
return util.format_timedelta(datetime.utcfromtimestamp(ts), now)
|
||||||
|
|
||||||
incoming = await bot.database.execute_fetchall("SELECT * FROM calls WHERE to_id = ?", (addr,))
|
incoming = await self.bot.database.execute_fetchall("SELECT * FROM calls WHERE to_id = ?", (addr,))
|
||||||
fields.extend(map(lambda x: ["Incoming call", f"From {x['from_id']} - for {delta(x['start_time'])}"], incoming))
|
fields.extend(map(lambda x: ["Incoming call", f"From {x['from_id']} - for {delta(x['start_time'])}"], incoming))
|
||||||
outgoing = await bot.database.execute_fetchall("SELECT * FROM calls WHERE from_id = ?", (addr,))
|
outgoing = await self.bot.database.execute_fetchall("SELECT * FROM calls WHERE from_id = ?", (addr,))
|
||||||
fields.extend(map(lambda x: ["Outgoing call", f"To {x['to_id']} - for {delta(x['start_time'])}"], outgoing))
|
fields.extend(map(lambda x: ["Outgoing call", f"To {x['to_id']} - for {delta(x['start_time'])}"], outgoing))
|
||||||
await ctx.send(embed=util.info_embed(title, f"Connected: {len(incoming) + len(outgoing)}", fields))
|
await ctx.send(embed=util.info_embed(title, f"Connected: {len(incoming) + len(outgoing)}", fields))
|
||||||
|
|
||||||
|
@telephone.command(brief="Dump links out of current channel.")
|
||||||
|
async def graph(self, ctx):
|
||||||
|
graph = pydot.Dot("linkgraph")
|
||||||
|
seen = set()
|
||||||
|
seen_edges = set()
|
||||||
|
def node_name(x):
|
||||||
|
if x[0] == "discord":
|
||||||
|
chan = self.bot.get_channel(x[1])
|
||||||
|
if chan:
|
||||||
|
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")
|
||||||
|
graph.write_png(tmppath)
|
||||||
|
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)
|
||||||
|
asyncio.create_task(cog.initial_load_webhooks())
|
@ -265,7 +265,6 @@ extensions = (
|
|||||||
"commands",
|
"commands",
|
||||||
"userdata",
|
"userdata",
|
||||||
"irc_link",
|
"irc_link",
|
||||||
"discord_link",
|
|
||||||
"duckduckgo"
|
"duckduckgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user