2 Commits

Author SHA1 Message Date
jude
192e316926 Restructure guilds table 2025-06-25 20:08:16 +01:00
jude
19cfacffe5 Restructure database tables 2025-05-08 20:55:17 +01:00
30 changed files with 214 additions and 13007 deletions

14
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
version = 3
[[package]]
name = "addr2line"
@@ -524,15 +524,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "cron-parser"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa5650eabdaa360e2c240c2a5f544f10185b439cd76d748e44e3f28128a016b"
dependencies = [
"chrono",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
@@ -2623,12 +2614,11 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reminder-rs"
version = "1.7.40"
version = "1.7.37"
dependencies = [
"base64 0.22.1",
"chrono",
"chrono-tz",
"cron-parser",
"csv",
"dotenv",
"env_logger",

View File

@@ -1,6 +1,6 @@
[package]
name = "reminder-rs"
version = "1.7.40"
version = "1.7.37"
authors = ["Jude Southworth <judesouthworth@pm.me>"]
edition = "2021"
license = "AGPL-3.0 only"
@@ -35,7 +35,6 @@ serenity = { version = "0.12", default-features = false, features = ["builder",
oauth2 = "4"
csv = "1.2"
sd-notify = "0.4.1"
cron-parser = "0.10"
[dependencies.extract_derive]
path = "extract_derive"

12676
gb-ipv4.csv

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
-- Drop all old tables
DROP TABLE IF EXISTS users_old;
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS embeds;
DROP TABLE IF EXISTS embed_fields;
DROP TABLE IF EXISTS command_aliases;
DROP TABLE IF EXISTS macro;
DROP TABLE IF EXISTS command_restrictions;
DROP TABLE IF EXISTS roles;
SET FOREIGN_KEY_CHECKS = 0;
-- Drop columns from channels that are no longer used
ALTER TABLE channels DROP COLUMN `name`;
ALTER TABLE channels DROP COLUMN `blacklisted`;
-- Drop columns from guilds table that are no longer used and rebuild table
CREATE TABLE guilds_new (
id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
ephemeral_confirmations BOOLEAN NOT NULL DEFAULT 0
);
INSERT INTO guilds_new (id, ephemeral_confirmations) SELECT guild, ephemeral_confirmations FROM guilds;
RENAME TABLE guilds TO guilds_old;
RENAME TABLE guilds_new TO guilds;
-- Update fk on channels to point at new guild table
ALTER TABLE channels
DROP FOREIGN KEY `channels_ibfk_1`,
MODIFY COLUMN guild_id BIGINT UNSIGNED,
ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
REFERENCES `guilds` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
UPDATE channels SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
-- Rebuild todos table
CREATE TABLE `todos_new` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`guild_id` BIGINT UNSIGNED DEFAULT NULL,
`channel_id` INT UNSIGNED DEFAULT NULL,
`user_id` BIGINT UNSIGNED DEFAULT NULL,
`value` VARCHAR(2000) NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY `fk_channel_id` (`channel_id`)
REFERENCES `channels` (`id`)
ON DELETE SET NULL
ON UPDATE CASCADE,
FOREIGN KEY `fk_guild_id` (`guild_id`)
REFERENCES `guilds` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY `fk_user_id` (`user_id`)
REFERENCES `users` (`id`)
ON DELETE SET NULL
ON UPDATE CASCADE
);
INSERT INTO todos_new (id, guild_id, channel_id, user_id, value) SELECT id, (SELECT guild FROM guilds_old WHERE id = guild_id), channel_id, user_id, value FROM todos;
RENAME TABLE todos TO todos_old;
RENAME TABLE todos_new TO todos;
-- Update fk on reminder_template to point at new guild table
ALTER TABLE reminder_template
DROP FOREIGN KEY `reminder_template_ibfk_1`,
MODIFY COLUMN guild_id BIGINT UNSIGNED NOT NULL,
ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
REFERENCES `guilds` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
UPDATE reminder_template SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
-- Update fk on command_macro to point at new guild table
ALTER TABLE command_macro
DROP FOREIGN KEY `command_macro_ibfk_1`,
MODIFY COLUMN guild_id BIGINT UNSIGNED NOT NULL,
ADD FOREIGN KEY `fk_guild_id` (`guild_id`)
REFERENCES `guilds` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
UPDATE command_macro SET guild_id = (SELECT guild FROM guilds_old WHERE id = guild_id);
DROP TABLE todos_old;
DROP TABLE guilds_old;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -3,7 +3,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
use chrono_tz::TZ_VARIANTS;
use poise::serenity_prelude::AutocompleteChoice;
use crate::time_parser::cron_next_timestamp;
use crate::{models::CtxData, time_parser::natural_parser, Context};
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
@@ -25,7 +24,7 @@ pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Str
SELECT name
FROM command_macro
WHERE
guild_id = (SELECT id FROM guilds WHERE guild = ?)
guild_id = ?
AND name LIKE CONCAT(?, '%')
",
ctx.guild_id().unwrap().get(),
@@ -43,13 +42,7 @@ pub async fn time_hint_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Auto
if partial.is_empty() {
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
} else {
let timezone = ctx.timezone().await;
let timestamp = match cron_next_timestamp(partial, timezone) {
Some(ts) => Some(ts),
None => natural_parser(partial, &timezone.to_string()).await,
};
match timestamp {
match natural_parser(partial, &ctx.timezone().await.to_string()).await {
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(now) => {
let diff = timestamp - now.as_secs() as i64;

View File

@@ -17,12 +17,10 @@ pub async fn delete_macro(
) -> Result<(), Error> {
match sqlx::query!(
"
SELECT m.id
FROM command_macro m
INNER JOIN guilds
ON guilds.id = m.guild_id
WHERE guild = ?
AND m.name = ?
SELECT id
FROM command_macro
WHERE guild_id = ?
AND name = ?
",
ctx.guild_id().unwrap().get(),
name

View File

@@ -32,7 +32,7 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
let json = serde_json::to_string(&command_macro.commands).unwrap();
sqlx::query!(
"INSERT INTO command_macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
"INSERT INTO command_macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
command_macro.guild_id.get(),
command_macro.name,
command_macro.description,

View File

@@ -35,7 +35,7 @@ pub async fn record_macro(
"
SELECT 1 as _e
FROM command_macro
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
WHERE guild_id = ?
AND name = ?
",
guild_id.get(),
@@ -75,8 +75,8 @@ Please select a unique name for your macro.",
CreateEmbed::new()
.title("Macro Recording Started")
.description(
"Run up to 5 commands to record in this macro. Use `/macro finish` to stop recording at any point.
Any commands performed during recording won't take any actual action- they are only captured for the macro.",
"Run up to 5 commands, or type `/macro finish` to stop at any point.
Any commands ran as part of recording will be inconsequential",
)
.color(*THEME_COLOR),
),

View File

@@ -21,7 +21,7 @@ impl Recordable for Options {
"
INSERT INTO todos (guild_id, channel_id, value)
VALUES (
(SELECT id FROM guilds WHERE guild = ?),
?,
(SELECT id FROM channels WHERE channel = ?),
?
)

View File

@@ -18,7 +18,7 @@ impl Recordable for Options {
"
INSERT INTO todos (guild_id, value)
VALUES (
(SELECT id FROM guilds WHERE guild = ?), ?
?, ?
)
",
ctx.guild_id().unwrap().get(),

View File

@@ -13,9 +13,8 @@ impl Recordable for Options {
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
let values = sqlx::query!(
"
SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?
SELECT id, value FROM todos
WHERE guild_id = ?
",
ctx.guild_id().unwrap().get(),
)

View File

@@ -219,9 +219,8 @@ impl ComponentDataModel {
} else {
sqlx::query!(
"
SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?
SELECT id, value FROM todos
WHERE guild_id = ?
",
pager.guild_id,
)
@@ -311,10 +310,9 @@ impl ComponentDataModel {
} else {
sqlx::query!(
"
SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?
",
SELECT id, value FROM todos
WHERE guild_id = ?
",
selector.guild_id,
)
.fetch_all(&data.database)

View File

@@ -23,7 +23,7 @@ pub async fn listener(
if is_new.unwrap_or(false) {
let guild_id = guild.id.get().to_owned();
sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id)
sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id)
.execute(&data.database)
.await?;
@@ -56,7 +56,7 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
}
FullEvent::GuildDelete { incomplete, .. } => {
if !incomplete.unavailable {
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.get())
let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.get())
.execute(&data.database)
.await;
}

View File

@@ -1,6 +1,4 @@
use crate::consts::THEME_COLOR;
use poise::{serenity_prelude::CreateEmbed, CommandInteractionType, CreateReply};
use serenity::builder::CreateEmbedFooter;
use poise::{CommandInteractionType, CreateReply};
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
@@ -20,18 +18,7 @@ async fn macro_check(ctx: Context<'_>) -> bool {
.send(
CreateReply::default()
.ephemeral(true)
.embed(CreateEmbed::new()
.title("💾 Currently recording macro")
.description(
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
)
.footer(
CreateEmbedFooter::new(
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
)
)
.color(*THEME_COLOR),
),
.content(format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS))
)
.await;
} else {
@@ -41,19 +28,9 @@ async fn macro_check(ctx: Context<'_>) -> bool {
let _ = ctx
.send(
CreateReply::default().ephemeral(true).embed(
CreateEmbed::new()
.title("💾 Currently recording macro")
.description(
"Command recorded. Use `/macro finish` to end recording.",
)
.footer(
CreateEmbedFooter::new(
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
)
)
.color(*THEME_COLOR),
),
CreateReply::default()
.ephemeral(true)
.content("Command recorded to macro"),
)
.await;
}
@@ -61,18 +38,8 @@ async fn macro_check(ctx: Context<'_>) -> bool {
None => {
let _ = ctx
.send(
CreateReply::default().ephemeral(true).embed(
CreateEmbed::new()
.title("💾 Currently recording macro")
.description(
"This command is not supported in macros, so it hasn't been recorded. Use `/macro finish` to end recording.",
)
.footer(
CreateEmbedFooter::new(
"Any commands performed during recording won't take any actual action- they are only captured for the macro"
)
)
.color(*THEME_COLOR),
CreateReply::default().ephemeral(true).content(
"This command is not supported in macros yet.",
),
)
.await;
@@ -107,7 +74,6 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
return if permissions.send_messages()
&& permissions.embed_links()
&& manage_webhooks
&& permissions.view_channel()
{
true
} else {
@@ -115,13 +81,12 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
.send(CreateReply::default().content(format!(
"The bot appears to be missing some permissions:
{} **View Channels**
{} **Send Message**
{} **Embed Links**
{} **Manage Webhooks**
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot \"Administrator\" will bypass permission checks",
if permissions.view_channel() { "" } else { "" },
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot
\"Administrator\" will bypass permission checks",
if permissions.send_messages() { "" } else { "" },
if permissions.embed_links() { "" } else { "" },
if manage_webhooks { "" } else { "" },
@@ -135,7 +100,9 @@ Please check the bot's roles, and any channel overrides. Alternatively, giving t
manage_webhooks
}
None => true,
None => {
return true;
}
}
}

View File

@@ -8,9 +8,7 @@ use crate::{consts::DEFAULT_AVATAR, Error};
pub struct ChannelData {
pub id: u32,
pub channel: u64,
pub name: Option<String>,
pub nudge: i16,
pub blacklisted: bool,
pub webhook_id: Option<u64>,
pub webhook_token: Option<String>,
pub paused: bool,
@@ -27,7 +25,7 @@ impl ChannelData {
if let Ok(c) = sqlx::query_as_unchecked!(
Self,
"
SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,
SELECT id, channel, nudge, webhook_id, webhook_token, paused,
paused_until
FROM channels
WHERE channel = ?
@@ -39,15 +37,11 @@ impl ChannelData {
{
Ok(c)
} else {
let props = channel.to_owned().guild().map(|g| (g.guild_id.get().to_owned(), g.name));
let (guild_id, channel_name) =
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
let guild_id = channel.to_owned().guild().map(|g| g.guild_id.get().to_owned());
sqlx::query!(
"INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
"INSERT IGNORE INTO channels (channel, guild_id) VALUES (?, ?)",
channel_id,
channel_name,
guild_id
)
.execute(&pool.clone())
@@ -56,7 +50,7 @@ impl ChannelData {
Ok(sqlx::query_as_unchecked!(
Self,
"
SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until
SELECT id, channel, nudge, webhook_id, webhook_token, paused, paused_until
FROM channels
WHERE channel = ?
",
@@ -72,18 +66,14 @@ impl ChannelData {
"
UPDATE channels
SET
name = ?,
nudge = ?,
blacklisted = ?,
webhook_id = ?,
webhook_token = ?,
paused = ?,
paused_until = ?
WHERE id = ?
",
self.name,
self.nudge,
self.blacklisted,
self.webhook_id,
self.webhook_token,
self.paused,

View File

@@ -144,12 +144,10 @@ pub async fn guild_command_macro(ctx: &Context<'_>, name: &str) -> Option<Comman
let row = sqlx::query_as!(
Row,
"
SELECT m.name, m.description, m.commands
SELECT name, description, commands
FROM command_macro m
INNER JOIN guilds g
ON g.id = m.guild_id
WHERE guild = ?
AND m.name = ?
WHERE guild_id = ?
AND name = ?
",
ctx.guild_id().unwrap().get(),
name

View File

@@ -3,7 +3,7 @@ use sqlx::MySqlPool;
pub struct GuildData {
pub ephemeral_confirmations: bool,
pub id: u32,
pub guild_id: u64,
}
impl GuildData {
@@ -13,7 +13,7 @@ impl GuildData {
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
if let Ok(c) = sqlx::query_as_unchecked!(
Self,
"SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
"SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
guild_id.get()
)
.fetch_one(pool)
@@ -21,13 +21,13 @@ impl GuildData {
{
Ok(c)
} else {
sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.get())
sqlx::query!("INSERT IGNORE INTO guilds (id) VALUES (?)", guild_id.get())
.execute(&pool.clone())
.await?;
Ok(sqlx::query_as_unchecked!(
Self,
"SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
"SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
guild_id.get()
)
.fetch_one(pool)
@@ -39,7 +39,7 @@ impl GuildData {
sqlx::query!(
"UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
self.ephemeral_confirmations,
self.id
self.guild_id
)
.execute(pool)
.await

View File

@@ -68,16 +68,19 @@ impl Data {
guild_id: GuildId,
) -> Result<Vec<CommandMacro>, Error> {
let rows = sqlx::query!(
"SELECT name, description, commands FROM command_macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
"SELECT name, description, commands FROM command_macro WHERE guild_id = ?",
guild_id.get()
)
.fetch_all(&self.database)
.await?.iter().map(|row| CommandMacro {
.await?
.iter()
.map(|row| CommandMacro {
guild_id,
name: row.name.clone(),
description: row.description.clone(),
commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
}).collect();
})
.collect();
Ok(rows)
}

View File

@@ -16,7 +16,7 @@ use crate::{
},
CtxData,
},
time_parser::{cron_next_timestamp, natural_parser},
time_parser::natural_parser,
utils::{check_guild_subscription, check_subscription},
Context, Database, Error,
};
@@ -262,7 +262,7 @@ impl Reminder {
channels.id = reminders.channel_id
WHERE
`status` = 'pending' AND
channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
channels.guild_id = ?
",
guild_id.get()
)
@@ -486,10 +486,7 @@ pub async fn create_reminder(
let user_data = ctx.author_data().await.unwrap();
let timezone = timezone.unwrap_or(ctx.timezone().await);
let time = match cron_next_timestamp(&time, timezone) {
Some(ts) => Some(ts),
None => natural_parser(&time, &timezone.to_string()).await,
};
let time = natural_parser(&time, &timezone.to_string()).await;
match time {
Some(time) => {

View File

@@ -34,10 +34,8 @@ use crate::{
lazy_static! {
pub static ref TIMEFROM_REGEX: Regex =
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
pub static ref TIMENOW_REGEX: Regex = Regex::new(
r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
)
.unwrap();
pub static ref TIMENOW_REGEX: Regex =
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
}
@@ -66,7 +64,7 @@ fn fmt_displacement(format: &str, seconds: u64) -> String {
}
pub fn substitute(string: &str) -> String {
let new = TIMEFROM_REGEX.replace_all(string, |caps: &Captures| {
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
let format = caps.name("format").map(|m| m.as_str());
@@ -94,26 +92,12 @@ pub fn substitute(string: &str) -> String {
});
TIMENOW_REGEX
.replace_all(&new, |caps: &Captures| {
.replace(&new, |caps: &Captures| {
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
let format = caps.name("format").map(|m| m.as_str());
let sign = caps.name("sign").map(|m| m.as_str());
let offset = caps.name("offset").map(|m| m.as_str().parse::<i64>().ok()).flatten();
if let (Some(timezone), Some(format)) = (timezone, format) {
let mut now = Utc::now().with_timezone(&timezone);
if let (Some(sign), Some(offset)) = (sign, offset) {
now = now
.checked_add_signed(TimeDelta::seconds(
offset * {
match sign {
"-" => -1,
_ => 1,
}
},
))
.unwrap_or(now)
}
let now = Utc::now().with_timezone(&timezone);
now.format(format).to_string()
} else {

View File

@@ -6,8 +6,6 @@ use std::{
use chrono::{DateTime, Datelike, Timelike, Utc};
use chrono_tz::Tz;
use cron_parser::parse;
use std::str::FromStr;
use tokio::process::Command;
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
@@ -221,7 +219,3 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
})
.and_then(|inner| if inner < 0 { None } else { Some(inner) })
}
pub fn cron_next_timestamp(expr: &str, timezone: Tz) -> Option<i64> {
parse(expr, &Utc::now().with_timezone(&timezone)).ok().map(|next| next.timestamp() as i64)
}

View File

@@ -92,8 +92,6 @@ enum Error {
SQLx(sqlx::Error),
#[allow(unused)]
Serenity(serenity::Error),
#[allow(unused)]
MissingDiscordPermission(&'static str),
}
pub async fn initialize(

View File

@@ -305,15 +305,7 @@ pub async fn edit_reminder(
Err(e) => {
warn!("`create_database_channel` returned an error code: {:?}", e);
// Provide more specific error messages based on the error type
match e {
crate::web::Error::MissingDiscordPermission(permission) => {
error.push(format!("Please ensure the bot has the \"{}\" permission in the channel", permission));
}
_ => {
error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
}
}
error.push("Failed to configure channel for reminders. Please check the bot permissions".to_string());
}
}
}

View File

@@ -31,7 +31,7 @@ pub async fn get_reminder_templates(
match sqlx::query_as_unchecked!(
ReminderTemplate,
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
"SELECT * FROM reminder_template WHERE guild_id = ?",
id
)
.fetch_all(pool.inner())
@@ -57,7 +57,6 @@ pub async fn create_guild_reminder_template(
check_authorization(cookies, ctx.inner(), id).await?;
match create_reminder_template(
ctx.inner(),
&mut transaction,
GuildId::new(id),
reminder_template.into_inner(),
@@ -87,15 +86,14 @@ pub async fn delete_reminder_template(
check_authorization(cookies, ctx.inner(), id).await?;
match sqlx::query!(
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
id, delete_reminder_template.id
"DELETE FROM reminder_template WHERE guild_id = ? AND id = ?",
id,
delete_reminder_template.id
)
.fetch_all(pool.inner())
.await
{
Ok(_) => {
Ok(json!({}))
}
Ok(_) => Ok(json!({})),
Err(e) => {
warn!("Could not delete template from {}: {:?}", id, e);

View File

@@ -66,7 +66,7 @@ pub async fn create_todo(
"
INSERT INTO todos (guild_id, channel_id, value)
VALUES (
(SELECT id FROM guilds WHERE guild = ?),
?,
(SELECT id FROM channels WHERE channel = ?),
?
)
@@ -88,7 +88,7 @@ pub async fn create_todo(
"
INSERT INTO todos (guild_id, channel_id, value)
VALUES (
(SELECT id FROM guilds WHERE guild = ?),
?,
NULL,
?
)
@@ -130,11 +130,9 @@ pub async fn get_todo(
channels.channel AS channel_id,
value
FROM todos
INNER JOIN guilds
ON guilds.id = todos.guild_id
LEFT JOIN channels
ON channels.id = todos.channel_id
WHERE guilds.guild = ?
WHERE todos.guild_id = ?
",
id
)
@@ -167,7 +165,7 @@ pub async fn update_todo(
"
UPDATE todos
SET value = ?
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
WHERE guild_id = ?
AND id = ?
",
todo.value,
@@ -202,7 +200,7 @@ pub async fn delete_todo(
sqlx::query!(
"
DELETE FROM todos
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
WHERE guild_id = ?
AND id = ?
",
guild_id,

View File

@@ -65,16 +65,7 @@ pub async fn create_reminder(
if let Err(e) = channel {
warn!("`create_database_channel` returned an error code: {:?}", e);
// Provide more specific error messages based on the error type
let error_msg = match e {
Error::MissingDiscordPermission(permission) => format!(
"Please ensure the bot has the \"{}\" permission in the channel",
permission
),
_ => "Failed to configure channel for reminders.".to_string(),
};
return Err(json!({"error": error_msg}));
return Err(json!({"error": "Failed to configure channel for reminders."}));
}
let channel = channel.unwrap();

View File

@@ -39,28 +39,31 @@ pub async fn export(
match sqlx::query_as_unchecked!(
ReminderTemplateCsv,
"SELECT
name,
attachment,
attachment_name,
avatar,
content,
embed_author,
embed_author_url,
embed_color,
embed_description,
embed_footer,
embed_footer_url,
embed_image_url,
embed_thumbnail_url,
embed_title,
embed_fields,
interval_seconds,
interval_days,
interval_months,
tts,
username
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
"
SELECT
name,
attachment,
attachment_name,
avatar,
content,
embed_author,
embed_author_url,
embed_color,
embed_description,
embed_footer,
embed_footer_url,
embed_image_url,
embed_thumbnail_url,
embed_title,
embed_fields,
interval_seconds,
interval_days,
interval_months,
tts,
username
FROM reminder_template
WHERE guild_id = ?
",
id
)
.fetch_all(pool.inner())
@@ -144,7 +147,6 @@ pub async fn import(
};
create_reminder_template(
ctx.inner(),
&mut transaction,
GuildId::new(id),
reminder_template,

View File

@@ -38,10 +38,11 @@ pub async fn export(
match sqlx::query_as_unchecked!(
TodoCsv,
"SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
"
SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
LEFT JOIN channels ON todos.channel_id = channels.id
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
INNER JOIN guilds ON todos.guild_id = ?
",
id
)
.fetch_all(pool.inner())
@@ -96,7 +97,7 @@ pub async fn import(
Ok(body) => {
let mut reader = csv::Reader::from_reader(body.as_slice());
let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), ?)";
let mut query_params = vec![];
for result in reader.deserialize::<TodoCsv>() {

View File

@@ -10,7 +10,6 @@ use rocket::{
use rocket_dyn_templates::Template;
use secrecy::ExposeSecret;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serenity::http::HttpError;
use serenity::{
all::CacheHttp,
builder::CreateWebhook,
@@ -374,12 +373,12 @@ pub(crate) async fn create_reminder(
reminder: CreateReminder,
) -> JsonResult {
// check guild in db
match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.get())
match sqlx::query!("SELECT 1 as A FROM guilds WHERE id = ?", guild_id.get())
.fetch_one(transaction.executor())
.await
{
Err(sqlx::Error::RowNotFound) => {
if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.get())
if sqlx::query!("INSERT INTO guilds (id) VALUES (?)", guild_id.get())
.execute(transaction.executor())
.await
.is_err()
@@ -405,19 +404,9 @@ pub(crate) async fn create_reminder(
if let Err(e) = channel {
warn!("`create_database_channel` returned an error code: {:?}", e);
// Provide more specific error messages based on the error type
let error_msg = match e {
Error::MissingDiscordPermission(permission) => {
format!(
"Please ensure the bot has the \"{}\" permission in the channel",
permission
)
}
_ => "Failed to configure channel for reminders. Please check the bot permissions"
.to_string(),
};
return Err(json!({"error": error_msg}));
return Err(
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
);
}
let channel = channel.unwrap();
@@ -605,7 +594,6 @@ pub(crate) async fn create_reminder(
}
pub(crate) async fn create_reminder_template(
ctx: &Context,
transaction: &mut Transaction<'_>,
guild_id: GuildId,
reminder_template: ReminderTemplate,
@@ -670,7 +658,7 @@ pub(crate) async fn create_reminder_template(
interval_months,
tts,
username
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?)",
guild_id.get(),
name,
@@ -727,36 +715,13 @@ async fn create_database_channel(
match row {
Ok(row) => {
let is_dm = channel
.to_channel(&ctx)
.await
.map_err(|e| {
if let serenity::Error::Http(http_error) = &e {
if let HttpError::UnsuccessfulRequest(response) = http_error {
if response.error.code == 50001 {
return Error::MissingDiscordPermission("View Channel");
}
}
}
Error::Serenity(e)
})?
.private()
.is_some();
let is_dm =
channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
let webhook = channel
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
.await
.map_err(|e| match &e {
serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
match response.error.code {
50001 => Error::MissingDiscordPermission("View Channel"),
50013 => Error::MissingDiscordPermission("Manage Webhooks"),
_ => Error::Serenity(e),
}
}
_ => Error::Serenity(e),
})?;
.map_err(|e| Error::Serenity(e))?;
let token = webhook.token.unwrap();
@@ -781,16 +746,7 @@ async fn create_database_channel(
let webhook = channel
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
.await
.map_err(|e| match &e {
serenity::Error::Http(HttpError::UnsuccessfulRequest(response)) => {
match response.error.code {
50001 => Error::MissingDiscordPermission("View Channel"),
50013 => Error::MissingDiscordPermission("Manage Webhooks"),
_ => Error::Serenity(e),
}
}
_ => Error::Serenity(e),
})?;
.map_err(|e| Error::Serenity(e))?;
let token = webhook.token.unwrap();
@@ -849,15 +805,22 @@ pub async fn todos_redirect(id: &str) -> Redirect {
#[get("/")]
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
render_dashboard(cookies).await
}
#[get("/<_..>")]
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
render_dashboard(cookies).await
}
async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
if cookies.get_private("userid").is_some() {
match NamedFile::open(Path::new(path!("static/index.html"))).await {
Ok(f) => DashboardPage::Ok(f),
Err(e) => {
warn!("Couldn't render dashboard: {:?}", e);
DashboardPage::NotConfigured(internal_server_error().await)
}
}
} else {
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
}
}
#[get("/<_..>")]
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
if cookies.get_private("userid").is_some() {
match NamedFile::open(Path::new(path!("static/index.html"))).await {
Ok(f) => DashboardPage::Ok(f),

View File

@@ -19,24 +19,6 @@
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
for the reminder.
</p>
<p class="subtitle">Time</p>
<p class="content">
The bot will take a "best-guess" at what time you entered. It will favour UK date formats
over US date formats (MM/DD/YY) where possible.
<br>
You can also use <code>cron</code>-like syntax to specify the time. For example, using
<code>0 0 1 * *</code> will send the reminder at midnight on the first of the next month.
For more information on cron syntax, see <a href="https://crontab.guru/">crontab.guru</a>.
<br>
<strong>Cron syntax is not repeating</strong>. Please use the optional "interval" field to specify a repetition interval.
</p>
<p class="subtitle">Pings</p>
<p class="content">
Roles and users can be pinged by including their @ mention in the "content" field.
To ping a role, the role must be set as mentionable, and the bot must have permissions to mention the role.
<br>
Please note that when using the dashboard, roles can only be pinged in the "Content..." field and not the embed fields.
</p>
</div>
</div>
</section>
@@ -55,40 +37,4 @@
</div>
</section>
<section class="hero is-small">
<div class="hero-body">
<div class="container">
<p class="title">Custom formatting rules</p>
<p class="content">
Reminder content can be customized using formatting rules.
</p>
<p class="subtitle">timefrom</p>
<p class="content">
The <code>timefrom</code> formatting rule will display a formatted difference
between the time the reminder sends and a specified time.
<br>
For example, if the current time is 1755800000 (UNIX time), the format string
<code>&lt;&lt;timefrom:1755803600&gt;&gt;</code> would display "1 hour"
</p>
<p class="subtitle">timenow</p>
<p class="content">
The <code>timenow</code> formatting rule displays the current time or an offset
from the current time in a given timezone in a custom format.
<br>
For example, if the current time is 1755800000 (UNIX time), the format string
<code>&lt;&lt;timenow:UTC:%H:%M:%S&gt;&gt;</code> would display "18:13:20"
<br>
Optionally, an offset can be provided to display a time from your current time.
For example, if the current time is 1755800000 (UNIX time), the format string
<code>&lt;&lt;timenow+120:UTC:%H:%M:%S&gt;&gt;</code> would display "18:15:20",
or <code>&lt;&lt;timenow-120:UTC:%H:%M:%S&gt;&gt;</code> would display "18:11:20"
<br>
You can use this feature alongside Discord's timestamp formatting. The following
will show the text "in 2 minutes" for all users as a Discord timestamp:
<code>&lt;t:&lt;&lt;timenow+120:UTC:%s&gt;&gt;:R&gt;</code>
</p>
</div>
</div>
</section>
{% endblock %}