modularize it, add mostly working phone function

This commit is contained in:
osmarks 2020-10-11 14:40:39 +01:00
parent 50e4c22565
commit f902ca0d64
7 changed files with 8635 additions and 181 deletions

View File

@ -21,16 +21,44 @@ CREATE TABLE reminders (
expired INTEGER NOT NULL,
extra TEXT NOT NULL
);
""",
"""
CREATE TABLE telephone_config (
id TEXT PRIMARY KEY,
guild_id INTEGER NOT NULL,
channel_id INTEGER NOT NULL UNIQUE,
webhook TEXT
);
""",
"""
CREATE TABLE calls (
from_id TEXT NOT NULL REFERENCES telephone_config(id) UNIQUE,
to_id TEXT NOT NULL REFERENCES telephone_config(id),
start_time INTEGER NOT NULL
);
"""
]
async def execute_fetchone(self, sql, params=None):
if params == None: params = ()
return await self._execute(self._fetchone, sql, params)
def _fetchone(self, sql, params):
cursor = self._conn.execute(sql, params)
return cursor.fetchone()
async def init(db_path):
db = await aiosqlite.connect(db_path)
await db.execute("PRAGMA foreign_keys = ON")
db.row_factory = aiosqlite.Row
aiosqlite.Connection._fetchone = _fetchone
aiosqlite.Connection.execute_fetchone = execute_fetchone
version = (await (await db.execute("PRAGMA user_version")).fetchone())[0]
for i in range(version, len(migrations)):
await db.executescript(migrations[i])
# Normally this would be a terrible idea because of SQL injection.
# Normally interpolating like this would be a terrible idea because of SQL injection.
# However, in this case there is not an obvious alternative (the parameter-based way apparently doesn't work)
# and i + 1 will always be an integer anyway
await db.execute(f"PRAGMA user_version = {i + 1}")

49
src/debug.py Normal file
View File

@ -0,0 +1,49 @@
import util
import asyncio
import traceback
from discord.ext import commands
def setup(bot):
async def admin_check(ctx):
return await bot.is_owner(ctx.author)
@bot.group()
@commands.check(admin_check)
async def magic(ctx):
if ctx.invoked_subcommand == None:
return await ctx.send("Invalid magic command.")
@magic.command(rest_is_raw=True)
async def py(ctx, *, code):
code = util.extract_codeblock(code)
try:
loc = {
**locals(),
"bot": bot,
"ctx": ctx,
"db": bot.database
}
result = await asyncio.wait_for(util.async_exec(code, loc, globals()), timeout=5.0)
if result != None:
if isinstance(result, str):
await ctx.send(result[:1999])
else:
await ctx.send(util.gen_codeblock(repr(result)))
except TimeoutError:
await ctx.send(embed=util.error_embed("Timed out."))
except BaseException as e:
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))
@magic.command(rest_is_raw=True)
async def sql(ctx, *, code):
code = util.extract_codeblock(code)
try:
csr = bot.database.execute(code)
out = ""
async with csr as cursor:
async for row in cursor:
out += " ".join(map(repr, row)) + "\n"
await ctx.send(util.gen_codeblock(out))
await bot.database.commit()
except Exception as e:
await ctx.send(embed=util.error_embed(util.gen_codeblock(traceback.format_exc())))

View File

@ -11,37 +11,18 @@ import argparse
import traceback
import random
import rolldice
from datetime import timezone, datetime
import tio
import db
import util
def timestamp(): return int(datetime.now(tz=timezone.utc).timestamp())
# TODO refactor this
database = None
config = toml.load(open("config.toml", "r"))
config = util.config
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(message)s", datefmt="%H:%M:%S %d/%m/%Y")
bot = commands.Bot(command_prefix=config["prefix"], description="AutoBotRobot, the most useless bot in the known universe.", case_insensitive=True)
bot._skip_check = lambda x, y: False
def make_embed(*, fields=[], footer_text=None, **kwargs):
embed = discord.Embed(**kwargs)
for field in fields:
if len(field) > 2:
embed.add_field(name=field[0], value=field[1], inline=field[2])
else:
embed.add_field(name=field[0], value=field[1], inline=False)
if footer_text:
embed.set_footer(text=footer_text)
return embed
def error_embed(msg, title="Error"):
return make_embed(color=config["colors"]["error"], description=msg, title=title)
cleaner = discord.ext.commands.clean_content()
def clean(ctx, text):
@ -53,19 +34,20 @@ async def on_message(message):
if len(words) == 10 and message.author.id == 435756251205468160:
await message.channel.send(util.unlyric(message.content))
else:
if message.author.id == bot.user.id: return
if message.author == bot.user or message.author.discriminator == "0000": return
ctx = await bot.get_context(message)
if not ctx.valid: return
await bot.invoke(ctx)
@bot.event
async def on_command_error(ctx, err):
print(ctx, err)
if isinstance(err, commands.CommandNotFound, commands.CheckFailure): return
#print(ctx, err)
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return
try:
trace = re.sub("\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__)))
print(trace)
await ctx.send(embed=error_embed(gen_codeblock(trace), title="Internal error"))
#print(trace)
logging.error("command error occured (in %s)", ctx.invoked_with, exc_info=err)
await ctx.send(embed=util.error_embed(util.gen_codeblock(trace), title="Internal error"))
except Exception as e: print("meta-error:", e)
@bot.command(help="Gives you a random fortune as generated by `fortune`.")
@ -84,13 +66,13 @@ async def ping(ctx):
async def delete(ctx, *, raw_target):
target = await clean(ctx, raw_target.strip().replace("\n", " "))
if len(target) > 256:
await ctx.send(embed=error_embed("Deletion target must be max 256 chars"))
await ctx.send(embed=util.error_embed("Deletion target must be max 256 chars"))
return
async with ctx.typing():
await ctx.send(f"Deleting {target}...")
await asyncio.sleep(1)
await database.execute("INSERT INTO deleted_items (timestamp, item) VALUES (?, ?)", (timestamp(), target))
await database.commit()
await bot.database.execute("INSERT INTO deleted_items (timestamp, item) VALUES (?, ?)", (util.timestamp(), target))
await bot.database.commit()
await ctx.send(f"Deleted {target} successfully.")
@bot.command(help="View recently deleted things, optionally matching a filter.")
@ -99,9 +81,9 @@ async def list_deleted(ctx, search=None):
if search: acc = f"Recently deleted (matching {search}):\n"
csr = None
if search:
csr = database.execute("SELECT * FROM deleted_items WHERE item LIKE ? ORDER BY timestamp DESC LIMIT 100", (f"%{search}%",))
csr = bot.database.execute("SELECT * FROM deleted_items WHERE item LIKE ? ORDER BY timestamp DESC LIMIT 100", (f"%{search}%",))
else:
csr = database.execute("SELECT * FROM deleted_items ORDER BY timestamp DESC LIMIT 100")
csr = bot.database.execute("SELECT * FROM deleted_items ORDER BY timestamp DESC LIMIT 100")
async with csr as cursor:
async for row in cursor:
to_add = "- " + row[2].replace("```", "[REDACTED]") + "\n"
@ -124,20 +106,17 @@ exec_flag_parser = NonExitingArgumentParser(add_help=False)
exec_flag_parser.add_argument("--verbose", "-v", action="store_true")
exec_flag_parser.add_argument("--language", "-L")
def gen_codeblock(content):
return "```\n" + content.replace("```", "\\`\\`\\`")[:1900] + "\n```"
@bot.command(rest_is_raw=True, help="Execute provided code (in a codeblock) using TIO.run.")
async def exec(ctx, *, arg):
match = re.match(EXEC_REGEX, arg, flags=re.DOTALL)
if match == None:
await ctx.send(embed=error_embed("Invalid format. Expected a codeblock."))
await ctx.send(embed=util.error_embed("Invalid format. Expected a codeblock."))
return
flags_raw = match.group(1)
flags = exec_flag_parser.parse_args(flags_raw.split())
lang = flags.language or match.group(2)
if not lang:
await ctx.send(embed=error_embed("No language specified. Use the -L flag or add a language to your codeblock."))
await ctx.send(embed=util.error_embed("No language specified. Use the -L flag or add a language to your codeblock."))
return
lang = lang.strip()
code = match.group(3)
@ -145,11 +124,11 @@ async def exec(ctx, *, arg):
async with ctx.typing():
ok, real_lang, result, debug = await tio.run(lang, code)
if not ok:
await ctx.send(embed=error_embed(gen_codeblock(result), "Execution failed"))
await ctx.send(embed=util.error_embed(util.gen_codeblock(result), "Execution failed"))
else:
out = result
if flags.verbose:
debug_block = "\n" + gen_codeblock(f"""{debug}\nLanguage: {real_lang}""")
debug_block = "\n" + util.gen_codeblock(f"""{debug}\nLanguage: {real_lang}""")
out = out[:2000 - len(debug_block)] + debug_block
else:
out = out[:2000]
@ -167,93 +146,6 @@ async def supported_langs(ctx, search=None):
if acc == "": acc = "No results."
await ctx.send(acc)
@bot.command(brief="Set a reminder to be reminded about later.", rest_is_raw=True, help="""Sets a reminder which you will (probably) be reminded about at/after the specified time.
All times are UTC.
Reminders are checked every minute, so while precise times are not guaranteed, reminders should under normal conditions be received within 2 minutes of what you specify.""")
async def remind(ctx, time, *, reminder):
reminder = reminder.strip()
if len(reminder) > 512:
await ctx.send(embed=error_embed("Maximum reminder length is 512 characters", "Foolish user error"))
return
extra_data = {
"author_id": ctx.author.id,
"channel_id": ctx.message.channel.id,
"message_id": ctx.message.id,
"guild_id": ctx.message.guild and ctx.message.guild.id,
"original_time_spec": time
}
try:
time = util.parse_time(time)
except:
await ctx.send(embed=error_embed("Invalid time"))
return
await database.execute("INSERT INTO reminders (remind_timestamp, created_timestamp, reminder, expired, extra) VALUES (?, ?, ?, ?, ?)",
(time.timestamp(), timestamp(), reminder, 0, json.dumps(extra_data, separators=(',', ':'))))
await database.commit()
await ctx.send(f"Reminder scheduled for {util.format_time(time)}.")
async def send_to_channel(info, text):
channel = bot.get_channel(info["channel_id"])
if not channel: raise Exception(f"channel {info['channel_id']} unavailable/nonexistent")
await channel.send(text)
async def send_by_dm(info, text):
user = bot.get_user(info["author_id"])
if not user: raise Exception(f"user {info['author_id']} unavailable/nonexistent")
if not user.dm_channel: await user.create_dm()
await user.dm_channel.send(text)
async def send_to_guild(info, text):
guild = bot.get_guild(info["guild_id"])
member = guild.get_member(info["author_id"])
self = guild.get_member(bot.user.id)
# if member is here, find a channel they can read and the bot can send in
if member:
for chan in guild.text_channels:
if chan.permissions_for(member).read_messages and chan.permissions_for(self).send_messages:
await chan.send(text)
return
# if member not here or no channel they can read messages in, send to any available channel
for chan in guild.text_channels:
if chan.permissions_for(self).send_messages:
await chan.send(text)
return
raise Exception(f"guild {info['author_id']} has no (valid) channels")
remind_send_methods = [
("original channel", send_to_channel),
("direct message", send_by_dm),
("originating guild", send_to_guild)
]
@tasks.loop(seconds=60)
async def remind_worker():
csr = database.execute("SELECT * FROM reminders WHERE expired = 0 AND remind_timestamp < ?", (timestamp(),))
to_expire = []
async with csr as cursor:
async for row in cursor:
rid, remind_timestamp, created_timestamp, reminder_text, _, extra = row
try:
remind_timestamp = datetime.utcfromtimestamp(remind_timestamp)
created_timestamp = datetime.utcfromtimestamp(created_timestamp)
extra = json.loads(extra)
uid = extra["author_id"]
text = f"<@{uid}> Reminder queued at {util.format_time(created_timestamp)}: {reminder_text}"
for method_name, func in remind_send_methods:
print("trying", method_name, rid)
try:
await func(extra, text)
to_expire.append(rid)
break
except Exception as e: logging.warning("failed to send %d to %s", rid, method_name, exc_info=e)
except Exception as e:
logging.warning("Could not send reminder %d", rid, exc_info=e)
for expiry_id in to_expire:
logging.info("Expiring reminder %d", expiry_id)
await database.execute("UPDATE reminders SET expired = 1 WHERE id = ?", (expiry_id,))
await database.commit()
@bot.command(help="Get some information about the bot.")
async def about(ctx):
await ctx.send("""**AutoBotRobot: The least useful Discord bot ever designed.**
@ -304,68 +196,23 @@ async def random_choice(ctx, *choices):
for choice in choices:
counts[choice] = counts.get(choice, 0) + 1
await ctx.send("\n".join(map(lambda x: f"{x[0]} x{x[1]}", counts.items())))
async def admin_check(ctx):
if not await bot.is_owner(ctx.author):
# apparently this has to be a pure function because ++help calls it for some reason because of course
#await ctx.send(embed=error_embed(f"{ctx.author.name} is not in the sudoers file. This incident has been reported."))
return False
return True
@bot.check
async def andrew_bad(ctx):
return ctx.message.author.id != 543131534685765673
@bot.group()
@commands.check(admin_check)
async def magic(ctx):
if ctx.invoked_subcommand == None:
return await ctx.send("Invalid magic command.")
@magic.command(rest_is_raw=True)
async def py(ctx, *, code):
code = util.extract_codeblock(code)
try:
loc = {
**locals(),
"bot": bot,
"ctx": ctx,
"db": database
}
result = await asyncio.wait_for(util.async_exec(code, loc, globals()), timeout=5.0)
if result != None:
if isinstance(result, str):
await ctx.send(result[:1999])
else:
await ctx.send(gen_codeblock(repr(result)))
except TimeoutError:
await ctx.send(embed=error_embed("Timed out."))
except BaseException as e:
await ctx.send(embed=error_embed(gen_codeblock(traceback.format_exc())))
@magic.command(rest_is_raw=True)
async def sql(ctx, *, code):
code = util.extract_codeblock(code)
try:
csr = database.execute(code)
out = ""
async with csr as cursor:
async for row in cursor:
out += " ".join(map(repr, row)) + "\n"
await ctx.send(gen_codeblock(out))
await database.commit()
except Exception as e:
await ctx.send(embed=error_embed(gen_codeblock(traceback.format_exc())))
@bot.event
async def on_ready():
logging.info("Connected as " + bot.user.name)
await bot.change_presence(status=discord.Status.online, activity=discord.Activity(type=discord.ActivityType.listening, name=f"commands beginning with {config['prefix']}"))
remind_worker.start()
await bot.change_presence(status=discord.Status.online, activity=discord.Activity(type=discord.ActivityType.listening, name=f"commands beginning with {bot.command_prefix}"))
async def run_bot():
global database
database = await db.init(config["database"])
bot.database = await db.init(config["database"])
for ext in (
"reminders",
"debug",
"telephone"
):
bot.load_extension(ext)
await bot.start(config["token"])
if __name__ == '__main__':
@ -373,7 +220,6 @@ if __name__ == '__main__':
try:
loop.run_until_complete(run_bot())
except KeyboardInterrupt:
remind_worker.cancel()
loop.run_until_complete(bot.logout())
finally:
loop.close()

