8 Commits

Author SHA1 Message Date
b0a04bb289 Default channel command 2022-09-17 18:09:40 +01:00
eef1f6f3e8 Correct migration. Add guilds on interaction. Correct queries 2022-09-17 18:08:45 +01:00
3d08027325 Add migration for guild IDs to use discord ID 2022-09-17 18:08:45 +01:00
94bfd39085 Patch compilation against live schema 2022-09-17 13:05:50 +01:00
40cd5f8a36 Patch compilation against live schema 2022-09-17 13:03:52 +01:00
133b00a2ce Patch compilation against live schema 2022-09-17 12:52:03 +01:00
57336f5c81 Change macro list to use fields to prevent going over limit
Add length checks for name and description
2022-09-17 12:37:58 +01:00
b62d24c024 Add an autocomplete for time hints
Shows the approximate time until a reminder will send in the autocomplete area.
2022-09-12 17:49:10 +01:00
23 changed files with 471 additions and 163 deletions

2
Cargo.lock generated
View File

@ -2145,7 +2145,7 @@ checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "reminder_rs"
version = "1.6.5"
version = "1.6.6"
dependencies = [
"base64",
"chrono",

View File

@ -1,6 +1,6 @@
[package]
name = "reminder_rs"
version = "1.6.5"
version = "1.6.6"
authors = ["jellywx <judesouthworth@pm.me>"]
edition = "2018"

View File

@ -0,0 +1,92 @@
SET foreign_key_checks = 0;
START TRANSACTION;
-- drop existing constraints
ALTER TABLE channels DROP FOREIGN KEY `channels_ibfk_1`;
ALTER TABLE command_aliases DROP FOREIGN KEY `command_aliases_ibfk_1`;
ALTER TABLE events DROP FOREIGN KEY `events_ibfk_1`;
ALTER TABLE guild_users DROP FOREIGN KEY `guild_users_ibfk_1`;
ALTER TABLE macro DROP FOREIGN KEY `macro_ibfk_1`;
ALTER TABLE roles DROP FOREIGN KEY `roles_ibfk_1`;
ALTER TABLE todos DROP FOREIGN KEY `todos_ibfk_2`;
ALTER TABLE reminder_template DROP FOREIGN KEY `reminder_template_ibfk_1`;
-- update foreign key types
ALTER TABLE channels MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE command_aliases MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE events MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE guild_users MODIFY `guild` BIGINT UNSIGNED;
ALTER TABLE macro MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE roles MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE todos MODIFY `guild_id` BIGINT UNSIGNED;
ALTER TABLE reminder_template MODIFY `guild_id` BIGINT UNSIGNED;
-- update foreign key values
UPDATE channels SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE command_aliases SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE events SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE guild_users SET `guild` = (SELECT `guild` FROM guilds WHERE guilds.`id` = guild_users.`guild`);
UPDATE macro SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE roles SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE todos SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
UPDATE reminder_template SET `guild_id` = (SELECT `guild` FROM guilds WHERE guilds.`id` = `guild_id`);
-- update guilds table
ALTER TABLE guilds MODIFY `id` BIGINT UNSIGNED NOT NULL;
UPDATE guilds SET `id` = `guild`;
ALTER TABLE guilds DROP COLUMN `guild`;
ALTER TABLE guilds ADD COLUMN `default_channel` BIGINT UNSIGNED;
ALTER TABLE guilds ADD CONSTRAINT `default_channel_fk`
FOREIGN KEY (`default_channel`)
REFERENCES channels(`channel`)
ON DELETE SET NULL
ON UPDATE CASCADE;
-- re-add constraints
ALTER TABLE channels ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE command_aliases ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE events ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE guild_users ADD CONSTRAINT
FOREIGN KEY (`guild`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE macro ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE roles ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE todos ADD CONSTRAINT
FOREIGN KEY (`guild_id`)
REFERENCES guilds(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
COMMIT;
SET foreign_key_checks = 1;

View File

@ -1,6 +1,9 @@
use chrono_tz::TZ_VARIANTS;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::Context;
use chrono_tz::TZ_VARIANTS;
use poise::AutocompleteChoice;
use crate::{models::CtxData, time_parser::natural_parser, Context};
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
if partial.is_empty() {
@ -21,7 +24,7 @@ pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Str
SELECT name
FROM macro
WHERE
guild_id = (SELECT id FROM guilds WHERE guild = ?)
guild_id = ?
AND name LIKE CONCAT(?, '%')",
ctx.guild_id().unwrap().0,
partial,
@ -33,3 +36,96 @@ WHERE
.map(|s| s.name.clone())
.collect()
}
pub async fn multiline_autocomplete(
_ctx: Context<'_>,
partial: &str,
) -> Vec<AutocompleteChoice<String>> {
if partial.is_empty() {
vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }]
} else {
vec![
AutocompleteChoice { name: partial.to_string(), value: partial.to_string() },
AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() },
]
}
}
pub async fn time_hint_autocomplete(
ctx: Context<'_>,
partial: &str,
) -> Vec<AutocompleteChoice<String>> {
if partial.is_empty() {
vec![AutocompleteChoice {
name: "Start typing a time...".to_string(),
value: "now".to_string(),
}]
} else {
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;
if diff < 0 {
vec![AutocompleteChoice {
name: "Time is in the past".to_string(),
value: "now".to_string(),
}]
} else {
if diff > 86400 {
vec![
AutocompleteChoice {
name: partial.to_string(),
value: partial.to_string(),
},
AutocompleteChoice {
name: format!(
"In approximately {} days, {} hours",
diff / 86400,
(diff % 86400) / 3600
),
value: partial.to_string(),
},
]
} else if diff > 3600 {
vec![
AutocompleteChoice {
name: partial.to_string(),
value: partial.to_string(),
},
AutocompleteChoice {
name: format!("In approximately {} hours", diff / 3600),
value: partial.to_string(),
},
]
} else {
vec![
AutocompleteChoice {
name: partial.to_string(),
value: partial.to_string(),
},
AutocompleteChoice {
name: format!("In approximately {} minutes", diff / 60),
value: partial.to_string(),
},
]
}
}
}
Err(_) => {
vec![AutocompleteChoice {
name: partial.to_string(),
value: partial.to_string(),
}]
}
},
None => {
vec![AutocompleteChoice {
name: "Time not recognised".to_string(),
value: "now".to_string(),
}]
}
}
}
}

