-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Support for snippets (#21) #85
base: main
Are you sure you want to change the base?
Changes from all commits
ef7d015
2c72f23
2a702a7
685687e
878835b
70f2193
aaf697b
0466650
168925f
70a5dd1
1a5802c
ae250ef
d633fbc
32f7f2d
97b5587
2d467bc
8316770
e8f2f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
-- Revision Version: V5 | ||
-- Revises: V4 | ||
-- Creation Date: 2024-03-10 05:51:39.252162 UTC | ||
-- Reason: add table for snippets | ||
|
||
CREATE TABLE IF NOT EXISTS snippets | ||
( | ||
guild_id bigint NOT NULL, | ||
name VARCHAR(100), | ||
content TEXT, | ||
PRIMARY KEY (guild_id, name) | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Optional, Union | ||
|
||
import asyncpg.exceptions | ||
import discord | ||
from discord.ext import commands | ||
from libs.snippets.model import create_snippet, get_snippet | ||
from libs.snippets.views import SnippetPreCreationConfirmationView | ||
|
||
if TYPE_CHECKING: | ||
from libs.utils.context import GuildContext | ||
from rodhaj import Rodhaj | ||
|
||
|
||
class Snippets(commands.Cog): | ||
"""Send or display pre-written text to users""" | ||
|
||
def __init__(self, bot: Rodhaj): | ||
self.bot = bot | ||
self.pool = self.bot.pool | ||
|
||
# Editing Utilities | ||
|
||
async def edit_prompt_user(self, ctx: GuildContext, name: str): | ||
raise NotImplementedError("TODO: Add prompt for editing snippet.") | ||
|
||
@commands.guild_only() | ||
@commands.hybrid_group(name="snippets", alias=["snippet"], fallback="get") | ||
async def snippet(self, ctx: GuildContext, *, name: str): | ||
"""Allows for use snippets of text for later retrieval or for quicker responses | ||
|
||
If an subcommand is not called, then this will search | ||
the database for the requested snippet | ||
""" | ||
await ctx.send("Implement getting snippets here") | ||
|
||
@commands.guild_only() | ||
@snippet.command() | ||
async def remove(self, ctx: GuildContext, name: str): | ||
query = """ | ||
DELETE FROM snippets | ||
WHERE name = $2 | ||
RETURNING id | ||
""" | ||
result = await self.pool.fetchrow(query, ctx.guild.id, name) | ||
if result is None: | ||
await ctx.reply( | ||
embed=discord.Embed( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would strongly recommend replacing these embeds with just an message. It isn't necessary to do this as most users will find an jarring differences compared to the rest of the codebase |
||
title="Deletion failed", | ||
colour=discord.Colour.red(), | ||
description=f"Snippet `{name}` was not found and " | ||
+ "hence was not deleted.", | ||
), | ||
ephemeral=True, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
) | ||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For future reference, there isn't an need for an |
||
await ctx.reply( | ||
embed=discord.Embed( | ||
title="Deletion successful", | ||
colour=discord.Colour.green(), | ||
description=f"Snippet `{name}` was deleted successfully", | ||
), | ||
ephemeral=True, | ||
) | ||
|
||
# TODO: Run all str inputs through custom converters | ||
@commands.guild_only() | ||
@snippet.command() | ||
async def new( | ||
self, | ||
ctx: GuildContext, | ||
name: str, | ||
*, | ||
content: Optional[str] = None, | ||
): | ||
if ( | ||
await get_snippet(self.pool, ctx.guild.id, ctx.message.author.id, name) | ||
is not None | ||
): | ||
await ctx.send( | ||
content=f"Snippet `{name}` already exists!", | ||
) | ||
return | ||
|
||
if not content: | ||
timeout = 15 | ||
confirmation_view = SnippetPreCreationConfirmationView( | ||
self.bot, ctx, name, timeout | ||
) | ||
await ctx.reply( | ||
content=f"Create snippet with id `{name}`?", | ||
view=confirmation_view, | ||
delete_after=timeout, | ||
) | ||
else: | ||
self.bot.dispatch( | ||
"snippet_create", | ||
ctx.guild, | ||
ctx.message.author, | ||
name, | ||
content, | ||
ctx, | ||
) | ||
|
||
@commands.guild_only() | ||
@snippet.command(name="list") | ||
async def snippets_list( | ||
self, ctx: GuildContext, json: Optional[bool] = False | ||
) -> None: | ||
await ctx.send("list snippets") | ||
|
||
@commands.guild_only() | ||
@snippet.command() | ||
async def show(self, ctx: GuildContext, name: str): | ||
query = """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The rest of the code should follow the suggestions that are noted above |
||
SELECT content FROM snippets | ||
WHERE name = $1 | ||
""" | ||
data = await self.pool.fetchrow(query, name) | ||
if data is None: | ||
ret_embed = discord.Embed( | ||
title="Oops...", | ||
colour=discord.Colour.red(), | ||
description=f"The snippet `{name}` was not found. " | ||
+ "To create a new snippet with this name, " | ||
+ f"please run `snippet create {name} <content>`", | ||
) | ||
await ctx.reply(embed=ret_embed, ephemeral=True) | ||
else: | ||
ret_data = discord.Embed( | ||
title=f"Snippet information for `{name}`", | ||
colour=discord.Colour.green(), | ||
description=data[0], | ||
) | ||
await ctx.reply(embed=ret_data, ephemeral=True) | ||
|
||
@commands.guild_only() | ||
@snippet.command() | ||
async def edit(self, ctx: GuildContext, name: str, content: Optional[str]): | ||
if content is None: | ||
await self.edit_prompt_user(ctx, name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you have an function and it's only used once, then it would be better to contain the logic within the command function body instead. |
||
return | ||
query = """ | ||
UPDATE snippets | ||
SET content = $2 | ||
WHERE name = $1 | ||
RETURNING name | ||
""" | ||
result = await self.pool.fetchrow(query, name, content) | ||
if result is None: | ||
await ctx.reply( | ||
embed=discord.Embed( | ||
title="Oops...", | ||
colour=discord.Colour.red(), | ||
description=f"Cannot edit snippet `{name}` as there is no such " | ||
+ "snippet. To create a new snippet with the corresponding " | ||
+ f"name, please run `snippet new {name} <snippet text>`.", | ||
), | ||
ephemeral=True, | ||
) | ||
else: | ||
await ctx.reply( | ||
embed=discord.Embed( | ||
title="Snippet changed", | ||
colour=discord.Colour.green(), | ||
description=f"The contents of snippet {result[0]} has been " | ||
+ f"changed to \n\n{content}", | ||
), | ||
ephemeral=True, | ||
) | ||
|
||
@commands.Cog.listener() | ||
async def on_snippet_create( | ||
self, | ||
guild: discord.Guild, | ||
creator: Union[discord.User, discord.Member], | ||
snippet_name: str, | ||
snippet_text: str, | ||
response_context: GuildContext, | ||
): | ||
try: | ||
await create_snippet( | ||
self.pool, guild.id, creator.id, snippet_name, snippet_text | ||
) | ||
if response_context: | ||
await response_context.send( | ||
"Snippet created successfully", delete_after=5 | ||
) | ||
except asyncpg.exceptions.UniqueViolationError: | ||
if response_context: | ||
await response_context.send("Snippet already exists", delete_after=5) | ||
|
||
|
||
async def setup(bot: Rodhaj): | ||
await bot.add_cog(Snippets(bot)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from collections import namedtuple | ||
|
||
import asyncpg.pool | ||
|
||
SnippetHeader = namedtuple( | ||
"SnippetHeader", | ||
["id", "name", "content", "uses", "owner_id", "location_id", "created_at"], | ||
) | ||
|
||
|
||
async def get_snippet( | ||
pool: asyncpg.pool.Pool, guild_id: int, owner_id: int, snippet_name: str | ||
): | ||
fields_str = ",".join(SnippetHeader._fields) | ||
query = f""" | ||
SELECT {fields_str} from snippets | ||
WHERE location_id = $1 AND owner_id = $2 AND name = $3 | ||
""" | ||
row = await pool.fetchrow(query, guild_id, owner_id, snippet_name) | ||
if not row: | ||
return None | ||
return SnippetHeader(*row) | ||
|
||
|
||
async def create_snippet( | ||
pool: asyncpg.pool.Pool, | ||
guild_id: int, | ||
owner_id: int, | ||
snippet_name: str, | ||
snippet_text: str, | ||
): | ||
query = """ | ||
INSERT INTO snippets (owner_id, location_id, name, content) | ||
VALUES ($1, $2, $3, $4) | ||
""" | ||
await pool.execute(query, guild_id, owner_id, snippet_name, snippet_text) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import discord.ui | ||
from libs.utils import GuildContext, RoboView | ||
from rodhaj import Rodhaj | ||
|
||
|
||
class SnippetCreationModal(discord.ui.Modal, title="Editing Snippet"): | ||
content = discord.ui.TextInput( | ||
label="Snippet message", | ||
placeholder="Call me Ishmael. Some years ago—never mind " | ||
+ "how long precisely...", | ||
style=discord.TextStyle.paragraph, | ||
) | ||
|
||
def __init__(self, bot: Rodhaj, context: GuildContext, name: str): | ||
super().__init__(timeout=12 * 3600) | ||
self._bot = bot | ||
self._ctx = context | ||
self._snippet_name = name | ||
self.title = f"Creating Snippet {name}" | ||
|
||
async def on_submit(self, interaction: discord.Interaction): | ||
await interaction.response.defer() | ||
self._bot.dispatch( | ||
"snippet_create", | ||
self._ctx.guild, | ||
self._ctx.author, | ||
self._snippet_name, | ||
self.content.value, | ||
self._ctx, | ||
) | ||
self.stop() | ||
|
||
|
||
class SnippetPreCreationConfirmationView(discord.ui.View): | ||
def __init__(self, bot: Rodhaj, ctx: GuildContext, snippet_name: str, timeout=15): | ||
super().__init__(timeout=timeout) | ||
self._bot = bot | ||
self._ctx = ctx | ||
self._snippet_name = snippet_name | ||
|
||
@discord.ui.button(label="Create Snippet", style=discord.ButtonStyle.green) | ||
async def create_snippet( | ||
self, interaction: discord.Interaction, button: discord.ui.Button | ||
): | ||
if interaction.user.id != self._ctx.author.id: | ||
return | ||
button.disabled = True | ||
modal = SnippetCreationModal(self._bot, self._ctx, self._snippet_name) | ||
await interaction.response.send_modal(modal) | ||
await interaction.edit_original_response( | ||
content="Creating Snippet...", view=None | ||
) | ||
await modal.wait() | ||
await interaction.delete_original_response() | ||
self.stop() | ||
|
||
async def on_timeout(self): | ||
self.clear_items() | ||
self.stop() | ||
|
||
|
||
class SnippetInfoView(RoboView): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
-- Revision Version: V7 | ||
-- Revises: V6 | ||
-- Creation Date: 2024-06-25 22:08:13.554817 UTC | ||
-- Reason: snippets | ||
|
||
CREATE TABLE IF NOT EXISTS snippets ( | ||
id SERIAL PRIMARY KEY, | ||
name TEXT, | ||
content TEXT, | ||
uses INTEGER DEFAULT (0), | ||
owner_id BIGINT, | ||
location_id BIGINT, | ||
created_at TIMESTAMPTZ DEFAULT (now() at time zone 'utc') | ||
); | ||
|
||
-- Create indices to speed up regular and trigram searches | ||
CREATE INDEX IF NOT EXISTS snippets_name_idx ON snippets (name); | ||
CREATE INDEX IF NOT EXISTS snippets_location_id_idx ON snippets (location_id); | ||
CREATE INDEX IF NOT EXISTS snippets_name_trgm_idx ON snippets USING GIN (name gin_trgm_ops); | ||
CREATE INDEX IF NOT EXISTS snippets_name_lower_idx ON snippets (LOWER(name)); | ||
CREATE UNIQUE INDEX IF NOT EXISTS snippets_uniq_idx ON snippets (LOWER(name), location_id); | ||
|
||
CREATE TABLE IF NOT EXISTS snippets_lookup ( | ||
id SERIAL PRIMARY KEY, | ||
name TEXT, | ||
location_id BIGINT, | ||
owner_id BIGINT, | ||
created_at TIMESTAMPTZ DEFAULT (now() at time zone 'utc'), | ||
snippets_id INTEGER REFERENCES snippets (id) ON DELETE CASCADE ON UPDATE NO ACTION | ||
); | ||
|
||
CREATE INDEX IF NOT EXISTS snippets_lookup_name_idx ON snippets_lookup (name); | ||
CREATE INDEX IF NOT EXISTS snippets_lookup_location_id_idx ON snippets_lookup (location_id); | ||
CREATE INDEX IF NOT EXISTS snippets_lookup_name_trgm_idx ON snippets_lookup USING GIN (name gin_trgm_ops); | ||
CREATE INDEX IF NOT EXISTS snippets_lookup_name_lower_idx ON snippets_lookup (LOWER(name)); | ||
CREATE UNIQUE INDEX IF NOT EXISTS snippets_lookup_uniq_idx ON snippets_lookup (LOWER(name), location_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would strongly recommend not using
commands.Context.reply
in this case. It sends an unnecessary ping to the user, and is generally considered extremely annoying and bad practice. Instead, replace it withcommands.Context.send()