100
src/reminders.py Normal file
View File

@ -0,0 +1,100 @@
import json
import logging
from datetime import datetime
import discord.ext.tasks as tasks
import util
def setup(bot):
@bot.command(brief="Set a reminder to be reminded about later.", rest_is_raw=True, help="""Sets a reminder which you will (probably) be reminded about at/after the specified time.
All times are UTC.
Reminders are checked every minute, so while precise times are not guaranteed, reminders should under normal conditions be received within 2 minutes of what you specify.""")
async def remind(ctx, time, *, reminder):
reminder = reminder.strip()
if len(reminder) > 512:
await ctx.send(embed=util.error_embed("Maximum reminder length is 512 characters", "Foolish user error"))
return
extra_data = {
"author_id": ctx.author.id,
"channel_id": ctx.message.channel.id,
"message_id": ctx.message.id,
"guild_id": ctx.message.guild and ctx.message.guild.id,
"original_time_spec": time
}
try:
time = util.parse_time(time)
except:
await ctx.send(embed=util.error_embed("Invalid time"))
return
await bot.database.execute("INSERT INTO reminders (remind_timestamp, created_timestamp, reminder, expired, extra) VALUES (?, ?, ?, ?, ?)",
(time.timestamp(), util.timestamp(), reminder, 0, util.json_encode(extra_data)))
await bot.database.commit()
await ctx.send(f"Reminder scheduled for {util.format_time(time)}.")
async def send_to_channel(info, text):
channel = bot.get_channel(info["channel_id"])
if not channel: raise Exception(f"channel {info['channel_id']} unavailable/nonexistent")
await channel.send(text)
async def send_by_dm(info, text):
user = bot.get_user(info["author_id"])
if not user: raise Exception(f"user {info['author_id']} unavailable/nonexistent")
if not user.dm_channel: await user.create_dm()
await user.dm_channel.send(text)
async def send_to_guild(info, text):
guild = bot.get_guild(info["guild_id"])
member = guild.get_member(info["author_id"])
self = guild.get_member(bot.user.id)
# if member is here, find a channel they can read and the bot can send in
if member:
for chan in guild.text_channels:
if chan.permissions_for(member).read_messages and chan.permissions_for(self).send_messages:
await chan.send(text)
return
# if member not here or no channel they can read messages in, send to any available channel
for chan in guild.text_channels:
if chan.permissions_for(self).send_messages:
await chan.send(text)
return
raise Exception(f"guild {info['author_id']} has no (valid) channels")
remind_send_methods = [
("original channel", send_to_channel),
("direct message", send_by_dm),
("originating guild", send_to_guild)
]
@tasks.loop(seconds=60)
async def remind_worker():
csr = bot.database.execute("SELECT * FROM reminders WHERE expired = 0 AND remind_timestamp < ?", (util.timestamp(),))
to_expire = []
async with csr as cursor:
async for row in cursor:
rid, remind_timestamp, created_timestamp, reminder_text, _, extra = row
try:
remind_timestamp = datetime.utcfromtimestamp(remind_timestamp)
created_timestamp = datetime.utcfromtimestamp(created_timestamp)
extra = json.loads(extra)
uid = extra["author_id"]
text = f"<@{uid}> Reminder queued at {util.format_time(created_timestamp)}: {reminder_text}"
for method_name, func in remind_send_methods:
print("trying", method_name, rid)
try:
await func(extra, text)
to_expire.append(rid)
break
except Exception as e: logging.warning("failed to send %d to %s", rid, method_name, exc_info=e)
except Exception as e:
logging.warning("Could not send reminder %d", rid, exc_info=e)
for expiry_id in to_expire:
logging.info("Expiring reminder %d", expiry_id)
await bot.database.execute("UPDATE reminders SET expired = 1 WHERE id = ?", (expiry_id,))
await bot.database.commit()
remind_worker.start()
bot.remind_worker = remind_worker
def teardown(bot):
bot.remind_worker.cancel()

