mirror of
https://github.com/osmarks/autobotrobot
synced 2025-05-06 09:24:15 +00:00
Logging message fixes, bridge overhaul, associated fixes
This commit is contained in:
parent
a90f7fc49f
commit
9936827f4d
@ -44,7 +44,7 @@ async def achieve(bot: commands.Bot, message: discord.Message, achievement):
|
|||||||
metrics.achievements_achieved.inc()
|
metrics.achievements_achieved.inc()
|
||||||
await bot.database.execute("INSERT INTO achievements VALUES (?, ?, ?)", (uid, achievement, util.timestamp()))
|
await bot.database.execute("INSERT INTO achievements VALUES (?, ?, ?)", (uid, achievement, util.timestamp()))
|
||||||
await bot.database.commit()
|
await bot.database.commit()
|
||||||
logging.info("awarded achievement %s to %s", message.author.name, achievement)
|
logging.info("Awarded achievement %s to %s", message.author.name, achievement)
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
@bot.group(name="achievements", aliases=["ach", "achieve", "achievement"], brief="Achieve a wide variety of fun achievements!", help=f"""
|
@bot.group(name="achievements", aliases=["ach", "achieve", "achievement"], brief="Achieve a wide variety of fun achievements!", help=f"""
|
||||||
|
@ -71,7 +71,7 @@ def setup(bot):
|
|||||||
@magic.command(help="Reload configuration file.")
|
@magic.command(help="Reload configuration file.")
|
||||||
async def reload_config(ctx):
|
async def reload_config(ctx):
|
||||||
util.load_config()
|
util.load_config()
|
||||||
ctx.send("Done!")
|
await ctx.send("Done!")
|
||||||
|
|
||||||
@magic.command(help="Reload extensions (all or the specified one).")
|
@magic.command(help="Reload extensions (all or the specified one).")
|
||||||
async def reload_ext(ctx, ext="all"):
|
async def reload_ext(ctx, ext="all"):
|
||||||
|
90
src/discord_link.py
Normal file
90
src/discord_link.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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())
|
@ -22,7 +22,7 @@ def unpack_dataclass_without(d, without):
|
|||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class Message:
|
class Message:
|
||||||
author: AuthorInfo
|
author: AuthorInfo
|
||||||
message: str
|
message: list[typing.Union[str, dict]]
|
||||||
source: (str, any)
|
source: (str, any)
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
@ -75,7 +75,9 @@ async def push(msg: Message):
|
|||||||
for listener in listeners[dest_type]:
|
for listener in listeners[dest_type]:
|
||||||
asyncio.ensure_future(listener(dest_channel, msg))
|
asyncio.ensure_future(listener(dest_channel, msg))
|
||||||
|
|
||||||
def add_listener(s, l): listeners[s].add(l)
|
def add_listener(s, l):
|
||||||
|
listeners[s].add(l)
|
||||||
|
return lambda: listeners[s].remove(l)
|
||||||
|
|
||||||
async def add_bridge_link(db, c1, c2):
|
async def add_bridge_link(db, c1, c2):
|
||||||
logging.info("Bridging %s and %s", repr(c1), repr(c2))
|
logging.info("Bridging %s and %s", repr(c1), repr(c2))
|
||||||
|
@ -5,6 +5,7 @@ import random
|
|||||||
import util
|
import util
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import discord.ext.commands as commands
|
||||||
|
|
||||||
def scramble(text):
|
def scramble(text):
|
||||||
n = list(text)
|
n = list(text)
|
||||||
@ -15,7 +16,23 @@ def color_code(x):
|
|||||||
return f"\x03{x}"
|
return f"\x03{x}"
|
||||||
def random_color(id): return color_code(hashlib.blake2b(str(id).encode("utf-8")).digest()[0] % 13 + 2)
|
def random_color(id): return color_code(hashlib.blake2b(str(id).encode("utf-8")).digest()[0] % 13 + 2)
|
||||||
|
|
||||||
|
def render_formatting(message):
|
||||||
|
out = ""
|
||||||
|
for seg in message:
|
||||||
|
if isinstance(seg, str):
|
||||||
|
out += seg.replace("\n", " ")
|
||||||
|
else:
|
||||||
|
kind = seg["type"]
|
||||||
|
# TODO: check if user exists on both ends, and possibly drop if so
|
||||||
|
if kind == "user_mention":
|
||||||
|
out += f"@{random_color(seg['id'])}{seg['name']}{color_code('')}"
|
||||||
|
elif kind == "channel_mention": # these appear to be clickable across servers/guilds
|
||||||
|
out += f"#{seg['name']}"
|
||||||
|
else: logging.warn("Unrecognized message seg %s", kind)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
global_conn = None
|
global_conn = None
|
||||||
|
unlisten = None
|
||||||
|
|
||||||
async def initialize():
|
async def initialize():
|
||||||
logging.info("Initializing IRC link")
|
logging.info("Initializing IRC link")
|
||||||
@ -32,15 +49,14 @@ async def initialize():
|
|||||||
conn.nick(scramble(conn.get_nickname()))
|
conn.nick(scramble(conn.get_nickname()))
|
||||||
|
|
||||||
def pubmsg(conn, event):
|
def pubmsg(conn, event):
|
||||||
msg = eventbus.Message(eventbus.AuthorInfo(event.source.nick, str(event.source), None), " ".join(event.arguments), (util.config["irc"]["name"], event.target), util.random_id())
|
msg = eventbus.Message(eventbus.AuthorInfo(event.source.nick, str(event.source), None), [" ".join(event.arguments)], (util.config["irc"]["name"], event.target), util.random_id())
|
||||||
asyncio.create_task(eventbus.push(msg))
|
asyncio.create_task(eventbus.push(msg))
|
||||||
|
|
||||||
async def on_bridge_message(channel_name, msg):
|
async def on_bridge_message(channel_name, msg):
|
||||||
if channel_name in util.config["irc"]["channels"]:
|
if channel_name in util.config["irc"]["channels"]:
|
||||||
if channel_name not in joined: conn.join(channel_name)
|
if channel_name not in joined: conn.join(channel_name)
|
||||||
line = msg.message.replace("\n", " ")
|
|
||||||
# ping fix - zero width space embedded in messages
|
# ping fix - zero width space embedded in messages
|
||||||
line = f"<{random_color(msg.author.id)}{msg.author.name[0]}\u200B{msg.author.name[1:]}{color_code('')}> " + line.strip()[:400]
|
line = f"<{random_color(msg.author.id)}{msg.author.name[0]}\u200B{msg.author.name[1:]}{color_code('')}> " + render_formatting(msg.message)[:400]
|
||||||
conn.privmsg(channel_name, line)
|
conn.privmsg(channel_name, line)
|
||||||
else:
|
else:
|
||||||
logging.warning("IRC channel %s not allowed", channel_name)
|
logging.warning("IRC channel %s not allowed", channel_name)
|
||||||
@ -48,19 +64,21 @@ async def initialize():
|
|||||||
def connect(conn, event):
|
def connect(conn, event):
|
||||||
for channel in util.config["irc"]["channels"]:
|
for channel in util.config["irc"]["channels"]:
|
||||||
conn.join(channel)
|
conn.join(channel)
|
||||||
logging.info("connected to %s", channel)
|
logging.info("Connected to %s on IRC", channel)
|
||||||
joined.add(channel)
|
joined.add(channel)
|
||||||
|
|
||||||
# TODO: do better thing
|
# TODO: do better thing
|
||||||
conn.add_global_handler("welcome", connect)
|
conn.add_global_handler("welcome", connect)
|
||||||
conn.add_global_handler("disconnect", lambda conn, event: logging.warn("disconnected from IRC, oh no"))
|
conn.add_global_handler("disconnect", lambda conn, event: logging.warn("Disconnected from IRC"))
|
||||||
conn.add_global_handler("nicknameinuse", inuse)
|
conn.add_global_handler("nicknameinuse", inuse)
|
||||||
conn.add_global_handler("pubmsg", pubmsg)
|
conn.add_global_handler("pubmsg", pubmsg)
|
||||||
|
|
||||||
eventbus.add_listener(util.config["irc"]["name"], on_bridge_message)
|
global unlisten
|
||||||
|
unlisten = eventbus.add_listener(util.config["irc"]["name"], on_bridge_message)
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
asyncio.create_task(initialize())
|
asyncio.create_task(initialize())
|
||||||
|
|
||||||
def teardown(bot):
|
def teardown(bot):
|
||||||
if global_conn: global_conn.disconnect()
|
if global_conn: global_conn.disconnect()
|
||||||
|
if unlisten: unlisten()
|
45
src/main.py
45
src/main.py
@ -51,14 +51,16 @@ command_errors = prometheus_client.Counter("abr_errors", "Count of errors encoun
|
|||||||
@bot.event
|
@bot.event
|
||||||
async def on_command_error(ctx, err):
|
async def on_command_error(ctx, err):
|
||||||
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return
|
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return
|
||||||
if isinstance(err, commands.CommandInvokeError) and isinstance(err.original, ValueError): return await ctx.send(embed=util.error_embed(str(err.original)))
|
if isinstance(err, commands.CommandInvokeError) and isinstance(err.original, ValueError):
|
||||||
|
return await ctx.send(embed=util.error_embed(str(err.original), title=f"Error in {ctx.invoked_with}"))
|
||||||
# TODO: really should find a way to detect ALL user errors here?
|
# TODO: really should find a way to detect ALL user errors here?
|
||||||
if isinstance(err, (commands.UserInputError)): return await ctx.send(embed=util.error_embed(str(err)))
|
if isinstance(err, (commands.UserInputError)):
|
||||||
|
return await ctx.send(embed=util.error_embed(str(err), title=f"Error in {ctx.invoked_with}"))
|
||||||
try:
|
try:
|
||||||
command_errors.inc()
|
command_errors.inc()
|
||||||
trace = re.sub("\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__)))
|
trace = re.sub("\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__)))
|
||||||
logging.error("command error occured (in %s)", ctx.invoked_with, exc_info=err)
|
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"))
|
await ctx.send(embed=util.error_embed(util.gen_codeblock(trace), title=f"Internal error in {ctx.invoked_with}"))
|
||||||
await achievement.achieve(ctx.bot, ctx.message, "error")
|
await achievement.achieve(ctx.bot, ctx.message, "error")
|
||||||
except Exception as e: print("meta-error:", e)
|
except Exception as e: print("meta-error:", e)
|
||||||
|
|
||||||
@ -72,38 +74,6 @@ async def on_ready():
|
|||||||
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"{bot.command_prefix}help", type=discord.ActivityType.listening))
|
||||||
|
|
||||||
webhooks = {}
|
|
||||||
|
|
||||||
async def initial_load_webhooks(db):
|
|
||||||
for row in await db.execute_fetchall("SELECT * FROM discord_webhooks"):
|
|
||||||
webhooks[row["channel_id"]] = row["webhook"]
|
|
||||||
|
|
||||||
@bot.listen("on_message")
|
|
||||||
async def send_to_bridge(msg):
|
|
||||||
# discard webhooks and bridge messages (hackily, admittedly, not sure how else to do this)
|
|
||||||
if (msg.author == bot.user and msg.content[0] == "<") or msg.author.discriminator == "0000": return
|
|
||||||
if msg.content == "": 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), msg.content, ("discord", channel_id), msg.id)
|
|
||||||
await eventbus.push(msg)
|
|
||||||
|
|
||||||
async def on_bridge_message(channel_id, msg):
|
|
||||||
channel = bot.get_channel(channel_id)
|
|
||||||
if channel:
|
|
||||||
webhook = webhooks.get(channel_id)
|
|
||||||
if webhook:
|
|
||||||
wh_obj = discord.Webhook.from_url(webhook, adapter=discord.AsyncWebhookAdapter(bot.http._HTTPClient__session))
|
|
||||||
await wh_obj.send(
|
|
||||||
content=msg.message, 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}> {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)
|
|
||||||
|
|
||||||
eventbus.add_listener("discord", on_bridge_message)
|
|
||||||
|
|
||||||
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():
|
||||||
return len(bot.users)
|
return len(bot.users)
|
||||||
@ -128,9 +98,8 @@ guild_count.set_function(get_guild_count)
|
|||||||
async def run_bot():
|
async def run_bot():
|
||||||
bot.database = await db.init(config["database"])
|
bot.database = await db.init(config["database"])
|
||||||
await eventbus.initial_load(bot.database)
|
await eventbus.initial_load(bot.database)
|
||||||
await initial_load_webhooks(bot.database)
|
|
||||||
for ext in util.extensions:
|
for ext in util.extensions:
|
||||||
logging.info("loaded %s", ext)
|
logging.info("Loaded %s", ext)
|
||||||
bot.load_extension(ext)
|
bot.load_extension(ext)
|
||||||
await bot.start(config["token"])
|
await bot.start(config["token"])
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ def setup(bot):
|
|||||||
metrics.reminders_fired.inc()
|
metrics.reminders_fired.inc()
|
||||||
to_expire.append((1, rid)) # 1 = expired normally
|
to_expire.append((1, rid)) # 1 = expired normally
|
||||||
break
|
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("Failed to send %d to %s", rid, method_name, exc_info=e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning("Could not send reminder %d", rid, exc_info=e)
|
logging.warning("Could not send reminder %d", rid, exc_info=e)
|
||||||
to_expire.append((2, rid)) # 2 = errored
|
to_expire.append((2, rid)) # 2 = errored
|
||||||
|
@ -38,20 +38,22 @@ def setup(bot):
|
|||||||
""")
|
""")
|
||||||
@commands.check(util.admin_check)
|
@commands.check(util.admin_check)
|
||||||
async def link(ctx, target_type, target_id):
|
async def link(ctx, target_type, 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, util.extract_codeblock(target_id)))
|
await eventbus.add_bridge_link(bot.database, ("discord", ctx.channel.id), (target_type, target_id))
|
||||||
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(ctx, target_type, 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, util.extract_codeblock(target_id)))
|
await eventbus.remove_bridge_link(bot.database, ("discord", ctx.channel.id), (target_type, target_id))
|
||||||
await ctx.send(f"Successfully deleted.")
|
await ctx.send(f"Successfully deleted.")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -77,7 +79,7 @@ def setup(bot):
|
|||||||
await bot.database.execute("INSERT OR REPLACE INTO discord_webhooks VALUES (?, ?)", (ctx.channel.id, webhook))
|
await 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 bot.database.execute("INSERT OR REPLACE INTO telephone_config VALUES (?, ?, ?, ?)", (num, ctx.guild.id, ctx.channel.id, webhook))
|
||||||
await bot.database.commit()
|
await bot.database.commit()
|
||||||
|
@ -264,7 +264,8 @@ extensions = (
|
|||||||
"voice",
|
"voice",
|
||||||
"commands",
|
"commands",
|
||||||
"userdata",
|
"userdata",
|
||||||
"irc_link"
|
"irc_link",
|
||||||
|
"discord_link"
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://github.com/SawdustSoftware/simpleflake/blob/master/simpleflake/simpleflake.py
|
# https://github.com/SawdustSoftware/simpleflake/blob/master/simpleflake/simpleflake.py
|
||||||
|
Loading…
x
Reference in New Issue
Block a user