Compare commits
No commits in common. "bd5706c0ce46b811ed67f4a69e8a120fcb22a17e" and "49a7ac9fd7f19f0578ea3fe0dd3fe3f556fd854a" have entirely different histories.
bd5706c0ce
...
49a7ac9fd7
6 changed files with 137 additions and 411 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,7 +10,6 @@ result
|
|||
# Python
|
||||
.ruff_cache
|
||||
.ropeproject
|
||||
**/__pycache__/
|
||||
|
||||
# Runtime
|
||||
*.sqlite3
|
||||
|
|
41
nomen/db.py
41
nomen/db.py
|
@ -12,6 +12,9 @@ log.setLevel(logging.INFO)
|
|||
schema = """
|
||||
PRAGMA user_version = 1;
|
||||
|
||||
PRAGMA main.synchronous = NORMAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE keywords (
|
||||
guild_id INTEGER NOT NULL,
|
||||
keyword TEXT NOT NULL,
|
||||
|
@ -32,7 +35,7 @@ CREATE TABLE users (
|
|||
use_embed INTEGER NOT NULL DEFAULT 1 CHECK(use_embed IN (0, 1)),
|
||||
notify_self INTEGER NOT NULL DEFAULT 0 CHECK(notify_self IN (0, 1)),
|
||||
bots_notify INTEGER NOT NULL DEFAULT 0 CHECK(bots_notify IN (0, 1)),
|
||||
ignore_active INTEGER NOT NULL DEFAULT 0 CHECK(ignore_active IN (0, 1))
|
||||
ignore_active INTEGER NOT NULL DEFAULT 0 CHECK(bots_notify IN (0, 1))
|
||||
)
|
||||
WITHOUT ROWID;
|
||||
|
||||
|
@ -70,37 +73,6 @@ def run_db_migrations(db_file):
|
|||
log.debug("Finished running automatic migration")
|
||||
|
||||
|
||||
async def fetch_singleton(db, sql, params=None):
|
||||
"""
|
||||
Fetch an object from the database, with the assumption that the result is 1 row by 1 column
|
||||
"""
|
||||
|
||||
result = await db.execute_fetchall(f"{sql} LIMIT 1", params)
|
||||
return result[0][0]
|
||||
|
||||
|
||||
async def fetch_exists(db, sql, params=None):
|
||||
return await fetch_singleton(db, f"SELECT EXISTS({sql})", params)
|
||||
|
||||
|
||||
async def fetch_unpacked(db, sql, params=None):
|
||||
cur = await db.cursor()
|
||||
cur.row_factory = lambda cursor, row: row[0]
|
||||
cur = await cur.execute(sql, params)
|
||||
return await cur.fetchall()
|
||||
|
||||
|
||||
log.debug("Monkeypatching in helpers")
|
||||
aiosqlite.Connection.fetch_singleton = fetch_singleton
|
||||
aiosqlite.Connection.fetch_exists = fetch_exists
|
||||
aiosqlite.Connection.fetch_unpacked = fetch_unpacked
|
||||
|
||||
|
||||
class Row(sqlite3.Row):
|
||||
def __repr__(self):
|
||||
return f"Row<{repr(dict(self))}>"
|
||||
|
||||
|
||||
async def setup_db(db_file):
|
||||
log.debug(f"Connecting to {db_file}")
|
||||
db = await aiosqlite.connect(db_file)
|
||||
|
@ -108,13 +80,8 @@ async def setup_db(db_file):
|
|||
log.debug("Running start script")
|
||||
await db.executescript("""
|
||||
PRAGMA optimize(0x10002);
|
||||
PRAGMA main.synchronous = NORMAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
""")
|
||||
|
||||
log.debug("Setting row factory")
|
||||
db.row_factory = Row
|
||||
|
||||
log.debug("Adding contains function")
|
||||
await db.create_function("contains", 3, contains, deterministic=True)
|
||||
|
||||
|
|
106
nomen/main.py
106
nomen/main.py
|
@ -1,7 +1,6 @@
|
|||
import io
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
|
@ -9,7 +8,7 @@ from contextlib import redirect_stdout
|
|||
|
||||
from disnake import Guild, Intents, Message
|
||||
from disnake.ext import commands
|
||||
from disnake.ext.commands import Bot, Paginator
|
||||
from disnake.ext.commands import Bot
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
from .db import run_db_migrations, setup_db
|
||||
|
@ -90,11 +89,11 @@ class Nomen(Bot):
|
|||
await super().close()
|
||||
await self.db.close()
|
||||
|
||||
async def get_setting(self, user_id, setting):
|
||||
return await self.db.fetch_singleton(f"SELECT {setting} FROM users WHERE user_id=?", (user_id,))
|
||||
|
||||
async def set_setting(self, user_id, setting, value: bool):
|
||||
await self.db.execute(f"REPLACE INTO users (user_id, {setting}) VALUES(?, ?)", (user_id, value))
|
||||
async def user_toggle(self, user_id, item):
|
||||
await self.db.execute(
|
||||
"REPLACE INTO users (user_id, {item}) VALUES(:user_id, iff((SELECT {item} FROM users WHERE user_id=:user_id)), 0, 1)",
|
||||
{"user_id": user_id},
|
||||
)
|
||||
await self.db.commit()
|
||||
|
||||
|
||||
|
@ -121,68 +120,18 @@ async def on_ready():
|
|||
log.info(bot.user.id)
|
||||
log.info("-------------")
|
||||
|
||||
bot.app_info = await bot.application_info()
|
||||
bot.owner = bot.app_info.owner
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx, error):
|
||||
if isinstance(error, commands.CommandNotFound):
|
||||
return
|
||||
|
||||
if isinstance(
|
||||
error,
|
||||
(
|
||||
commands.UserInputError,
|
||||
commands.CheckFailure,
|
||||
),
|
||||
):
|
||||
if isinstance(error, commands.NoPrivateMessage):
|
||||
await ctx.send(error)
|
||||
return
|
||||
|
||||
log.error(error, exc_info=error)
|
||||
timestamp = int(ctx.message.created_at.timestamp())
|
||||
paginator = Paginator()
|
||||
ptraceback = "".join(traceback.format_exception(error))
|
||||
for line in ptraceback.split("\n"):
|
||||
paginator.add_line(line)
|
||||
await ctx.bot.owner.send(
|
||||
f"# Command Error Occurred: {error}\n"
|
||||
f"## Context\n"
|
||||
f"**From:** {ctx.author.mention} ({ctx.author.name})\n"
|
||||
f"<t:{timestamp}>, <t:{timestamp}:R> | [Jump to Command]({ctx.message.jump_url}) | <#{ctx.channel.id}>\n"
|
||||
f"## Traceback"
|
||||
)
|
||||
for page in paginator.pages:
|
||||
await ctx.bot.owner.send(page)
|
||||
await ctx.send(
|
||||
f"Error: {error}\n\n**Note:** Infinidoge (Developer) has already been sent a notification that an error occurred and will come investigate when available."
|
||||
)
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_error(event, *args, **kwargs):
|
||||
pargs = pprint.pformat(args)
|
||||
pkwargs = pprint.pformat(kwargs)
|
||||
|
||||
try:
|
||||
paginator = Paginator()
|
||||
for line in traceback.format_exc().split("\n"):
|
||||
paginator.add_line(line)
|
||||
await bot.owner.send(
|
||||
f"# Error Occurred: Event `{event[:100]}`\n"
|
||||
f"## args\n"
|
||||
f"```\n{pargs}\n```\n"
|
||||
f"## kwargs\n"
|
||||
f"```\n{pkwargs}\n```\n"
|
||||
f"## Traceback"
|
||||
)
|
||||
for page in paginator.pages:
|
||||
await bot.owner.send(page)
|
||||
except: # Don't want recursive errors
|
||||
pass
|
||||
|
||||
log.error(f"Error in event {event}.\nargs={pargs}\nkwargs={pkwargs}", exc_info=sys.exc_info())
|
||||
log.exception(error)
|
||||
await ctx.send(error)
|
||||
|
||||
|
||||
@bot.command(hidden=True)
|
||||
|
@ -209,13 +158,13 @@ async def sql(ctx, *, query):
|
|||
|
||||
@bot.command(hidden=True, name="eval")
|
||||
@commands.is_owner()
|
||||
async def admin_eval(ctx, *, body: str):
|
||||
async def admin_eval(self, ctx, *, body: str):
|
||||
env = {
|
||||
"bot": ctx.bot,
|
||||
"bot": self.bot,
|
||||
"ctx": ctx,
|
||||
"channel": ctx.channel,
|
||||
"author": ctx.author,
|
||||
"guild": ctx.guild,
|
||||
"channel": ctx.message.channel,
|
||||
"author": ctx.message.author,
|
||||
"guild": ctx.message.guild,
|
||||
"message": ctx.message,
|
||||
}
|
||||
|
||||
|
@ -237,11 +186,7 @@ async def admin_eval(ctx, *, body: str):
|
|||
ret = await func()
|
||||
except Exception:
|
||||
value = stdout.getvalue()
|
||||
ret = traceback.format_exc()
|
||||
try:
|
||||
await ctx.message.add_reaction("\u2705")
|
||||
except:
|
||||
pass
|
||||
await ctx.send("```py\n{}{}\n```".format(value, traceback.format_exc()))
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
try:
|
||||
|
@ -249,18 +194,11 @@ async def admin_eval(ctx, *, body: str):
|
|||
except:
|
||||
pass
|
||||
|
||||
paginator = Paginator(prefix="```py", suffix="```")
|
||||
if ret is None:
|
||||
if value:
|
||||
for line in str(value).split("\n"):
|
||||
paginator.add_line(line)
|
||||
paginator.close_page()
|
||||
if ret:
|
||||
for line in str(ret).split("\n"):
|
||||
paginator.add_line(line)
|
||||
paginator.close_page()
|
||||
|
||||
for page in paginator.pages:
|
||||
await ctx.send(page)
|
||||
await ctx.send("```py\n{}\n```".format(value))
|
||||
else:
|
||||
await ctx.send("```py\n{}{}\n```".format(value, ret))
|
||||
|
||||
|
||||
@bot.command()
|
||||
|
@ -269,7 +207,8 @@ async def ping(ctx):
|
|||
|
||||
|
||||
@bot.command()
|
||||
@commands.has_guild_permissions(manage_guild=True)
|
||||
@commands.guild_only()
|
||||
@commands.is_owner()
|
||||
async def prefix(ctx, prefix=None):
|
||||
if prefix is None:
|
||||
prefix = await ctx.bot.get_guild_prefix(ctx.guild)
|
||||
|
@ -287,7 +226,8 @@ async def prefix(ctx, prefix=None):
|
|||
|
||||
def run():
|
||||
try:
|
||||
run_db_migrations(DB_FILE)
|
||||
if run_db_migrations(DB_FILE):
|
||||
log.info(f"Migrated DB {DB_FILE}")
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
|
|
|
@ -3,11 +3,10 @@ from asyncio import TaskGroup
|
|||
from textwrap import indent
|
||||
from typing import Union
|
||||
|
||||
from disnake import Embed, Member, Thread, User
|
||||
from disnake.abc import GuildChannel
|
||||
from disnake import Embed, Member, TextChannel
|
||||
from disnake.ext.commands import Cog, group, guild_only
|
||||
|
||||
from .utils import can_view, confirm, test_keyword
|
||||
from .utils import can_view, confirm, fetch_exists, fetch_unpacked, test_keyword
|
||||
|
||||
log = logging.getLogger("nomen.notifications")
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
@ -22,19 +21,6 @@ class NotifierLogAdapter(logging.LoggerAdapter):
|
|||
)
|
||||
|
||||
|
||||
class KeywordError(Exception):
|
||||
def __init__(self, msg=None, dm_msg=None, *args):
|
||||
self.msg = msg
|
||||
self.dm_msg = dm_msg
|
||||
super().__init__(msg, dm_msg, *args)
|
||||
|
||||
async def send(self, ctx):
|
||||
if self.msg:
|
||||
await ctx.send(self.msg + " (check DMs)")
|
||||
if self.dm_msg:
|
||||
await ctx.author.send(self.dm_msg)
|
||||
|
||||
|
||||
async def handle_notification(db_updates, ctx, message, keyword, user_id, use_embed):
|
||||
"""
|
||||
Async task to dispatch a notification
|
||||
|
@ -82,18 +68,13 @@ async def handle_triggers(ctx, message):
|
|||
params = {
|
||||
"author": ctx.author.id,
|
||||
"channel": ctx.channel.id,
|
||||
"category": ctx.channel.category_id,
|
||||
"guild": ctx.guild.id,
|
||||
"content": message.content,
|
||||
"is_bot": ctx.author.bot,
|
||||
"parent": 0,
|
||||
}
|
||||
|
||||
if isinstance(ctx.channel, Thread) and ctx.channel.parent:
|
||||
params["parent"] = ctx.channel.parent.id
|
||||
|
||||
disabled = await ctx.bot.db.fetch_exists(
|
||||
"SELECT * FROM users WHERE user_id=:author AND unlikely(disabled IS 1)", params
|
||||
disabled = await fetch_exists(
|
||||
ctx.bot.db, "SELECT * FROM users WHERE user_id=:author AND unlikely(disabled IS 1)", params
|
||||
)
|
||||
|
||||
if disabled:
|
||||
|
@ -113,12 +94,11 @@ async def handle_triggers(ctx, message):
|
|||
AND (unlikely(bots_notify IS 1) OR :is_bot IS NOT 1) -- Don't notify from bots unless wanted
|
||||
AND likely(NOT EXISTS(SELECT * FROM ( -- Don't notify if...
|
||||
SELECT target FROM user_ignores -- Author or channel is ignored in this guild
|
||||
WHERE user_id=user_id AND guild_id=:guild
|
||||
WHERE user_id=user_id AND guild_id=guild_id
|
||||
UNION
|
||||
SELECT target FROM user_blocks -- Author is blocked
|
||||
WHERE user_id=user_id
|
||||
) WHERE target IN (:author, :channel, :category, :parent)))
|
||||
AND likely(NOT EXISTS(SELECT * FROM user_pauses WHERE user_id=user_id AND guild_id=:guild))
|
||||
) WHERE target IN (:author, :channel)))
|
||||
AND guild_id=:guild AND unlikely(contains(:content, keyword, regex))
|
||||
)
|
||||
WHERE n=1
|
||||
|
@ -158,8 +138,8 @@ class Notifications(Cog):
|
|||
|
||||
await handle_triggers(ctx, message)
|
||||
|
||||
async def cog_before_invoke(self, ctx):
|
||||
await ctx.bot.db.execute("INSERT OR IGNORE INTO users (user_id) VALUES(?)", (ctx.author.id,))
|
||||
async def cog_before_invoke(ctx):
|
||||
await ctx.bot.db.execute("INSERT OR IGNORE INTO users (userid) VALUE(?)", (ctx.author.id,))
|
||||
|
||||
@group(
|
||||
aliases=["kw", "notification", "notifications", "notif", "noti"],
|
||||
|
@ -171,47 +151,64 @@ class Notifications(Cog):
|
|||
"""
|
||||
await ctx.send_help(self.keyword)
|
||||
|
||||
async def _check_conflicts(self, guild_id, user_id, keyword):
|
||||
return await self.bot.db.fetch_unpacked(
|
||||
"SELECT keyword FROM keywords WHERE guild_id=? AND user_id=? AND contains(?, keyword, regex)",
|
||||
(guild_id, user_id, keyword),
|
||||
)
|
||||
|
||||
async def _check_redundants(self, guild_id, user_id, keyword, regex):
|
||||
return await self.bot.db.fetch_unpacked(
|
||||
"SELECT keyword FROM keywords WHERE guild_id=? AND user_id=? AND contains(keyword, ?, ?)",
|
||||
(guild_id, user_id, keyword, regex),
|
||||
)
|
||||
|
||||
async def add_keyword(self, guild_id: int, user_id: int, keyword: str, regex: bool, initial_count: int = 0):
|
||||
async def _add_keyword(self, ctx, keyword: str, regex: bool):
|
||||
log.debug(
|
||||
f"Adding {'regex' if regex else 'keyword'}: {keyword} of {user_id} on {guild_id} with count {initial_count}"
|
||||
f"Adding {'regex' if regex else 'keyword'}: {keyword} of {ctx.author} ({ctx.author.id}) on {ctx.guild} ({ctx.guild.id})"
|
||||
)
|
||||
|
||||
params = {
|
||||
"keyword": keyword,
|
||||
"guild_id": ctx.guild.id,
|
||||
"user_id": ctx.author.id,
|
||||
"regex": regex,
|
||||
}
|
||||
|
||||
existing = """
|
||||
SELECT keyword FROM keywords
|
||||
WHERE guild_id=:guild_id
|
||||
AND user_id=:user_id
|
||||
AND contains(:keyword, keyword, regex)
|
||||
"""
|
||||
redundant = """
|
||||
SELECT keyword FROM keywords
|
||||
WHERE guild_id=:guild_id
|
||||
AND user_id=:user_id
|
||||
AND contains(keyword, :keyword, :regex)
|
||||
"""
|
||||
|
||||
if test_keyword(keyword, regex):
|
||||
log.debug("Keyword too common")
|
||||
raise KeywordError(f"{'Regex' if regex else 'Keyword'} matches a word that is too common")
|
||||
await ctx.send(f"{'Regex' if regex else 'Keyword'} matches a word that is too common")
|
||||
return
|
||||
|
||||
if conflicts := await self._check_conflicts(guild_id, user_id, keyword):
|
||||
conflicts = await fetch_unpacked(ctx.bot.db, existing, params)
|
||||
|
||||
if conflicts:
|
||||
log.debug("Keyword conflicts with existing keyword")
|
||||
raise KeywordError(
|
||||
f"Any instance of `{keyword}` would be matched by existing keywords",
|
||||
f"Conflicts with keyword `{keyword}`:\n" + "\n".join(f"- `{conflict}`" for conflict in conflicts),
|
||||
await ctx.send(f"Any instance of `{keyword}` would be matched by existing keywords (check DMs)")
|
||||
await ctx.author.send(
|
||||
f"Conflicts with keyword `{keyword}`:\n" + "\n".join(f"- `{conflict}`" for conflict in conflicts)
|
||||
)
|
||||
return
|
||||
|
||||
if redundants := await self._check_redundants(guild_id, user_id, keyword, regex):
|
||||
conflicts = await fetch_unpacked(ctx.bot.db, redundant, params)
|
||||
|
||||
if conflicts:
|
||||
log.debug("Keyword renders existing redundant")
|
||||
raise KeywordError(
|
||||
f"Adding `{keyword}` will cause existing keywords to never match",
|
||||
f"Keywords redundant from `{keyword}`:\n" + "\n".join(f" - `{conflict}`" for conflict in redundants),
|
||||
await ctx.send(f"Adding `{keyword}` will cause existing keywords to never match (check DMs)")
|
||||
await ctx.author.send(
|
||||
f"Keywords redundant from `{keyword}`:\n" + "\n".join(f" - `{conflict}`" for conflict in conflicts)
|
||||
)
|
||||
return
|
||||
|
||||
log.debug("Keyword valid, adding")
|
||||
|
||||
await self.bot.db.execute(
|
||||
"INSERT INTO keywords VALUES (?, ?, ?, ?, ?)", (guild_id, keyword, user_id, regex, initial_count)
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO keywords (guild_id, keyword, user_id, regex) VALUES (:guild_id, :keyword, :user_id, :regex)",
|
||||
params,
|
||||
)
|
||||
await self.bot.db.commit()
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.send(f"Added `{keyword}` to your list of keywords")
|
||||
|
||||
@keyword.command()
|
||||
@guild_only()
|
||||
|
@ -222,12 +219,7 @@ class Notifications(Cog):
|
|||
Use quotes for a keyword with spaces!
|
||||
"""
|
||||
|
||||
try:
|
||||
await self.add_keyword(ctx.guild.id, ctx.author.id, keyword, False)
|
||||
except KeywordError as e:
|
||||
await e.send(ctx)
|
||||
else:
|
||||
await ctx.send(f"Added `{keyword}` to your list of keywords")
|
||||
await self._add_keyword(ctx, keyword, False)
|
||||
|
||||
@keyword.command()
|
||||
@guild_only()
|
||||
|
@ -238,13 +230,7 @@ class Notifications(Cog):
|
|||
Use quotes for a regex with spaces!
|
||||
"""
|
||||
|
||||
# TODO: Add regex names to make notifications cleaner
|
||||
try:
|
||||
await self.add_keyword(ctx.guild.id, ctx.author.id, keyword, True)
|
||||
except KeywordError as e:
|
||||
await e.send(ctx)
|
||||
else:
|
||||
await ctx.send(f"Added regex `{keyword}` to your list of keywords")
|
||||
await self._add_keyword(ctx, keyword, True)
|
||||
|
||||
@keyword.command(aliases=["delete", "del"])
|
||||
@guild_only()
|
||||
|
@ -272,9 +258,11 @@ class Notifications(Cog):
|
|||
|
||||
log.debug(f"Listing keywords: {ctx.author} ({ctx.author.id}) on {ctx.guild} ({ctx.guild.id})")
|
||||
|
||||
keywords = await ctx.bot.db.execute_fetchall(
|
||||
"SELECT keyword, count, regex FROM keywords WHERE guild_id=? AND user_id=?", (ctx.guild.id, ctx.author.id)
|
||||
)
|
||||
async with ctx.bot.db.execute(
|
||||
"SELECT keyword, count, regex FROM keywords WHERE guild_id=? AND user_id=?",
|
||||
(ctx.guild.id, ctx.author.id),
|
||||
) as cur:
|
||||
keywords = await cur.fetchall()
|
||||
|
||||
embed = Embed(
|
||||
description="\n".join(
|
||||
|
@ -315,110 +303,32 @@ class Notifications(Cog):
|
|||
@keyword.command()
|
||||
@guild_only()
|
||||
async def pause(self, ctx):
|
||||
"""
|
||||
Pause notifications in this guild
|
||||
"""
|
||||
|
||||
params = (ctx.author.id, ctx.guild.id)
|
||||
if await ctx.bot.db.fetch_exists("SELECT * FROM user_pauses WHERE user_id=? AND guild_id=?", params):
|
||||
await ctx.send(f"Notifications in {ctx.guild} are already paused")
|
||||
params = (ctx.author.id, ctx.guild_id)
|
||||
if await fetch_exists("SELECT * FROM user_pauses WHERE user_id=? AND guild_id=?"):
|
||||
await ctx.bot.db.execute("DELETE FROM user_pauses WHERE user_id=? AND guild_id=?", params)
|
||||
await ctx.bot.send(f"Resumed notifications in {ctx.guild}")
|
||||
else:
|
||||
await ctx.bot.db.execute("INSERT INTO user_pauses VALUES(?, ?)", params)
|
||||
await ctx.bot.send(f"Paused notifications in {ctx.guild}")
|
||||
# TODO: Pause guild notifications in handler
|
||||
|
||||
@keyword.group(invoke_without_command=True)
|
||||
@guild_only()
|
||||
async def ignore(self, ctx, target: Union[TextChannel, Member]):
|
||||
await ctx.bot.db.execute("INSERT INTO user_ignores VALUES(?, ?, ?)", (ctx.author.id, ctx.guild.id, target.id))
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.send(f"Paused notifications in {ctx.guild}")
|
||||
await ctx.send(f"Now ignoring {target}")
|
||||
|
||||
@keyword.command()
|
||||
@guild_only()
|
||||
async def resume(self, ctx):
|
||||
"""
|
||||
Resume notifications in this guild
|
||||
"""
|
||||
async def uningnore(self, ctx, target: Union[TextChannel, Member]):
|
||||
pass
|
||||
# TODO: Unignore
|
||||
|
||||
params = (ctx.author.id, ctx.guild_id)
|
||||
if await ctx.bot.db.fetch_exists("SELECT * FROM user_pauses WHERE user_id=? AND guild_id=?", params):
|
||||
await ctx.bot.db.execute("DELETE FROM user_pauses WHERE user_id=? AND guild_id=?", params)
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.send(f"Resumed notifications in {ctx.guild}")
|
||||
else:
|
||||
await ctx.send(f"Notifications in {ctx.guild} aren't paused")
|
||||
|
||||
@keyword.command()
|
||||
@ignore.command()
|
||||
@guild_only()
|
||||
async def ignore(self, ctx, target: Union[GuildChannel, Member]):
|
||||
"""
|
||||
Ignore a channel or member in this guild
|
||||
async def active(self, ctx):
|
||||
await ctx.bot.user_toggle(ctx.author.id, "ignore_active")
|
||||
await ctx.bot.send("Toggled ignore active channel") # TODO: Send current state
|
||||
|
||||
This prevents you from receiving any notifications from that channel or user in this guild
|
||||
"""
|
||||
|
||||
params = (ctx.author.id, ctx.guild.id, target.id)
|
||||
if await ctx.bot.db.fetch_exists(
|
||||
"SELECT * FROM user_ignores WHERE user_id=? AND guild_id=? AND target=?", params
|
||||
):
|
||||
await ctx.send(f"Target `{repr(target)}` already ignored")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute("INSERT INTO user_ignores VALUES(?, ?, ?)", params)
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.send(f"Now ignoring `{repr(target)}`")
|
||||
|
||||
@keyword.command()
|
||||
@guild_only()
|
||||
async def unignore(self, ctx, target: Union[GuildChannel, Member]):
|
||||
"""
|
||||
Unignore a channel or member in this guild
|
||||
|
||||
This allows you to receive notifications from that channel or user again
|
||||
"""
|
||||
|
||||
params = (ctx.author.id, ctx.guild.id, target.id)
|
||||
if not await ctx.bot.db.fetch_exists(
|
||||
"SELECT * FROM user_ignores WHERE user_id=? AND guild_id=? AND target=?", params
|
||||
):
|
||||
await ctx.send(f"Target `{repr(target)}` is not currently being ignored")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute("DELETE FROM user_ignores WHERE user_id=? AND guild_id=? AND target=?", params)
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.send(f"No longer ignoring `{repr(target)}`")
|
||||
|
||||
@keyword.command()
|
||||
async def block(self, ctx, target: User):
|
||||
"""
|
||||
Block a user from triggering your notifications
|
||||
|
||||
This prevents you from receiving notifications from that user in any guild
|
||||
NOTE: They will still receive notifications from you
|
||||
"""
|
||||
await ctx.message.delete(delay=0) # Delete invoking message if able to
|
||||
|
||||
params = (ctx.author.id, ctx.target.id)
|
||||
if await ctx.bot.db.fetch_exists("SELECT * FROM user_blocks WHERE user_id=? AND target=?", params):
|
||||
await ctx.author.send(f"Target {target} already blocked")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute(
|
||||
"INSERT INTO user_blocks VALUES(?, ?)",
|
||||
(ctx.author.id, target.id),
|
||||
)
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.author.send(f"Blocked {target}")
|
||||
|
||||
@keyword.command()
|
||||
async def unblock(self, ctx, target: User):
|
||||
"""
|
||||
Unblock a user
|
||||
|
||||
This allows you to receive notifications from that user again
|
||||
"""
|
||||
|
||||
await ctx.message.delete(delay=0) # Delete invoking message if able to
|
||||
|
||||
params = (ctx.author.id, ctx.target.id)
|
||||
if await ctx.bot.db.fetch_exists("SELECT * FROM user_blocks WHERE user_id=? AND target=?", params):
|
||||
await ctx.author.send(f"Target {target} already blocked")
|
||||
return
|
||||
|
||||
await ctx.bot.db.execute("DELETE FROM user_ignores WHERE user_id=? AND target=?", params)
|
||||
await ctx.bot.db.commit()
|
||||
await ctx.author.send(f"Unblocked {target}")
|
||||
# TODO: Ignore active channel in handler
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import disnake
|
||||
import re2 as re
|
||||
from disnake.ext.commands import Cog, group, guild_only
|
||||
|
||||
from .notifications import KeywordError
|
||||
from .utils import confirm
|
||||
|
||||
log = logging.getLogger("nomen.settings")
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
DUSTY_DATA_LINE = re.compile(r"^(.+) – notified (\d+) times$")
|
||||
|
||||
|
||||
class InvalidImportFormat(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Settings(Cog):
|
||||
def __init__(self, bot):
|
||||
|
@ -26,7 +15,7 @@ class Settings(Cog):
|
|||
@group(invoke_without_command=True)
|
||||
async def nomen(self, ctx):
|
||||
"""
|
||||
Managing your data in Nomen
|
||||
Settings for Nomen
|
||||
"""
|
||||
|
||||
await ctx.send_help(self.nomen)
|
||||
|
@ -46,64 +35,24 @@ You may want to `{ctx.clean_prefix}nomen export` first"""
|
|||
to_purge = await confirm(ctx, msg)
|
||||
|
||||
if to_purge:
|
||||
# Foreign key constraints will automatically cascade to delete dependent data
|
||||
await self.bot.db.execute("DELETE FROM users WHERE user_id=?", (ctx.author.id,))
|
||||
await self.bot.db.execute(
|
||||
"""
|
||||
DELETE FROM keywords WHERE user_id=?;
|
||||
DELETE FROM users WHERE user_id=?;
|
||||
DELETE FROM user_ignores WHERE user_id=?;
|
||||
""",
|
||||
(ctx.author.id,),
|
||||
)
|
||||
await self.bot.db.commit()
|
||||
await ctx.send("Deleted all user data.")
|
||||
else:
|
||||
await ctx.send("Cancelled.")
|
||||
|
||||
async def import_json(self, ctx, data):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except json.JSONDecodeError as e:
|
||||
raise InvalidImportFormat from e
|
||||
await ctx.send("Importing JSON")
|
||||
|
||||
async def import_dusty(self, ctx, data):
|
||||
lines = map(str.strip, data.split("\n"))
|
||||
to_add = []
|
||||
|
||||
for line in lines:
|
||||
if match := DUSTY_DATA_LINE.fullmatch(line):
|
||||
to_add.append((match.group(1), int(match.group(2))))
|
||||
else:
|
||||
raise InvalidImportFormat
|
||||
|
||||
await ctx.send(f"Importing from Dusty notification list to {ctx.guild}...")
|
||||
|
||||
added = []
|
||||
errors = []
|
||||
|
||||
async with ctx.typing():
|
||||
notifications = self.bot.get_cog("Notifications")
|
||||
for keyword, count in to_add:
|
||||
try:
|
||||
await notifications.add_keyword(ctx.guild.id, ctx.author.id, keyword, False, count)
|
||||
added.append(keyword)
|
||||
except KeywordError as e:
|
||||
errors.append(e)
|
||||
|
||||
if not errors:
|
||||
await ctx.send("Successfully imported all keywords!")
|
||||
else:
|
||||
await ctx.send(
|
||||
f"Import successful, with some errors\n\n"
|
||||
f"Sucessfully imported:\n{"\n".join(f"- {keyword}" for keyword in added)}\n"
|
||||
f"Errors (check PMs):\n{"\n".join(f"- {e.msg}" for e in errors)}\n\n"
|
||||
f"If you want to import erroring keywords, delete conflicting keywords and modify the list to only include the ones that failed to import, and run the command again."
|
||||
)
|
||||
for e in errors:
|
||||
await ctx.author.send(e.dm_msg)
|
||||
|
||||
async def import_raw(self, data):
|
||||
pass
|
||||
|
||||
@nomen.command(name="import")
|
||||
@guild_only()
|
||||
async def _import(self, ctx, *, data):
|
||||
async def _import(self, ctx):
|
||||
"""
|
||||
Imports user data, either from JSON or from a simple list
|
||||
Imports a JSON of all of your user data
|
||||
"""
|
||||
schema = {
|
||||
"disabled": bool,
|
||||
|
@ -120,25 +69,13 @@ You may want to `{ctx.clean_prefix}nomen export` first"""
|
|||
],
|
||||
}
|
||||
|
||||
notifications = self.bot.get_cog("Notifications")
|
||||
|
||||
try:
|
||||
await self.import_json(ctx, data)
|
||||
await ctx.send("Sorry, JSON imports and exports haven't been implemented yet.")
|
||||
return
|
||||
except InvalidImportFormat:
|
||||
try:
|
||||
await self.import_dusty(ctx, data)
|
||||
except InvalidImportFormat:
|
||||
await ctx.send("Invalid import format")
|
||||
|
||||
# @nomen.command()
|
||||
# @guild_only()
|
||||
# async def export(self, ctx):
|
||||
# """
|
||||
# Exports a JSON of all of your user data
|
||||
# """
|
||||
# pass
|
||||
@nomen.command()
|
||||
@guild_only()
|
||||
async def export(self, ctx):
|
||||
"""
|
||||
Exports a JSON of all of your user data
|
||||
"""
|
||||
pass
|
||||
|
||||
@nomen.command(name="opt-out")
|
||||
async def opt_out(self, ctx):
|
||||
|
@ -171,42 +108,3 @@ You may want to `{ctx.clean_prefix}nomen export` first"""
|
|||
|
||||
await self.bot.db.execute("REPLACE INTO users (user_id, disabled) VALUES(?, 0)", (ctx.author.id,))
|
||||
await self.bot.db.commit()
|
||||
|
||||
@group(invoke_without_command=True)
|
||||
async def settings(
|
||||
self,
|
||||
ctx,
|
||||
setting: Literal["use-embed", "notify-self", "bots-notify"],
|
||||
value: bool,
|
||||
):
|
||||
"""
|
||||
Change settings for Nomen
|
||||
|
||||
Valid settings are:
|
||||
- `use-embed`
|
||||
- `notify-self`
|
||||
- `bots-notify`
|
||||
"""
|
||||
setting_db = setting.replace("-", "_")
|
||||
|
||||
await ctx.bot.set_setting(ctx.author.id, setting_db, value)
|
||||
await self.settings_list(ctx)
|
||||
|
||||
@settings.command(name="list")
|
||||
async def settings_list(self, ctx):
|
||||
"""
|
||||
Print all of your current settings
|
||||
"""
|
||||
|
||||
settings = await ctx.bot.db.execute_fetchall(
|
||||
"SELECT use_embed, notify_self, bots_notify FROM users WHERE user_id=?", (ctx.author.id,)
|
||||
)
|
||||
|
||||
settings = ["Yes" if v else "No" for v in settings[0]]
|
||||
|
||||
embed = disnake.Embed(title="Current Settings")
|
||||
embed.add_field("Use Embeds in Notifications (`use-embed`)", settings[0], inline=False)
|
||||
embed.add_field("Notify Self (`notify-self`)", settings[1], inline=False)
|
||||
embed.add_field("Notifications from Bots (`bots-notify`)", settings[2], inline=False)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
|
|
@ -111,6 +111,18 @@ def unpack(lst_of_tpl):
|
|||
return list(map(first, lst_of_tpl))
|
||||
|
||||
|
||||
async def fetch_unpacked(db, sql, params=None):
|
||||
cur = await db.cursor()
|
||||
cur.row_factory = lambda cursor, row: first(row)
|
||||
cur = await cur.execute(sql, params)
|
||||
return await cur.fetchall()
|
||||
|
||||
|
||||
async def fetch_exists(db, sql, params=None):
|
||||
result = await db.execute_fetchall(f"SELECT EXISTS({sql})", params)
|
||||
return result[0][0]
|
||||
|
||||
|
||||
async def in_thread(member, thread):
|
||||
# FIXME: Currently overlooks the situation where a moderator isn't in a thread but has manage threads
|
||||
return any(member.id == thread_member.id for thread_member in await thread.fetch_members())
|
||||
|
|
Loading…
Reference in a new issue