197
src/telephone.py Normal file
View File

@ -0,0 +1,197 @@
from discord.ext import commands
import discord
import logging
import asyncio
import hashlib
from datetime import datetime
import util
# 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
channel_calls_cache = {}
def setup(bot):
async def server_mod_check(ctx):
return ctx.author.permissions_in(ctx.channel).manage_channels or bot.is_owner(ctx.author)
@bot.group(name="apiotelephone", aliases=["tel", "tele", "telephone", "apiotel"], brief="ApioTelephone lets you 'call' other servers.", help=f"""
Call other (participating) servers with ApioTelephone! To start, do {bot.command_prefix} tel setup (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.
""")
async def telephone(ctx): pass
async def get_channel_config(channel):
return await bot.database.execute_fetchone("SELECT * FROM telephone_config WHERE channel_id = ?", (channel,))
async def get_addr_config(addr):
return await bot.database.execute_fetchone("SELECT * FROM telephone_config WHERE id = ?", (addr,))
def cache_call(chan, other, other_wh):
x = channel_calls_cache.get(chan)
if not x:
x = []
channel_calls_cache[chan] = x
# append other end and associated webhook
x.append((other, other_wh))
def uncache_call(chan, other):
# remove other end
l = channel_calls_cache[chan]
for i, (c, _) in enumerate(l):
if c == other: l.pop(i)
async def populate_cache(db):
for row in await db.execute_fetchall("""SELECT tcf.channel_id AS from_channel, tct.channel_id AS to_channel, tcf.webhook AS from_webhook, tct.webhook AS to_webhook FROM calls
JOIN telephone_config AS tcf ON tcf.id = calls.from_id JOIN telephone_config AS tct ON tct.id = calls.to_id"""):
cache_call(row["from_channel"], row["to_channel"], row["to_webhook"])
cache_call(row["to_channel"], row["from_channel"], row["from_webhook"])
bot.loop.create_task(populate_cache(bot.database))
@bot.listen("on_message")
async def forward_call_messages(message):
calls = channel_calls_cache.get(message.channel.id, None)
if not calls: return
if (message.author.discriminator == "0000" and message.author.bot) or message.author == bot.user or message.content == "": # check if webhook, from itself, or only has embeds
return
async def send_to(call):
other_channel, other_webhook = call
if other_webhook:
await discord.Webhook.from_url(other_webhook, adapter=discord.AsyncWebhookAdapter(bot.http._HTTPClient__session)).send(
content=message.content, username=message.author.name, avatar_url=message.author.avatar_url,
allowed_mentions=discord.AllowedMentions(everyone=False))
else:
m = f"**{message.author.name}**: "
m += message.content[:2000 - len(m)]
await bot.get_channel(other_channel).send(m)
await asyncio.gather(*map(send_to, calls))
@telephone.command()
@commands.check(server_mod_check)
async def setup(ctx):
await ctx.send("Configuring...")
num = generate_address(ctx)
await ctx.send(f"Your address is {num}.")
info = await get_addr_config(num)
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
await ctx.send("Created webhook.")
except discord.Forbidden as 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 bot.database.execute("INSERT OR REPLACE INTO telephone_config VALUES (?, ?, ?, ?)", (num, ctx.guild.id, ctx.channel.id, webhook))
await bot.database.commit()
await ctx.send("Configured.")
@telephone.command(aliases=["call"])
async def dial(ctx, address):
# basic checks - ensure this is a phone channel and has no other open calls
channel_info = await get_channel_config(ctx.channel.id)
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."))
recv_info = await 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."))
current_call = await 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."))
# post embed in the receiving channel prompting people to accept/decline call
recv_channel = bot.get_channel(recv_info["channel_id"])
_, 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("")
)
def check(re, u): return (str(re.emoji) == "" or str(re.emoji) == "") and u != bot.user
# wait until someone clicks the reactions, or time out and say so
try:
reaction, user = await bot.wait_for("reaction_add", timeout=util.config["call_timeout"], check=check)
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"))
)
await asyncio.gather(call_message.remove_reaction("", bot.user), call_message.remove_reaction("", bot.user))
em = str(reaction.emoji)
if em == "": # accept call
await bot.database.execute("INSERT INTO calls VALUES (?, ?, ?)", (originating_address, address, util.timestamp()))
await bot.database.commit()
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."))
)
cache_call(ctx.channel.id, recv_channel.id, recv_info["webhook"])
cache_call(recv_channel.id, ctx.channel.id, channel_info["webhook"])
elif em == "": # drop call
await ctx.send(embed=util.error_embed("Your call was declined.", "Call declined"))
async def get_calls(addr):
pass
@telephone.command(aliases=["disconnect", "quit"])
async def hangup(ctx):
channel_info = await get_channel_config(ctx.channel.id)
addr = channel_info["id"]
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,))
to_here = await 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."))
other = None
if from_here:
other = from_here["to_id"]
await bot.database.execute("DELETE FROM calls WHERE from_id = ? AND to_id = ?", (addr, other))
elif to_here:
other = to_here["from_id"]
await bot.database.execute("DELETE FROM calls WHERE to_id = ? AND from_id = ?", (addr, other))
await bot.database.commit()
other_channel = (await get_addr_config(other))["channel_id"]
uncache_call(ctx.channel.id, other_channel)
uncache_call(other_channel, ctx.channel.id)
await asyncio.gather(
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."))
)
@telephone.command(aliases=["status"])
async def info(ctx):
channel_info = await get_channel_config(ctx.channel.id)
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)
incoming = await 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))
outgoing = await bot.database.execute_fetchall("SELECT * FROM calls WHERE from_id = ?", (addr,))
fields.extend(map(lambda x: ["Outgoing call", f"To {x['from_id']} - for {delta(x['start_time'])}"], outgoing))
await ctx.send(embed=util.info_embed(title, f"Connected: {len(incoming) + len(outgoing)}", fields))