View File

@ -17,7 +17,7 @@ pub async fn delete_macro(
) -> Result<(), Error> {
match sqlx::query!(
"
SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
SELECT id FROM macro WHERE guild_id = ? AND name = ?",
ctx.guild_id().unwrap().0,
name
)

View File

@ -2,7 +2,7 @@ use poise::CreateReply;
use crate::{
component_models::pager::{MacroPager, Pager},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
consts::THEME_COLOR,
models::{command_macro::CommandMacro, CtxData},
Context, Error,
};
@ -30,27 +30,7 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
}
pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
let mut skipped_char_count = 0;
macros
.iter()
.map(|m| {
if let Some(description) = &m.description {
format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
} else {
format!("**{}**\n- Has {} commands", m.name, m.commands.len())
}
})
.fold(1, |mut pages, p| {
skipped_char_count += p.len();
if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
skipped_char_count = p.len();
pages += 1;
}
pages
})
((macros.len() as f64) / 25.0).ceil() as usize
}
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
@ -75,45 +55,27 @@ pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea
page = pages - 1;
}
let mut char_count = 0;
let mut skipped_char_count = 0;
let lower = (page * 25).min(macros.len());
let upper = ((page + 1) * 25).min(macros.len());
let mut skipped_pages = 0;
let display_vec: Vec<String> = macros
.iter()
.map(|m| {
if let Some(description) = &m.description {
format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
} else {
format!("**{}**\n- Has {} commands", m.name, m.commands.len())
}
})
.skip_while(|p| {
skipped_char_count += p.len();
if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
skipped_char_count = p.len();
skipped_pages += 1;
}
skipped_pages < page
})
.take_while(|p| {
char_count += p.len();
char_count < EMBED_DESCRIPTION_MAX_LENGTH
})
.collect::<Vec<String>>();
let display = display_vec.join("\n");
let fields = macros[lower..upper].iter().map(|m| {
if let Some(description) = &m.description {
(
m.name.clone(),
format!("*{}*\n- Has {} commands", description, m.commands.len()),
true,
)
} else {
(m.name.clone(), format!("- Has {} commands", m.commands.len()), true)
}
});
let mut reply = CreateReply::default();
reply
.embed(|e| {
e.title("Macros")
.description(display)
.fields(fields)
.footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
.color(*THEME_COLOR)
})

