Compare commits
2 Commits
current
...
jude/restr
Author | SHA1 | Date | |
---|---|---|---|
|
192e316926 | ||
|
19cfacffe5 |
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@@ -524,15 +524,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
@@ -2623,12 +2614,11 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.40"
|
version = "1.7.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"cron-parser",
|
|
||||||
"csv",
|
"csv",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.40"
|
version = "1.7.37"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
@@ -35,7 +35,6 @@ serenity = { version = "0.12", default-features = false, features = ["builder",
|
|||||||
oauth2 = "4"
|
oauth2 = "4"
|
||||||
csv = "1.2"
|
csv = "1.2"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
cron-parser = "0.10"
|
|
||||||
|
|
||||||
[dependencies.extract_derive]
|
[dependencies.extract_derive]
|
||||||
path = "extract_derive"
|
path = "extract_derive"
|
||||||
|
12676
gb-ipv4.csv
12676
gb-ipv4.csv
File diff suppressed because it is too large
Load Diff
84
migrations/20250506184716_remove_unused_columns.sql
Normal file
84
migrations/20250506184716_remove_unused_columns.sql
Normal 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;
|
@@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE patreon_link
|
|
||||||
(
|
|
||||||
user_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
guild_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
linked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
PRIMARY KEY (user_id),
|
|
||||||
INDEX `idx_user_id` (user_id),
|
|
||||||
INDEX `idx_guild_id` (guild_id),
|
|
||||||
INDEX `idx_linked_at` (linked_at)
|
|
||||||
);
|
|
@@ -3,7 +3,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use chrono_tz::TZ_VARIANTS;
|
use chrono_tz::TZ_VARIANTS;
|
||||||
use poise::serenity_prelude::AutocompleteChoice;
|
use poise::serenity_prelude::AutocompleteChoice;
|
||||||
|
|
||||||
use crate::time_parser::cron_next_timestamp;
|
|
||||||
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
||||||
|
|
||||||
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
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
|
SELECT name
|
||||||
FROM command_macro
|
FROM command_macro
|
||||||
WHERE
|
WHERE
|
||||||
guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
guild_id = ?
|
||||||
AND name LIKE CONCAT(?, '%')
|
AND name LIKE CONCAT(?, '%')
|
||||||
",
|
",
|
||||||
ctx.guild_id().unwrap().get(),
|
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() {
|
if partial.is_empty() {
|
||||||
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
||||||
} else {
|
} else {
|
||||||
let timezone = ctx.timezone().await;
|
match natural_parser(partial, &ctx.timezone().await.to_string()).await {
|
||||||
let timestamp = match cron_next_timestamp(partial, timezone) {
|
|
||||||
Some(ts) => Some(ts),
|
|
||||||
None => natural_parser(partial, &timezone.to_string()).await,
|
|
||||||
};
|
|
||||||
|
|
||||||
match timestamp {
|
|
||||||
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(now) => {
|
Ok(now) => {
|
||||||
let diff = timestamp - now.as_secs() as i64;
|
let diff = timestamp - now.as_secs() as i64;
|
||||||
|
@@ -17,12 +17,10 @@ pub async fn delete_macro(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match sqlx::query!(
|
match sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT m.id
|
SELECT id
|
||||||
FROM command_macro m
|
FROM command_macro
|
||||||
INNER JOIN guilds
|
WHERE guild_id = ?
|
||||||
ON guilds.id = m.guild_id
|
AND name = ?
|
||||||
WHERE guild = ?
|
|
||||||
AND m.name = ?
|
|
||||||
",
|
",
|
||||||
ctx.guild_id().unwrap().get(),
|
ctx.guild_id().unwrap().get(),
|
||||||
name
|
name
|
||||||
|
@@ -32,7 +32,7 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
let json = serde_json::to_string(&command_macro.commands).unwrap();
|
let json = serde_json::to_string(&command_macro.commands).unwrap();
|
||||||
|
|
||||||
sqlx::query!(
|
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.guild_id.get(),
|
||||||
command_macro.name,
|
command_macro.name,
|
||||||
command_macro.description,
|
command_macro.description,
|
||||||
|
@@ -35,7 +35,7 @@ pub async fn record_macro(
|
|||||||
"
|
"
|
||||||
SELECT 1 as _e
|
SELECT 1 as _e
|
||||||
FROM command_macro
|
FROM command_macro
|
||||||
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
WHERE guild_id = ?
|
||||||
AND name = ?
|
AND name = ?
|
||||||
",
|
",
|
||||||
guild_id.get(),
|
guild_id.get(),
|
||||||
@@ -75,8 +75,8 @@ Please select a unique name for your macro.",
|
|||||||
CreateEmbed::new()
|
CreateEmbed::new()
|
||||||
.title("Macro Recording Started")
|
.title("Macro Recording Started")
|
||||||
.description(
|
.description(
|
||||||
"Run up to 5 commands to record in this macro. Use `/macro finish` to stop recording at any point.
|
"Run up to 5 commands, or type `/macro finish` to stop at any point.
|
||||||
Any commands performed during recording won't take any actual action- they are only captured for the macro.",
|
Any commands ran as part of recording will be inconsequential",
|
||||||
)
|
)
|
||||||
.color(*THEME_COLOR),
|
.color(*THEME_COLOR),
|
||||||
),
|
),
|
||||||
|
@@ -15,8 +15,8 @@ impl Recordable for Options {
|
|||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
ctx.send(CreateReply::default().embed(CreateEmbed::new().title("Donate")
|
||||||
.description("Thinking of subscribing?
|
.description("Thinking of adding a monthly contribution?
|
||||||
Click below for my Patreon and official bot server
|
Click below for my Patreon and official bot server :)
|
||||||
|
|
||||||
**https://www.patreon.com/jellywx/**
|
**https://www.patreon.com/jellywx/**
|
||||||
**https://discord.jellywx.com/**
|
**https://discord.jellywx.com/**
|
||||||
@@ -26,7 +26,7 @@ With your new rank, you'll be able to:
|
|||||||
• Set repeating reminders with `/remind` or the dashboard
|
• Set repeating reminders with `/remind` or the dashboard
|
||||||
• Use unlimited uploads on SoundFX
|
• Use unlimited uploads on SoundFX
|
||||||
|
|
||||||
Members of servers you __own__ will be able to set repeating reminders via commands. You can also choose to share your membership with one other server.
|
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
|
||||||
|
|
||||||
Just $2 USD/month!
|
Just $2 USD/month!
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ Just $2 USD/month!
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show Patreon information
|
/// Details on supporting the bot and Patreon benefits
|
||||||
#[poise::command(slash_command, rename = "info", identifying_name = "patreon_info")]
|
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
||||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn command(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
(Options {}).run(ctx).await
|
(Options {}).run(ctx).await
|
||||||
}
|
}
|
@@ -12,6 +12,8 @@ pub mod dashboard;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
pub mod donate;
|
||||||
|
#[cfg(not(test))]
|
||||||
pub mod help;
|
pub mod help;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod info;
|
pub mod info;
|
||||||
@@ -24,8 +26,6 @@ pub mod nudge;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod offset;
|
pub mod offset;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod patreon;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub mod pause;
|
pub mod pause;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub mod remind;
|
pub mod remind;
|
||||||
|
@@ -1,73 +0,0 @@
|
|||||||
use poise::CreateReply;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
utils::{check_user_subscription, Extract, Recordable},
|
|
||||||
Context, Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Extract)]
|
|
||||||
pub struct Options;
|
|
||||||
|
|
||||||
impl Recordable for Options {
|
|
||||||
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
let guild_id = ctx.guild_id().ok_or("This command must be used in a server")?;
|
|
||||||
let user_id = ctx.author().id;
|
|
||||||
|
|
||||||
// Check if user has Patreon subscription
|
|
||||||
if !check_user_subscription(ctx, user_id).await {
|
|
||||||
ctx.send(CreateReply::default()
|
|
||||||
.content("❌ You must be a Patreon subscriber to use this command. Use `/patreon info` for more information.")
|
|
||||||
.ephemeral(true)
|
|
||||||
).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let existing_link = sqlx::query!(
|
|
||||||
"SELECT linked_at FROM patreon_link WHERE user_id = ? AND linked_at > NOW() - INTERVAL 4 WEEK",
|
|
||||||
user_id.get()
|
|
||||||
)
|
|
||||||
.fetch_optional(&ctx.data().database)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if existing_link.is_some() {
|
|
||||||
ctx.send(
|
|
||||||
CreateReply::default()
|
|
||||||
.content("❌ You can only link once every 4 weeks. Please try again later.")
|
|
||||||
.ephemeral(true),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert or update the patreon_link entry
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO patreon_link (user_id, guild_id, linked_at) VALUES (?, ?, NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE user_id = user_id",
|
|
||||||
user_id.get(),
|
|
||||||
guild_id.get()
|
|
||||||
)
|
|
||||||
.execute(&ctx.data().database)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ctx.send(
|
|
||||||
CreateReply::default()
|
|
||||||
.content("✅ Successfully linked your Patreon subscription to this server!")
|
|
||||||
.ephemeral(true),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Link your Patreon subscription to this server. This command can be run once every four weeks
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "link",
|
|
||||||
identifying_name = "patreon_link",
|
|
||||||
guild_only = true
|
|
||||||
)]
|
|
||||||
pub async fn link(ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
(Options {}).run(ctx).await
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
pub mod info;
|
|
||||||
pub mod link;
|
|
||||||
pub mod unlink;
|
|
||||||
|
|
||||||
use crate::{Context, Error};
|
|
||||||
|
|
||||||
/// Manage Patreon subscription features
|
|
||||||
#[poise::command(slash_command, rename = "patreon", identifying_name = "patreon")]
|
|
||||||
pub async fn command(_ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@@ -1,55 +0,0 @@
|
|||||||
use poise::CreateReply;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
utils::{Extract, Recordable},
|
|
||||||
Context, Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Extract)]
|
|
||||||
pub struct Options;
|
|
||||||
|
|
||||||
impl Recordable for Options {
|
|
||||||
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
let guild_id = ctx.guild_id().ok_or("This command must be used in a server")?;
|
|
||||||
let user_id = ctx.author().id;
|
|
||||||
|
|
||||||
// Remove the patreon_link entry
|
|
||||||
let result = sqlx::query!(
|
|
||||||
"DELETE FROM patreon_link WHERE user_id = ? AND guild_id = ?",
|
|
||||||
user_id.get(),
|
|
||||||
guild_id.get()
|
|
||||||
)
|
|
||||||
.execute(&ctx.data().database)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if result.rows_affected() > 0 {
|
|
||||||
ctx.send(
|
|
||||||
CreateReply::default()
|
|
||||||
.content("✅ Successfully unlinked your Patreon subscription from this server!")
|
|
||||||
.ephemeral(true),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
ctx.send(
|
|
||||||
CreateReply::default()
|
|
||||||
.content("❌ No existing Patreon link found for this server.")
|
|
||||||
.ephemeral(true),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unlink your Patreon subscription from this server
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "unlink",
|
|
||||||
identifying_name = "patreon_unlink",
|
|
||||||
guild_only = true
|
|
||||||
)]
|
|
||||||
pub async fn unlink(ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
(Options {}).run(ctx).await
|
|
||||||
}
|
|
@@ -21,7 +21,7 @@ impl Recordable for Options {
|
|||||||
"
|
"
|
||||||
INSERT INTO todos (guild_id, channel_id, value)
|
INSERT INTO todos (guild_id, channel_id, value)
|
||||||
VALUES (
|
VALUES (
|
||||||
(SELECT id FROM guilds WHERE guild = ?),
|
?,
|
||||||
(SELECT id FROM channels WHERE channel = ?),
|
(SELECT id FROM channels WHERE channel = ?),
|
||||||
?
|
?
|
||||||
)
|
)
|
||||||
|
@@ -18,7 +18,7 @@ impl Recordable for Options {
|
|||||||
"
|
"
|
||||||
INSERT INTO todos (guild_id, value)
|
INSERT INTO todos (guild_id, value)
|
||||||
VALUES (
|
VALUES (
|
||||||
(SELECT id FROM guilds WHERE guild = ?), ?
|
?, ?
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
ctx.guild_id().unwrap().get(),
|
ctx.guild_id().unwrap().get(),
|
||||||
|
@@ -13,9 +13,8 @@ impl Recordable for Options {
|
|||||||
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
async fn run(self, ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let values = sqlx::query!(
|
let values = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT todos.id, value FROM todos
|
SELECT id, value FROM todos
|
||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
WHERE guild_id = ?
|
||||||
WHERE guilds.guild = ?
|
|
||||||
",
|
",
|
||||||
ctx.guild_id().unwrap().get(),
|
ctx.guild_id().unwrap().get(),
|
||||||
)
|
)
|
||||||
|
@@ -219,9 +219,8 @@ impl ComponentDataModel {
|
|||||||
} else {
|
} else {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT todos.id, value FROM todos
|
SELECT id, value FROM todos
|
||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
WHERE guild_id = ?
|
||||||
WHERE guilds.guild = ?
|
|
||||||
",
|
",
|
||||||
pager.guild_id,
|
pager.guild_id,
|
||||||
)
|
)
|
||||||
@@ -311,9 +310,8 @@ impl ComponentDataModel {
|
|||||||
} else {
|
} else {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT todos.id, value FROM todos
|
SELECT id, value FROM todos
|
||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
WHERE guild_id = ?
|
||||||
WHERE guilds.guild = ?
|
|
||||||
",
|
",
|
||||||
selector.guild_id,
|
selector.guild_id,
|
||||||
)
|
)
|
||||||
|
@@ -23,7 +23,7 @@ pub async fn listener(
|
|||||||
if is_new.unwrap_or(false) {
|
if is_new.unwrap_or(false) {
|
||||||
let guild_id = guild.id.get().to_owned();
|
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)
|
.execute(&data.database)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
|
|||||||
}
|
}
|
||||||
FullEvent::GuildDelete { incomplete, .. } => {
|
FullEvent::GuildDelete { incomplete, .. } => {
|
||||||
if !incomplete.unavailable {
|
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)
|
.execute(&data.database)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
57
src/hooks.rs
57
src/hooks.rs
@@ -1,6 +1,4 @@
|
|||||||
use crate::consts::THEME_COLOR;
|
use poise::{CommandInteractionType, CreateReply};
|
||||||
use poise::{serenity_prelude::CreateEmbed, CommandInteractionType, CreateReply};
|
|
||||||
use serenity::builder::CreateEmbedFooter;
|
|
||||||
|
|
||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
|
||||||
|
|
||||||
@@ -20,18 +18,7 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
.send(
|
.send(
|
||||||
CreateReply::default()
|
CreateReply::default()
|
||||||
.ephemeral(true)
|
.ephemeral(true)
|
||||||
.embed(CreateEmbed::new()
|
.content(format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS))
|
||||||
.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),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
@@ -41,19 +28,9 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
|
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.send(
|
.send(
|
||||||
CreateReply::default().ephemeral(true).embed(
|
CreateReply::default()
|
||||||
CreateEmbed::new()
|
.ephemeral(true)
|
||||||
.title("💾 Currently recording macro")
|
.content("Command recorded to 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),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -61,18 +38,8 @@ async fn macro_check(ctx: Context<'_>) -> bool {
|
|||||||
None => {
|
None => {
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.send(
|
.send(
|
||||||
CreateReply::default().ephemeral(true).embed(
|
CreateReply::default().ephemeral(true).content(
|
||||||
CreateEmbed::new()
|
"This command is not supported in macros yet.",
|
||||||
.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),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -107,7 +74,6 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
return if permissions.send_messages()
|
return if permissions.send_messages()
|
||||||
&& permissions.embed_links()
|
&& permissions.embed_links()
|
||||||
&& manage_webhooks
|
&& manage_webhooks
|
||||||
&& permissions.view_channel()
|
|
||||||
{
|
{
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -115,13 +81,12 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
.send(CreateReply::default().content(format!(
|
.send(CreateReply::default().content(format!(
|
||||||
"The bot appears to be missing some permissions:
|
"The bot appears to be missing some permissions:
|
||||||
|
|
||||||
{} **View Channels**
|
|
||||||
{} **Send Message**
|
{} **Send Message**
|
||||||
{} **Embed Links**
|
{} **Embed Links**
|
||||||
{} **Manage Webhooks**
|
{} **Manage Webhooks**
|
||||||
|
|
||||||
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot \"Administrator\" will bypass permission checks",
|
Please check the bot's roles, and any channel overrides. Alternatively, giving the bot
|
||||||
if permissions.view_channel() { "✅" } else { "❌" },
|
\"Administrator\" will bypass permission checks",
|
||||||
if permissions.send_messages() { "✅" } else { "❌" },
|
if permissions.send_messages() { "✅" } else { "❌" },
|
||||||
if permissions.embed_links() { "✅" } else { "❌" },
|
if permissions.embed_links() { "✅" } else { "❌" },
|
||||||
if manage_webhooks { "✅" } else { "❌" },
|
if manage_webhooks { "✅" } else { "❌" },
|
||||||
@@ -135,7 +100,9 @@ Please check the bot's roles, and any channel overrides. Alternatively, giving t
|
|||||||
manage_webhooks
|
manage_webhooks
|
||||||
}
|
}
|
||||||
|
|
||||||
None => true,
|
None => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
src/main.rs
13
src/main.rs
@@ -48,8 +48,8 @@ use crate::test::TestContext;
|
|||||||
use crate::{
|
use crate::{
|
||||||
commands::{
|
commands::{
|
||||||
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
allowed_dm, clock, clock_context_menu::clock_context_menu, command_macro, dashboard,
|
||||||
delete, help, info, look, multiline, nudge, offset, patreon, pause, remind, settings,
|
delete, donate, help, info, look, multiline, nudge, offset, pause, remind, settings, timer,
|
||||||
timer, timezone, todo, webhook,
|
timezone, todo, webhook,
|
||||||
},
|
},
|
||||||
consts::THEME_COLOR,
|
consts::THEME_COLOR,
|
||||||
event_handlers::listener,
|
event_handlers::listener,
|
||||||
@@ -165,14 +165,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
|
|||||||
help::command(),
|
help::command(),
|
||||||
info::command(),
|
info::command(),
|
||||||
clock::command(),
|
clock::command(),
|
||||||
poise::Command {
|
donate::command(),
|
||||||
subcommands: vec![
|
|
||||||
patreon::link::link(),
|
|
||||||
patreon::unlink::unlink(),
|
|
||||||
patreon::info::info(),
|
|
||||||
],
|
|
||||||
..patreon::command()
|
|
||||||
},
|
|
||||||
clock_context_menu(),
|
clock_context_menu(),
|
||||||
dashboard::command(),
|
dashboard::command(),
|
||||||
timezone::command(),
|
timezone::command(),
|
||||||
|
@@ -8,9 +8,7 @@ use crate::{consts::DEFAULT_AVATAR, Error};
|
|||||||
pub struct ChannelData {
|
pub struct ChannelData {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub channel: u64,
|
pub channel: u64,
|
||||||
pub name: Option<String>,
|
|
||||||
pub nudge: i16,
|
pub nudge: i16,
|
||||||
pub blacklisted: bool,
|
|
||||||
pub webhook_id: Option<u64>,
|
pub webhook_id: Option<u64>,
|
||||||
pub webhook_token: Option<String>,
|
pub webhook_token: Option<String>,
|
||||||
pub paused: bool,
|
pub paused: bool,
|
||||||
@@ -27,7 +25,7 @@ impl ChannelData {
|
|||||||
if let Ok(c) = sqlx::query_as_unchecked!(
|
if let Ok(c) = sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, channel, name, nudge, blacklisted, webhook_id, webhook_token, paused,
|
SELECT id, channel, nudge, webhook_id, webhook_token, paused,
|
||||||
paused_until
|
paused_until
|
||||||
FROM channels
|
FROM channels
|
||||||
WHERE channel = ?
|
WHERE channel = ?
|
||||||
@@ -39,15 +37,11 @@ impl ChannelData {
|
|||||||
{
|
{
|
||||||
Ok(c)
|
Ok(c)
|
||||||
} else {
|
} else {
|
||||||
let props = channel.to_owned().guild().map(|g| (g.guild_id.get().to_owned(), g.name));
|
let guild_id = channel.to_owned().guild().map(|g| g.guild_id.get().to_owned());
|
||||||
|
|
||||||
let (guild_id, channel_name) =
|
|
||||||
if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
|
|
||||||
|
|
||||||
sqlx::query!(
|
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_id,
|
||||||
channel_name,
|
|
||||||
guild_id
|
guild_id
|
||||||
)
|
)
|
||||||
.execute(&pool.clone())
|
.execute(&pool.clone())
|
||||||
@@ -56,7 +50,7 @@ impl ChannelData {
|
|||||||
Ok(sqlx::query_as_unchecked!(
|
Ok(sqlx::query_as_unchecked!(
|
||||||
Self,
|
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
|
FROM channels
|
||||||
WHERE channel = ?
|
WHERE channel = ?
|
||||||
",
|
",
|
||||||
@@ -72,18 +66,14 @@ impl ChannelData {
|
|||||||
"
|
"
|
||||||
UPDATE channels
|
UPDATE channels
|
||||||
SET
|
SET
|
||||||
name = ?,
|
|
||||||
nudge = ?,
|
nudge = ?,
|
||||||
blacklisted = ?,
|
|
||||||
webhook_id = ?,
|
webhook_id = ?,
|
||||||
webhook_token = ?,
|
webhook_token = ?,
|
||||||
paused = ?,
|
paused = ?,
|
||||||
paused_until = ?
|
paused_until = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
",
|
",
|
||||||
self.name,
|
|
||||||
self.nudge,
|
self.nudge,
|
||||||
self.blacklisted,
|
|
||||||
self.webhook_id,
|
self.webhook_id,
|
||||||
self.webhook_token,
|
self.webhook_token,
|
||||||
self.paused,
|
self.paused,
|
||||||
|
@@ -43,7 +43,7 @@ pub enum RecordedCommand {
|
|||||||
#[serde(rename = "delete")]
|
#[serde(rename = "delete")]
|
||||||
Delete(crate::commands::delete::Options),
|
Delete(crate::commands::delete::Options),
|
||||||
#[serde(rename = "donate")]
|
#[serde(rename = "donate")]
|
||||||
Donate(crate::commands::patreon::info::Options),
|
Donate(crate::commands::donate::Options),
|
||||||
#[serde(rename = "help")]
|
#[serde(rename = "help")]
|
||||||
Help(crate::commands::help::Options),
|
Help(crate::commands::help::Options),
|
||||||
#[serde(rename = "info")]
|
#[serde(rename = "info")]
|
||||||
@@ -111,7 +111,7 @@ impl RecordedCommand {
|
|||||||
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
"clock" => Some(Self::Clock(crate::commands::clock::Options::extract(ctx))),
|
||||||
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
"dashboard" => Some(Self::Dashboard(crate::commands::dashboard::Options::extract(ctx))),
|
||||||
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
"delete" => Some(Self::Delete(crate::commands::delete::Options::extract(ctx))),
|
||||||
"donate" => Some(Self::Donate(crate::commands::patreon::info::Options::extract(ctx))),
|
"donate" => Some(Self::Donate(crate::commands::donate::Options::extract(ctx))),
|
||||||
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
"help" => Some(Self::Help(crate::commands::help::Options::extract(ctx))),
|
||||||
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
"info" => Some(Self::Info(crate::commands::info::Options::extract(ctx))),
|
||||||
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
"look" => Some(Self::Look(crate::commands::look::Options::extract(ctx))),
|
||||||
@@ -144,12 +144,10 @@ pub async fn guild_command_macro(ctx: &Context<'_>, name: &str) -> Option<Comman
|
|||||||
let row = sqlx::query_as!(
|
let row = sqlx::query_as!(
|
||||||
Row,
|
Row,
|
||||||
"
|
"
|
||||||
SELECT m.name, m.description, m.commands
|
SELECT name, description, commands
|
||||||
FROM command_macro m
|
FROM command_macro m
|
||||||
INNER JOIN guilds g
|
WHERE guild_id = ?
|
||||||
ON g.id = m.guild_id
|
AND name = ?
|
||||||
WHERE guild = ?
|
|
||||||
AND m.name = ?
|
|
||||||
",
|
",
|
||||||
ctx.guild_id().unwrap().get(),
|
ctx.guild_id().unwrap().get(),
|
||||||
name
|
name
|
||||||
|
@@ -3,7 +3,7 @@ use sqlx::MySqlPool;
|
|||||||
|
|
||||||
pub struct GuildData {
|
pub struct GuildData {
|
||||||
pub ephemeral_confirmations: bool,
|
pub ephemeral_confirmations: bool,
|
||||||
pub id: u32,
|
pub guild_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GuildData {
|
impl GuildData {
|
||||||
@@ -13,7 +13,7 @@ impl GuildData {
|
|||||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||||
if let Ok(c) = sqlx::query_as_unchecked!(
|
if let Ok(c) = sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
|
"SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
|
||||||
guild_id.get()
|
guild_id.get()
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
@@ -21,13 +21,13 @@ impl GuildData {
|
|||||||
{
|
{
|
||||||
Ok(c)
|
Ok(c)
|
||||||
} else {
|
} 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())
|
.execute(&pool.clone())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(sqlx::query_as_unchecked!(
|
Ok(sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
|
"SELECT ephemeral_confirmations, id as guild_id FROM guilds WHERE id = ?",
|
||||||
guild_id.get()
|
guild_id.get()
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
@@ -39,7 +39,7 @@ impl GuildData {
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
|
"UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
|
||||||
self.ephemeral_confirmations,
|
self.ephemeral_confirmations,
|
||||||
self.id
|
self.guild_id
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
|
@@ -68,16 +68,19 @@ impl Data {
|
|||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
) -> Result<Vec<CommandMacro>, Error> {
|
) -> Result<Vec<CommandMacro>, Error> {
|
||||||
let rows = sqlx::query!(
|
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()
|
guild_id.get()
|
||||||
)
|
)
|
||||||
.fetch_all(&self.database)
|
.fetch_all(&self.database)
|
||||||
.await?.iter().map(|row| CommandMacro {
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|row| CommandMacro {
|
||||||
guild_id,
|
guild_id,
|
||||||
name: row.name.clone(),
|
name: row.name.clone(),
|
||||||
description: row.description.clone(),
|
description: row.description.clone(),
|
||||||
commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
|
commands: serde_json::from_str(&row.commands.to_string()).unwrap(),
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
@@ -16,8 +16,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
CtxData,
|
CtxData,
|
||||||
},
|
},
|
||||||
time_parser::{cron_next_timestamp, natural_parser},
|
time_parser::natural_parser,
|
||||||
utils::check_subscription,
|
utils::{check_guild_subscription, check_subscription},
|
||||||
Context, Database, Error,
|
Context, Database, Error,
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
@@ -262,7 +262,7 @@ impl Reminder {
|
|||||||
channels.id = reminders.channel_id
|
channels.id = reminders.channel_id
|
||||||
WHERE
|
WHERE
|
||||||
`status` = 'pending' AND
|
`status` = 'pending' AND
|
||||||
channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
channels.guild_id = ?
|
||||||
",
|
",
|
||||||
guild_id.get()
|
guild_id.get()
|
||||||
)
|
)
|
||||||
@@ -486,10 +486,7 @@ pub async fn create_reminder(
|
|||||||
let user_data = ctx.author_data().await.unwrap();
|
let user_data = ctx.author_data().await.unwrap();
|
||||||
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
||||||
|
|
||||||
let time = match cron_next_timestamp(&time, timezone) {
|
let time = natural_parser(&time, &timezone.to_string()).await;
|
||||||
Some(ts) => Some(ts),
|
|
||||||
None => natural_parser(&time, &timezone.to_string()).await,
|
|
||||||
};
|
|
||||||
|
|
||||||
match time {
|
match time {
|
||||||
Some(time) => {
|
Some(time) => {
|
||||||
@@ -532,7 +529,10 @@ pub async fn create_reminder(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
|
let (processed_interval, processed_expires) = if let Some(repeat) = &interval {
|
||||||
if check_subscription(&ctx, ctx.author().id, ctx.guild_id()).await {
|
if check_subscription(&ctx, ctx.author().id).await
|
||||||
|
|| (ctx.guild_id().is_some()
|
||||||
|
&& check_guild_subscription(&ctx, ctx.guild_id().unwrap()).await)
|
||||||
|
{
|
||||||
(
|
(
|
||||||
parse_duration(repeat)
|
parse_duration(repeat)
|
||||||
.or_else(|_| parse_duration(&format!("1 {}", repeat)))
|
.or_else(|_| parse_duration(&format!("1 {}", repeat)))
|
||||||
|
@@ -34,10 +34,8 @@ use crate::{
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref TIMEFROM_REGEX: Regex =
|
pub static ref TIMEFROM_REGEX: Regex =
|
||||||
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
||||||
pub static ref TIMENOW_REGEX: Regex = Regex::new(
|
pub static ref TIMENOW_REGEX: Regex =
|
||||||
r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
|
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
|
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 {
|
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 final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
let format = caps.name("format").map(|m| m.as_str());
|
||||||
|
|
||||||
@@ -94,26 +92,12 @@ pub fn substitute(string: &str) -> String {
|
|||||||
});
|
});
|
||||||
|
|
||||||
TIMENOW_REGEX
|
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 timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
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) {
|
if let (Some(timezone), Some(format)) = (timezone, format) {
|
||||||
let mut now = Utc::now().with_timezone(&timezone);
|
let 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
now.format(format).to_string()
|
now.format(format).to_string()
|
||||||
} else {
|
} else {
|
||||||
|
@@ -3,8 +3,6 @@ use poise::{
|
|||||||
CreateReply,
|
CreateReply,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serenity::all::Http;
|
|
||||||
use serenity::http::CacheHttp;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::{Data, Error};
|
use crate::{Data, Error};
|
||||||
@@ -21,12 +19,6 @@ pub(crate) struct TestContext<'a> {
|
|||||||
pub(crate) shard_id: usize,
|
pub(crate) shard_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CacheHttp for TestContext<'_> {
|
|
||||||
fn http(&self) -> &Http {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct MockUser {
|
pub(crate) struct MockUser {
|
||||||
pub(crate) id: UserId,
|
pub(crate) id: UserId,
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,6 @@ use std::{
|
|||||||
|
|
||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use cron_parser::parse;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
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) })
|
.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)
|
|
||||||
}
|
|
||||||
|
64
src/utils.rs
64
src/utils.rs
@@ -12,58 +12,7 @@ use crate::{
|
|||||||
ApplicationContext, Context, Error,
|
ApplicationContext, Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Check if this user/guild combination should be considered subscribed.
|
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
||||||
/// If the guild has a patreon linked, check the user involved in the link.
|
|
||||||
/// Otherwise, check the user and the guild's owner
|
|
||||||
pub async fn check_subscription(
|
|
||||||
ctx: &Context<'_>,
|
|
||||||
user_id: UserId,
|
|
||||||
guild_id: Option<GuildId>,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
|
||||||
let user_subscribed = check_user_subscription(ctx, user_id).await;
|
|
||||||
|
|
||||||
let owner_subscribed = match guild_id {
|
|
||||||
Some(guild_id) => {
|
|
||||||
if let Some(owner) = ctx.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
|
|
||||||
check_user_subscription(ctx, owner).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let link_subscribed = match guild_id {
|
|
||||||
Some(guild_id) => {
|
|
||||||
if let Ok(row) = sqlx::query!(
|
|
||||||
"SELECT user_id FROM patreon_link WHERE user_id = ?",
|
|
||||||
guild_id.get()
|
|
||||||
)
|
|
||||||
.fetch_one(&ctx.data().database)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
check_user_subscription(ctx, row.user_id).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
user_subscribed || owner_subscribed || link_subscribed
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check a user's subscription status, ignoring Patreon linkage
|
|
||||||
pub async fn check_user_subscription(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
user_id: impl Into<UserId>,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
let guild_member = GuildId::new(subscription_guild).member(cache_http, user_id).await;
|
||||||
|
|
||||||
@@ -81,6 +30,17 @@ pub async fn check_user_subscription(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_guild_subscription(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
guild_id: impl Into<GuildId>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(owner) = cache_http.cache().unwrap().guild(guild_id).map(|g| g.owner_id) {
|
||||||
|
check_subscription(&cache_http, owner).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reply_to_interaction_response_message(
|
pub fn reply_to_interaction_response_message(
|
||||||
reply: CreateReply,
|
reply: CreateReply,
|
||||||
) -> CreateInteractionResponseMessage {
|
) -> CreateInteractionResponseMessage {
|
||||||
|
@@ -92,8 +92,6 @@ enum Error {
|
|||||||
SQLx(sqlx::Error),
|
SQLx(sqlx::Error),
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
Serenity(serenity::Error),
|
Serenity(serenity::Error),
|
||||||
#[allow(unused)]
|
|
||||||
MissingDiscordPermission(&'static str),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize(
|
pub async fn initialize(
|
||||||
|
@@ -305,19 +305,11 @@ pub async fn edit_reminder(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
warn!(
|
warn!(
|
||||||
|
@@ -31,7 +31,7 @@ pub async fn get_reminder_templates(
|
|||||||
|
|
||||||
match sqlx::query_as_unchecked!(
|
match sqlx::query_as_unchecked!(
|
||||||
ReminderTemplate,
|
ReminderTemplate,
|
||||||
"SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
"SELECT * FROM reminder_template WHERE guild_id = ?",
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
@@ -57,7 +57,6 @@ pub async fn create_guild_reminder_template(
|
|||||||
check_authorization(cookies, ctx.inner(), id).await?;
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
match create_reminder_template(
|
match create_reminder_template(
|
||||||
ctx.inner(),
|
|
||||||
&mut transaction,
|
&mut transaction,
|
||||||
GuildId::new(id),
|
GuildId::new(id),
|
||||||
reminder_template.into_inner(),
|
reminder_template.into_inner(),
|
||||||
@@ -87,15 +86,14 @@ pub async fn delete_reminder_template(
|
|||||||
check_authorization(cookies, ctx.inner(), id).await?;
|
check_authorization(cookies, ctx.inner(), id).await?;
|
||||||
|
|
||||||
match sqlx::query!(
|
match sqlx::query!(
|
||||||
"DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
|
"DELETE FROM reminder_template WHERE guild_id = ? AND id = ?",
|
||||||
id, delete_reminder_template.id
|
id,
|
||||||
|
delete_reminder_template.id
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => Ok(json!({})),
|
||||||
Ok(json!({}))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not delete template from {}: {:?}", id, e);
|
warn!("Could not delete template from {}: {:?}", id, e);
|
||||||
|
|
||||||
|
@@ -66,7 +66,7 @@ pub async fn create_todo(
|
|||||||
"
|
"
|
||||||
INSERT INTO todos (guild_id, channel_id, value)
|
INSERT INTO todos (guild_id, channel_id, value)
|
||||||
VALUES (
|
VALUES (
|
||||||
(SELECT id FROM guilds WHERE guild = ?),
|
?,
|
||||||
(SELECT id FROM channels WHERE channel = ?),
|
(SELECT id FROM channels WHERE channel = ?),
|
||||||
?
|
?
|
||||||
)
|
)
|
||||||
@@ -88,7 +88,7 @@ pub async fn create_todo(
|
|||||||
"
|
"
|
||||||
INSERT INTO todos (guild_id, channel_id, value)
|
INSERT INTO todos (guild_id, channel_id, value)
|
||||||
VALUES (
|
VALUES (
|
||||||
(SELECT id FROM guilds WHERE guild = ?),
|
?,
|
||||||
NULL,
|
NULL,
|
||||||
?
|
?
|
||||||
)
|
)
|
||||||
@@ -130,11 +130,9 @@ pub async fn get_todo(
|
|||||||
channels.channel AS channel_id,
|
channels.channel AS channel_id,
|
||||||
value
|
value
|
||||||
FROM todos
|
FROM todos
|
||||||
INNER JOIN guilds
|
|
||||||
ON guilds.id = todos.guild_id
|
|
||||||
LEFT JOIN channels
|
LEFT JOIN channels
|
||||||
ON channels.id = todos.channel_id
|
ON channels.id = todos.channel_id
|
||||||
WHERE guilds.guild = ?
|
WHERE todos.guild_id = ?
|
||||||
",
|
",
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
@@ -167,7 +165,7 @@ pub async fn update_todo(
|
|||||||
"
|
"
|
||||||
UPDATE todos
|
UPDATE todos
|
||||||
SET value = ?
|
SET value = ?
|
||||||
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
WHERE guild_id = ?
|
||||||
AND id = ?
|
AND id = ?
|
||||||
",
|
",
|
||||||
todo.value,
|
todo.value,
|
||||||
@@ -202,7 +200,7 @@ pub async fn delete_todo(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
DELETE FROM todos
|
DELETE FROM todos
|
||||||
WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
WHERE guild_id = ?
|
||||||
AND id = ?
|
AND id = ?
|
||||||
",
|
",
|
||||||
guild_id,
|
guild_id,
|
||||||
|
@@ -65,16 +65,7 @@ pub async fn create_reminder(
|
|||||||
if let Err(e) = channel {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
|
|
||||||
// Provide more specific error messages based on the error type
|
return Err(json!({"error": "Failed to configure channel for reminders."}));
|
||||||
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}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = channel.unwrap();
|
let channel = channel.unwrap();
|
||||||
|
@@ -39,7 +39,8 @@ pub async fn export(
|
|||||||
|
|
||||||
match sqlx::query_as_unchecked!(
|
match sqlx::query_as_unchecked!(
|
||||||
ReminderTemplateCsv,
|
ReminderTemplateCsv,
|
||||||
"SELECT
|
"
|
||||||
|
SELECT
|
||||||
name,
|
name,
|
||||||
attachment,
|
attachment,
|
||||||
attachment_name,
|
attachment_name,
|
||||||
@@ -60,7 +61,9 @@ pub async fn export(
|
|||||||
interval_months,
|
interval_months,
|
||||||
tts,
|
tts,
|
||||||
username
|
username
|
||||||
FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
FROM reminder_template
|
||||||
|
WHERE guild_id = ?
|
||||||
|
",
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
@@ -144,7 +147,6 @@ pub async fn import(
|
|||||||
};
|
};
|
||||||
|
|
||||||
create_reminder_template(
|
create_reminder_template(
|
||||||
ctx.inner(),
|
|
||||||
&mut transaction,
|
&mut transaction,
|
||||||
GuildId::new(id),
|
GuildId::new(id),
|
||||||
reminder_template,
|
reminder_template,
|
||||||
|
@@ -38,10 +38,11 @@ pub async fn export(
|
|||||||
|
|
||||||
match sqlx::query_as_unchecked!(
|
match sqlx::query_as_unchecked!(
|
||||||
TodoCsv,
|
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
|
LEFT JOIN channels ON todos.channel_id = channels.id
|
||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
|
INNER JOIN guilds ON todos.guild_id = ?
|
||||||
WHERE guilds.guild = ?",
|
",
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
.fetch_all(pool.inner())
|
.fetch_all(pool.inner())
|
||||||
@@ -96,7 +97,7 @@ pub async fn import(
|
|||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
let mut reader = csv::Reader::from_reader(body.as_slice());
|
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![];
|
let mut query_params = vec![];
|
||||||
|
|
||||||
for result in reader.deserialize::<TodoCsv>() {
|
for result in reader.deserialize::<TodoCsv>() {
|
||||||
|
@@ -10,7 +10,6 @@ use rocket::{
|
|||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serenity::http::HttpError;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::CacheHttp,
|
all::CacheHttp,
|
||||||
builder::CreateWebhook,
|
builder::CreateWebhook,
|
||||||
@@ -374,12 +373,12 @@ pub(crate) async fn create_reminder(
|
|||||||
reminder: CreateReminder,
|
reminder: CreateReminder,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// check guild in db
|
// 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())
|
.fetch_one(transaction.executor())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
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())
|
.execute(transaction.executor())
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
@@ -405,19 +404,9 @@ pub(crate) async fn create_reminder(
|
|||||||
if let Err(e) = channel {
|
if let Err(e) = channel {
|
||||||
warn!("`create_database_channel` returned an error code: {:?}", e);
|
warn!("`create_database_channel` returned an error code: {:?}", e);
|
||||||
|
|
||||||
// Provide more specific error messages based on the error type
|
return Err(
|
||||||
let error_msg = match e {
|
json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
|
||||||
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}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = channel.unwrap();
|
let channel = channel.unwrap();
|
||||||
@@ -605,7 +594,6 @@ pub(crate) async fn create_reminder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_reminder_template(
|
pub(crate) async fn create_reminder_template(
|
||||||
ctx: &Context,
|
|
||||||
transaction: &mut Transaction<'_>,
|
transaction: &mut Transaction<'_>,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
reminder_template: ReminderTemplate,
|
reminder_template: ReminderTemplate,
|
||||||
@@ -670,7 +658,7 @@ pub(crate) async fn create_reminder_template(
|
|||||||
interval_months,
|
interval_months,
|
||||||
tts,
|
tts,
|
||||||
username
|
username
|
||||||
) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
?, ?, ?, ?, ?, ?, ?)",
|
?, ?, ?, ?, ?, ?, ?)",
|
||||||
guild_id.get(),
|
guild_id.get(),
|
||||||
name,
|
name,
|
||||||
@@ -727,36 +715,13 @@ async fn create_database_channel(
|
|||||||
|
|
||||||
match row {
|
match row {
|
||||||
Ok(row) => {
|
Ok(row) => {
|
||||||
let is_dm = channel
|
let is_dm =
|
||||||
.to_channel(&ctx)
|
channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
|
||||||
.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();
|
|
||||||
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
|
if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
|
||||||
let webhook = channel
|
let webhook = channel
|
||||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match &e {
|
.map_err(|e| Error::Serenity(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),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let token = webhook.token.unwrap();
|
let token = webhook.token.unwrap();
|
||||||
|
|
||||||
@@ -781,16 +746,7 @@ async fn create_database_channel(
|
|||||||
let webhook = channel
|
let webhook = channel
|
||||||
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
.create_webhook(&ctx, CreateWebhook::new("Reminder").avatar(&*DEFAULT_AVATAR))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match &e {
|
.map_err(|e| Error::Serenity(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),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let token = webhook.token.unwrap();
|
let token = webhook.token.unwrap();
|
||||||
|
|
||||||
@@ -849,15 +805,22 @@ pub async fn todos_redirect(id: &str) -> Redirect {
|
|||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
|
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||||
render_dashboard(cookies).await
|
if cookies.get_private("userid").is_some() {
|
||||||
}
|
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
||||||
|
Ok(f) => DashboardPage::Ok(f),
|
||||||
#[get("/<_..>")]
|
Err(e) => {
|
||||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
warn!("Couldn't render dashboard: {:?}", e);
|
||||||
render_dashboard(cookies).await
|
|
||||||
}
|
DashboardPage::NotConfigured(internal_server_error().await)
|
||||||
|
}
|
||||||
async fn render_dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
}
|
||||||
|
} else {
|
||||||
|
DashboardPage::Unauthorised(Redirect::to("/login/discord"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<_..>")]
|
||||||
|
pub async fn dashboard(cookies: &CookieJar<'_>) -> DashboardPage {
|
||||||
if cookies.get_private("userid").is_some() {
|
if cookies.get_private("userid").is_some() {
|
||||||
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
match NamedFile::open(Path::new(path!("static/index.html"))).await {
|
||||||
Ok(f) => DashboardPage::Ok(f),
|
Ok(f) => DashboardPage::Ok(f),
|
||||||
|
@@ -19,24 +19,6 @@
|
|||||||
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
||||||
for the reminder.
|
for the reminder.
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -55,40 +37,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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><<timefrom:1755803600>></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><<timenow:UTC:%H:%M:%S>></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><<timenow+120:UTC:%H:%M:%S>></code> would display "18:15:20",
|
|
||||||
or <code><<timenow-120:UTC:%H:%M:%S>></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><t:<<timenow+120:UTC:%s>>:R></code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user