View File

@ -5,6 +5,13 @@ import ast
import copy
import random
from dateutil.relativedelta import relativedelta
import json
import discord
import toml
config = toml.load(open("config.toml", "r"))
def timestamp(): return int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
# from here: https://github.com/Rapptz/RoboDanny/blob/18b92ae2f53927aedebc25fb5eca02c8f6d7a874/cogs/utils/time.py
short_timedelta_regex = re.compile("""
@ -20,11 +27,11 @@ def parse_short_timedelta(text):
match = short_timedelta_regex.fullmatch(text)
if match is None or not match.group(0): raise ValueError("parse failed")
data = { k: int(v) for k, v in match.groupdict(default=0).items() }
return datetime.datetime.utcnow() + relativedelta(**data)
return datetime.datetime.now(tz=datetime.timezone.utc) + relativedelta(**data)
cal = parsedatetime.Calendar()
def parse_humantime(text):
time_struct, parse_status = cal.parse(text)
time_struct, parse_status = cal.nlp(text)
if parse_status == 1: return datetime.datetime(*time_struct[:6])
else: raise ValueError("parse failed")
@ -40,6 +47,22 @@ def parse_time(text):
def format_time(dt):
return dt.strftime("%H:%M:%S %d/%m/%Y")
timeparts = (
("y", "years"),
("mo", "months"),
("d", "days"),
("m", "minutes"),
("s", "seconds")
)
def format_timedelta(from_, to):
d = relativedelta(to, from_)
out = "0s" if d.seconds == 0 else ""
for short, attr in timeparts:
x = getattr(d, attr)
if x != 0: out += str(x) + short
return out
CODEBLOCK_REGEX = "^[^`]*```[a-zA-Z0-9_\-+]*\n(.+)```$"
CODELINE_REGEX = "^[^`]*`(.*)`$"
def extract_codeblock(s):
@ -72,6 +95,20 @@ async def async_exec(code, loc, glob):
exec(compile(wrapper, "<repl>", "exec"), loc, glob)
return await (loc.get("repl_coroutine") or glob.get("repl_coroutine"))()
def make_embed(*, fields=(), footer_text=None, **kwargs):
embed = discord.Embed(**kwargs)
for field in fields:
if len(field) > 2:
embed.add_field(name=field[0], value=field[1], inline=field[2])
else:
embed.add_field(name=field[0], value=field[1], inline=False)
if footer_text:
embed.set_footer(text=footer_text)
return embed
def error_embed(msg, title="Error"): return make_embed(color=config["colors"]["error"], description=msg, title=title)
def info_embed(title, msg, fields=()): return make_embed(color=config["colors"]["info"], description=msg, title=title, fields=fields)
# https://github.com/LyricLy/Esobot/blob/bcc9e548c84ea9b23fc832d0b0aaa8288de64886/cogs/general.py
lyrictable_raw = {
"a": "а",
@ -134,3 +171,8 @@ def apioform():
def unlyric(text):
return text.translate(lyrictable).replace("\u200b", "")
def gen_codeblock(content):
return "```\n" + content.replace("```", "\\`\\`\\`")[:1900] + "\n```"
def json_encode(x): return json.dumps(x, separators=(',', ':'))

8192
wordlist-8192.txt Normal file

File diff suppressed because it is too large Load Diff