View File

@ -24,7 +24,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
let aliases = sqlx::query_as!(
Alias,
"SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
"SELECT name, command FROM command_aliases WHERE guild_id = ?",
guild_id.0
)
.fetch_all(&mut transaction)
@ -36,7 +36,7 @@ pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
match parse_text_command(guild_id, alias.name, &alias.command) {
Some(cmd_macro) => {
sqlx::query!(
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
"INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
cmd_macro.guild_id.0,
cmd_macro.name,
cmd_macro.description,

View File

@ -15,11 +15,23 @@ pub async fn record_macro(
#[description = "Name for the new macro"] name: String,
#[description = "Description for the new macro"] description: Option<String>,
) -> Result<(), Error> {
if name.len() > 100 {
ctx.say("Name must be less than 100 characters").await?;
return Ok(());
}
if description.as_ref().map_or(0, |d| d.len()) > 100 {
ctx.say("Description must be less than 100 characters").await?;
return Ok(());
}
let guild_id = ctx.guild_id().unwrap();
let row = sqlx::query!(
"
SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
SELECT 1 as _e FROM macro WHERE guild_id = ? AND name = ?",
guild_id.0,
name
)
@ -109,15 +121,15 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
let json = serde_json::to_string(&command_macro.commands).unwrap();
sqlx::query!(
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
"INSERT INTO macro (guild_id, name, description, commands) VALUES (?, ?, ?, ?)",
command_macro.guild_id.0,
command_macro.name,
command_macro.description,
json
)
.execute(&ctx.data().database)
.await
.unwrap();
.execute(&ctx.data().database)
.await
.unwrap();
ctx.send(|m| {
m.embed(|e| {

View File

@ -1,6 +1,8 @@
use chrono::offset::Utc;
use chrono_tz::{Tz, TZ_VARIANTS};
use levenshtein::levenshtein;
use log::warn;
use poise::serenity_prelude::{ChannelId, Mentionable};
use super::autocomplete::timezone_autocomplete;
use crate::{consts::THEME_COLOR, models::CtxData, Context, Error};
@ -147,33 +149,74 @@ pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// View the webhook being used to send reminders to this channel
/// Set defaults for commands
#[poise::command(
slash_command,
identifying_name = "webhook_url",
required_permissions = "ADMINISTRATOR"
identifying_name = "default",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
match ctx.channel_data().await {
Ok(data) => {
if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
let _ = ctx
.send(|b| {
b.ephemeral(true).content(format!(
"**Warning!**
This link can be used by users to anonymously send messages, with or without permissions.
Do not share it!
|| https://discord.com/api/webhooks/{}/{} ||",
id, token,
))
})
.await;
} else {
let _ = ctx.say("No webhook configured on this channel.").await;
}
}
Err(_) => {
let _ = ctx.say("No webhook configured on this channel.").await;
pub async fn default(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Set a default channel for reminders to be sent to
#[poise::command(
slash_command,
guild_only = true,
identifying_name = "default_channel",
default_member_permissions = "MANAGE_GUILD"
)]
pub async fn default_channel(
ctx: Context<'_>,
#[description = "Channel to send reminders to by default"] channel: Option<ChannelId>,
) -> Result<(), Error> {
if let Some(mut guild_data) = ctx.guild_data().await {
guild_data.default_channel = channel.map(|c| c.0);
guild_data.commit_changes(&ctx.data().database).await?;
if let Some(channel) = channel {
ctx.send(|r| {
r.ephemeral(true).content(format!("Default channel set to {}", channel.mention()))
})
.await?;
} else {
ctx.send(|r| r.ephemeral(true).content("Default channel unset.")).await?;
}
}
Ok(())
}
/// View the webhook being used to send reminders to this channel
#[poise::command(
slash_command,
identifying_name = "webhook_url",
required_permissions = "ADMINISTRATOR",
default_member_permissions = "ADMINISTRATOR"
)]
pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
match ctx.channel_data().await {
Ok(data) => {
if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
ctx.send(|b| {
b.ephemeral(true).content(format!(
"**Warning!**
This link can be used by users to anonymously send messages, with or without permissions.
Do not share it!
|| https://discord.com/api/webhooks/{}/{} ||",
id, token,
))
})
.await?;
} else {
ctx.say("No webhook configured on this channel.").await?;
}
}
Err(e) => {
warn!("Error fetching channel data: {:?}", e);
ctx.say("No webhook configured on this channel.").await?;
}
}

View File

@ -11,11 +11,13 @@ use poise::{
serenity_prelude::{
builder::CreateEmbed, component::ButtonStyle, model::channel::Channel, ReactionType,
},
AutocompleteChoice, CreateReply, Modal,
CreateReply, Modal,
};
use super::autocomplete::timezone_autocomplete;
use crate::{
commands::autocomplete::{
multiline_autocomplete, time_hint_autocomplete, timezone_autocomplete,
},
component_models::{
pager::{DelPager, LookPager, Pager},
ComponentDataModel, DelSelector, UndoReminder,
@ -550,20 +552,6 @@ pub async fn delete_timer(
Ok(())
}
async fn multiline_autocomplete(
_ctx: Context<'_>,
partial: &str,
) -> Vec<AutocompleteChoice<String>> {
if partial.is_empty() {
vec![AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() }]
} else {
vec![
AutocompleteChoice { name: partial.to_string(), value: partial.to_string() },
AutocompleteChoice { name: "Multiline content...".to_string(), value: "".to_string() },
]
}
}
#[derive(poise::Modal)]
#[name = "Reminder"]
struct ContentModal {
@ -574,7 +562,7 @@ struct ContentModal {
content: String,
}
/// Create a reminder. Press "+5 more" for other options. A modal will open if "content" is not provided
/// Create a reminder. Press "+4 more" for other options.
#[poise::command(
slash_command,
identifying_name = "remind",
@ -582,7 +570,9 @@ struct ContentModal {
)]
pub async fn remind(
ctx: ApplicationContext<'_>,
#[description = "A description of the time to set the reminder for"] time: String,
#[description = "A description of the time to set the reminder for"]
#[autocomplete = "time_hint_autocomplete"]
time: String,
#[description = "The message content to send"]
#[autocomplete = "multiline_autocomplete"]
content: String,
@ -663,7 +653,9 @@ async fn create_reminder(
let list = channels.map(|arg| parse_mention_list(&arg)).unwrap_or_default();
if list.is_empty() {
if ctx.guild_id().is_some() {
if let Some(channel_id) = ctx.default_channel().await {
vec![ReminderScope::Channel(channel_id.0)]
} else if ctx.guild_id().is_some() {
vec![ReminderScope::Channel(ctx.channel_id().0)]
} else {
vec![ReminderScope::User(ctx.author().id.0)]

View File

@ -47,7 +47,7 @@ pub async fn todo_guild_add(
) -> Result<(), Error> {
sqlx::query!(
"INSERT INTO todos (guild_id, value)
VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
VALUES (?, ?)",
ctx.guild_id().unwrap().0,
task
)
@ -70,9 +70,7 @@ VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
)]
pub async fn todo_guild_view(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 todos.id, value FROM todos WHERE guild_id = ?",
ctx.guild_id().unwrap().0,
)
.fetch_all(&ctx.data().database)
@ -122,7 +120,7 @@ pub async fn todo_channel_add(
sqlx::query!(
"INSERT INTO todos (guild_id, channel_id, value)
VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)",
ctx.guild_id().unwrap().0,
ctx.channel_id().0,
task

View File

@ -222,9 +222,7 @@ WHERE channels.channel = ?",
.collect::<Vec<(usize, String)>>()
} else {
sqlx::query!(
"SELECT todos.id, value FROM todos
INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
"SELECT todos.id, value FROM todos WHERE guild_id = ?",
pager.guild_id,
)
.fetch_all(&data.database)

View File

@ -2,7 +2,7 @@ pub const DAY: u64 = 86_400;
pub const HOUR: u64 = 3_600;
pub const MINUTE: u64 = 60;
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4096;
pub const SELECT_MAX_ENTRIES: usize = 25;
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";

View File

@ -6,7 +6,9 @@ use poise::{
serenity_prelude::{model::application::interaction::Interaction, utils::shard_id},
};
use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR};
use crate::{
component_models::ComponentDataModel, models::guild_data::GuildData, Data, Error, THEME_COLOR,
};
pub async fn listener(
ctx: &serenity::Context,
@ -27,7 +29,7 @@ pub async fn listener(
if *is_new {
let guild_id = guild.id.as_u64().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?;
@ -61,15 +63,27 @@ To stay up to date on the latest features and fixes, join our [Discord](https://
}
}
poise::Event::GuildDelete { incomplete, .. } => {
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
let _ = sqlx::query!("DELETE FROM guilds WHERE id = ?", incomplete.id.0)
.execute(&data.database)
.await;
}
poise::Event::InteractionCreate { interaction } => {
if let Interaction::MessageComponent(component) = interaction {
let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
match interaction {
Interaction::ApplicationCommand(app_command) => {
if let Some(guild_id) = app_command.guild_id {
// check database guild exists
GuildData::from_guild(guild_id, &data.database).await?;
}
}
component_model.act(ctx, data, component).await;
Interaction::MessageComponent(component) => {
let component_model =
ComponentDataModel::from_custom_id(&component.data.custom_id);
component_model.act(ctx, data, component).await;
}
_ => {}
}
}
_ => {}

View File

@ -1,36 +1,42 @@
use poise::serenity_prelude::model::channel::Channel;
use poise::{
serenity_prelude::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction,
};
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
async fn macro_check(ctx: Context<'_>) -> bool {
if let Context::Application(app_ctx) = ctx {
if let Some(guild_id) = ctx.guild_id() {
if ctx.command().identifying_name != "finish_macro" {
let mut lock = ctx.data().recording_macros.write().await;
if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) =
app_ctx.interaction
{
if let Some(guild_id) = ctx.guild_id() {
if ctx.command().identifying_name != "finish_macro" {
let mut lock = ctx.data().recording_macros.write().await;
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
let _ = ctx.send(|m| {
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
let _ = ctx.send(|m| {
m.ephemeral(true).content(
format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
)
})
.await;
} else {
let recorded = RecordedCommand {
action: None,
command_name: ctx.command().identifying_name.clone(),
options: Vec::from(app_ctx.args),
};
} else {
let recorded = RecordedCommand {
action: None,
command_name: ctx.command().identifying_name.clone(),
options: Vec::from(app_ctx.args),
};
command_macro.commands.push(recorded);
command_macro.commands.push(recorded);
let _ = ctx
.send(|m| m.ephemeral(true).content("Command recorded to macro"))
.await;
let _ = ctx
.send(|m| m.ephemeral(true).content("Command recorded to macro"))
.await;
}
return false;
}
return false;
}
}
}

View File

@ -120,6 +120,10 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
],
..command_macro::macro_base()
},
poise::Command {
subcommands: vec![moderation_cmds::default_channel()],
..moderation_cmds::default()
},
reminder_cmds::pause(),
reminder_cmds::offset(),
reminder_cmds::nudge(),
@ -168,7 +172,12 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
let popular_timezones = sqlx::query!(
"SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
"SELECT IFNULL(timezone, 'UTC') AS timezone
FROM users
WHERE timezone IS NOT NULL
GROUP BY timezone
ORDER BY COUNT(timezone) DESC
LIMIT 21"
)
.fetch_all(&database)
.await

View File

@ -38,7 +38,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
sqlx::query!(
"
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, ?)
",
channel_id,
channel_name,

View File

@ -43,7 +43,7 @@ pub async fn guild_command_macro(
) -> Option<CommandMacro<Data, Error>> {
let row = sqlx::query!(
"
SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
SELECT * FROM macro WHERE guild_id = ? AND name = ?
",
ctx.guild_id().unwrap().0,
name

52
src/models/guild_data.rs Normal file
View File

@ -0,0 +1,52 @@
use sqlx::MySqlPool;
use crate::GuildId;
pub struct GuildData {
pub id: u64,
pub default_channel: Option<u64>,
}
impl GuildData {
pub async fn from_guild(guild: GuildId, pool: &MySqlPool) -> Result<Self, sqlx::Error> {
let guild_id = guild.0;
if let Ok(row) = sqlx::query_as_unchecked!(
Self,
"
SELECT id, default_channel FROM guilds WHERE id = ?
",
guild_id
)
.fetch_one(pool)
.await
{
Ok(row)
} else {
sqlx::query!(
"
INSERT IGNORE INTO guilds (id) VALUES (?)
",
guild_id
)
.execute(&pool.clone())
.await?;
Ok(Self { id: guild_id, default_channel: None })
}
}
pub async fn commit_changes(&self, pool: &MySqlPool) -> Result<(), sqlx::Error> {
sqlx::query!(
"
UPDATE guilds SET default_channel = ? WHERE id = ?
",
self.default_channel,
self.id
)
.execute(pool)
.await?;
Ok(())
}
}

View File

@ -1,28 +1,28 @@
pub mod channel_data;
pub mod command_macro;
pub mod guild_data;
pub mod reminder;
pub mod timer;
pub mod user_data;
use chrono_tz::Tz;
use poise::serenity_prelude::{async_trait, model::id::UserId};
use log::warn;
use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelId};
use crate::{
models::{channel_data::ChannelData, user_data::UserData},
models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData},
CommandMacro, Context, Data, Error, GuildId,
};
#[async_trait]
pub trait CtxData {
async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
async fn author_data(&self) -> Result<UserData, Error>;
async fn timezone(&self) -> Tz;
async fn channel_data(&self) -> Result<ChannelData, Error>;
async fn guild_data(&self) -> Option<GuildData>;
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
async fn default_channel(&self) -> Option<ChannelId>;
}
#[async_trait]
@ -51,24 +51,55 @@ impl CtxData for Context<'_> {
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
self.data().command_macros(self.guild_id().unwrap()).await
}
async fn default_channel(&self) -> Option<ChannelId> {
match self.guild_id() {
Some(guild_id) => {
let guild_data = GuildData::from_guild(guild_id, &self.data().database).await;
match guild_data {
Ok(data) => data.default_channel.map(|c| ChannelId(c)),
Err(e) => {
warn!("SQL error: {:?}", e);
None
}
}
}
None => None,
}
}
async fn guild_data(&self) -> Option<GuildData> {
match self.guild_id() {
Some(guild_id) => GuildData::from_guild(guild_id, &self.data().database).await.ok(),
None => None,
}
}
}
impl Data {
pub(crate) async fn command_macros(
pub async fn command_macros(
&self,
guild_id: GuildId,
) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
let rows = sqlx::query!(
"SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
"SELECT name, description, commands FROM macro WHERE guild_id = ?",
guild_id.0
)
.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).unwrap(),
}).collect();
})
.collect();
Ok(rows)
}

View File

@ -245,7 +245,7 @@ LEFT JOIN
ON
reminders.set_by = users.id
WHERE
channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
channels.guild_id = ?
",
guild_id.as_u64()
)

View File

@ -22,7 +22,7 @@ impl UserData {
match sqlx::query!(
"
SELECT timezone FROM users WHERE user = ?
SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
",
user_id
)

View File

@ -61,10 +61,13 @@ pub async fn get_user_info(
.member(&ctx.inner(), user_id)
.await;
let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
.fetch_one(pool.inner())
.await
.map_or(None, |q| Some(q.timezone));
let timezone = sqlx::query!(
"SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
user_id
)
.fetch_one(pool.inner())
.await
.map_or(None, |q| Some(q.timezone));
let user_info = UserInfo {
name: cookies