component models

This commit is contained in:
jude 2022-02-19 22:11:21 +00:00
parent afc376c44f
commit 06c4deeaa9
15 changed files with 269 additions and 184 deletions

View File

@ -1,23 +1,23 @@
mod sender;
use std::env;
use log::info;
use serenity::client::Context;
use sqlx::{Executor, MySql};
use std::env;
use tokio::time::sleep_until;
use tokio::time::{Duration, Instant};
use tokio::time::{sleep_until, Duration, Instant};
type Database = MySql;
pub async fn initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
let REMIND_INTERVAL = env::var("REMIND_INTERVAL")
let remind_interval = env::var("REMIND_INTERVAL")
.map(|inner| inner.parse::<u64>().ok())
.ok()
.flatten()
.unwrap_or(10);
loop {
let sleep_to = Instant::now() + Duration::from_secs(REMIND_INTERVAL);
let sleep_to = Instant::now() + Duration::from_secs(remind_interval);
let reminders = sender::Reminder::fetch_reminders(pool).await;
if reminders.len() > 0 {

View File

@ -1,4 +1,3 @@
use crate::Database;
use chrono::Duration;
use chrono_tz::Tz;
use lazy_static::lazy_static;
@ -20,6 +19,8 @@ use sqlx::{
Executor,
};
use crate::Database;
lazy_static! {
pub static ref TIMEFROM_REGEX: Regex =
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();

View File

@ -1,9 +1,11 @@
use chrono::offset::Utc;
use poise::serenity::builder::CreateEmbedFooter;
use poise::{serenity_prelude as serenity, serenity_prelude::Mentionable};
use crate::{models::CtxData, Context, Error, THEME_COLOR};
fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
fn footer(
ctx: Context<'_>,
) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter {
let shard_count = ctx.discord().cache.shard_count();
let shard = ctx.discord().shard_id;
@ -22,9 +24,8 @@ fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Creat
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
let _ = ctx
.send(|m| {
m.embed(|e| {
ctx.send(|m| {
m.ephemeral(true).embed(|e| {
e.title("Help")
.color(*THEME_COLOR)
.description(
@ -56,7 +57,7 @@ __Advanced Commands__
.footer(footer)
})
})
.await;
.await?;
Ok(())
}
@ -68,7 +69,7 @@ pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
let _ = ctx
.send(|m| {
m.embed(|e| {
m.ephemeral(true).embed(|e| {
e.title("Info")
.description(format!(
"Help: `/help`
@ -95,9 +96,10 @@ Use our dashboard: https://reminder-bot.com/",
pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
let _ = ctx.send(|m| m.embed(|e| {
ctx.send(|m| m.embed(|e| {
e.title("Donate")
.description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
.description("Thinking of adding a monthly contribution?
Click below for my Patreon and official bot server :)
**https://www.patreon.com/jellywx/**
**https://discord.jellywx.com/**
@ -116,7 +118,7 @@ Just $2 USD/month!
.color(*THEME_COLOR)
}),
)
.await;
.await?;
Ok(())
}
@ -126,21 +128,20 @@ Just $2 USD/month!
pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
let footer = footer(ctx);
let _ = ctx
.send(|m| {
m.embed(|e| {
ctx.send(|m| {
m.ephemeral(true).embed(|e| {
e.title("Dashboard")
.description("**https://reminder-bot.com/dashboard**")
.footer(footer)
.color(*THEME_COLOR)
})
})
.await;
.await?;
Ok(())
}
/// View the current time in a user's selected timezone
/// View the current time in your selected timezone
#[poise::command(slash_command)]
pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
@ -155,3 +156,25 @@ pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// View the current time in a user's selected timezone
#[poise::command(context_menu_command = "View Local Time")]
pub async fn clock_context_menu(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> {
ctx.defer_ephemeral().await?;
let user_data = ctx.user_data(user.id).await?;
let tz = user_data.timezone();
let now = Utc::now().with_timezone(&tz);
ctx.send(|m| {
m.ephemeral(true).content(format!(
"Time in {}'s timezone: `{}`",
user.mention(),
now.format("%H:%M")
))
})
.await?;
Ok(())
}

View File

@ -9,7 +9,6 @@ use chrono_tz::Tz;
use num_integer::Integer;
use poise::{
serenity::{builder::CreateEmbed, model::channel::Channel},
serenity_prelude::ActionRole::Create,
CreateReply,
};
@ -32,7 +31,6 @@ use crate::{
Reminder,
},
timer::Timer,
user_data::UserData,
CtxData,
},
time_parser::natural_parser,
@ -212,7 +210,7 @@ pub async fn look(
None
};
let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await;
let reminders = Reminder::from_channel(&ctx.data().database, channel_id, &flags).await;
if reminders.is_empty() {
let _ = ctx.say("No reminders on specified channel").await;
@ -266,7 +264,9 @@ pub async fn look(
pub async fn delete(ctx: Context<'_>) -> Result<(), Error> {
let timezone = ctx.timezone().await;
let reminders = Reminder::from_guild(&ctx, ctx.guild_id(), ctx.author().id).await;
let reminders =
Reminder::from_guild(&ctx.discord(), &ctx.data().database, ctx.guild_id(), ctx.author().id)
.await;
let resp = show_delete_page(&reminders, 0, timezone);

View File

@ -6,6 +6,7 @@ use crate::{
ComponentDataModel, TodoSelector,
},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
hooks::guild_only,
Context, Error,
};
@ -16,7 +17,7 @@ pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> {
}
/// Manage the server todo list
#[poise::command(slash_command, rename = "server")]
#[poise::command(slash_command, rename = "server", check = "guild_only")]
pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
@ -70,7 +71,7 @@ WHERE guilds.guild = ?",
}
/// Manage the channel todo list
#[poise::command(slash_command, rename = "channel")]
#[poise::command(slash_command, rename = "channel", check = "guild_only")]
pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}

View File

@ -5,6 +5,7 @@ use std::io::Cursor;
use chrono_tz::Tz;
use poise::serenity::{
builder::CreateEmbed,
client::Context,
model::{
channel::Channel,
interactions::{message_component::MessageComponentInteraction, InteractionResponseType},
@ -22,8 +23,9 @@ use crate::{
},
component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
models::{reminder::Reminder, CtxData},
Context, Data,
models::reminder::Reminder,
utils::send_as_initial_response,
Data,
};
#[derive(Deserialize, Serialize)]
@ -53,12 +55,12 @@ impl ComponentDataModel {
rmp_serde::from_read(cur).unwrap()
}
pub async fn act(&self, ctx: Context<'_>, component: &MessageComponentInteraction) {
pub async fn act(&self, ctx: &Context, data: &Data, component: &MessageComponentInteraction) {
match self {
ComponentDataModel::LookPager(pager) => {
let flags = pager.flags;
let channel_opt = component.channel_id.to_channel_cached(&ctx.discord());
let channel_opt = component.channel_id.to_channel_cached(&ctx);
let channel_id = if let Some(Channel::Guild(channel)) = channel_opt {
if Some(channel.guild_id) == component.guild_id {
@ -70,7 +72,7 @@ impl ComponentDataModel {
component.channel_id
};
let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await;
let reminders = Reminder::from_channel(&data.database, channel_id, &flags).await;
let pages = reminders
.iter()
@ -78,9 +80,8 @@ impl ComponentDataModel {
.fold(0, |t, r| t + r.len())
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
let channel_name = if let Some(Channel::Guild(channel)) =
channel_id.to_channel_cached(&ctx.discord())
{
let channel_name =
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
Some(channel.name)
} else {
None
@ -118,7 +119,7 @@ impl ComponentDataModel {
.color(*THEME_COLOR);
let _ = component
.create_interaction_response(&ctx.discord(), |r| {
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
|response| {
response.embeds(vec![embed]).components(|comp| {
@ -132,17 +133,26 @@ impl ComponentDataModel {
.await;
}
ComponentDataModel::DelPager(pager) => {
let reminders =
Reminder::from_guild(&ctx, component.guild_id, component.user.id).await;
let reminders = Reminder::from_guild(
&ctx,
&data.database,
component.guild_id,
component.user.id,
)
.await;
let max_pages = max_delete_page(&reminders, &pager.timezone);
let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
let _ = ctx
.send(|r| {
*r = resp;
r
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
|d| {
send_as_initial_response(resp, d);
d
},
)
})
.await;
}
@ -150,19 +160,28 @@ impl ComponentDataModel {
let selected_id = component.data.values.join(",");
sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
.execute(&ctx.data().database)
.execute(&data.database)
.await
.unwrap();
let reminders =
Reminder::from_guild(&ctx, component.guild_id, component.user.id).await;
let reminders = Reminder::from_guild(
&ctx,
&data.database,
component.guild_id,
component.user.id,
)
.await;
let resp = show_delete_page(&reminders, selector.page, selector.timezone);
let _ = ctx
.send(|r| {
*r = resp;
r
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
|d| {
send_as_initial_response(resp, d);
d
},
)
})
.await;
}
@ -175,7 +194,7 @@ INNER JOIN users ON todos.user_id = users.id
WHERE users.user = ?",
uid,
)
.fetch_all(&ctx.data().database)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
@ -188,7 +207,7 @@ INNER JOIN channels ON todos.channel_id = channels.id
WHERE channels.channel = ?",
cid,
)
.fetch_all(&ctx.data().database)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
@ -201,7 +220,7 @@ INNER JOIN guilds ON todos.guild_id = guilds.id
WHERE guilds.guild = ?",
pager.guild_id,
)
.fetch_all(&ctx.data().database)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
@ -219,15 +238,18 @@ WHERE guilds.guild = ?",
pager.guild_id,
);
let _ = ctx
.send(|r| {
*r = resp;
r
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage)
.interaction_response_data(|d| {
send_as_initial_response(resp, d);
d
})
})
.await;
} else {
let _ = component
.create_interaction_response(&ctx.discord(), |r| {
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.flags(
@ -244,7 +266,7 @@ WHERE guilds.guild = ?",
let selected_id = component.data.values.join(",");
sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
.execute(&ctx.data().database)
.execute(&data.database)
.await
.unwrap();
@ -255,7 +277,7 @@ WHERE guilds.guild = ?",
selector.channel_id,
selector.guild_id,
)
.fetch_all(&ctx.data().database)
.fetch_all(&data.database)
.await
.unwrap()
.iter()
@ -270,15 +292,18 @@ WHERE guilds.guild = ?",
selector.guild_id,
);
let _ = ctx
.send(|r| {
*r = resp;
r
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage)
.interaction_response_data(|d| {
send_as_initial_response(resp, d);
d
})
})
.await;
} else {
let _ = component
.create_interaction_response(&ctx.discord(), |r| {
.create_interaction_response(&ctx, |r| {
r.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|d| {
d.flags(
@ -291,17 +316,21 @@ WHERE guilds.guild = ?",
}
}
ComponentDataModel::MacroPager(pager) => {
let macros = ctx.command_macros().await.unwrap();
let macros = data.command_macros(component.guild_id.unwrap()).await.unwrap();
let max_page = max_macro_page(&macros);
let page = pager.next_page(max_page);
let resp = show_macro_page(&macros, page);
let _ = ctx
.send(|r| {
*r = resp;
r
let _ = component
.create_interaction_response(&ctx, |f| {
f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
|d| {
send_as_initial_response(resp, d);
d
},
)
})
.await;
}

View File

@ -1,21 +1,12 @@
use std::{
collections::HashMap,
env,
sync::atomic::{AtomicBool, Ordering},
};
use std::{collections::HashMap, env, sync::atomic::Ordering};
use log::{info, warn};
use poise::{
serenity::{model::interactions::Interaction, utils::shard_id},
serenity_prelude as serenity,
serenity_prelude::{
ApplicationCommandInteraction, ApplicationCommandInteractionData, ApplicationCommandType,
InteractionType,
},
ApplicationCommandOrAutocompleteInteraction, ApplicationContext, Command,
};
use crate::{component_models::ComponentDataModel, Context, Data, Error};
use crate::{component_models::ComponentDataModel, Data, Error};
pub async fn listener(
ctx: &serenity::Context,
@ -56,12 +47,7 @@ pub async fn listener(
}
}
poise::Event::ChannelDelete { channel } => {
sqlx::query!(
"
DELETE FROM channels WHERE channel = ?
",
channel.id.as_u64()
)
sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.as_u64())
.execute(&data.database)
.await
.unwrap();
@ -122,7 +108,7 @@ DELETE FROM channels WHERE channel = ?
Interaction::MessageComponent(component) => {
let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
// component_model.act(ctx, component).await;
component_model.act(ctx, data, component).await;
}
_ => {}
},

View File

@ -1,4 +1,4 @@
use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction};
use poise::serenity::model::channel::Channel;
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
@ -14,9 +14,6 @@ pub async fn guild_only(ctx: Context<'_>) -> Result<bool, Error> {
async fn macro_check(ctx: Context<'_>) -> bool {
if let Context::Application(app_ctx) = ctx {
if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(interaction) =
app_ctx.interaction
{
if let Some(guild_id) = ctx.guild_id() {
if ctx.command().identifying_name != "macro_finish" {
let mut lock = ctx.data().recording_macros.write().await;
@ -56,9 +53,6 @@ async fn macro_check(ctx: Context<'_>) -> bool {
} else {
true
}
} else {
true
}
}
async fn check_self_permissions(ctx: Context<'_>) -> bool {

View File

@ -59,6 +59,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info_cmds::info(),
info_cmds::donate(),
info_cmds::clock(),
info_cmds::clock_context_menu(),
info_cmds::dashboard(),
moderation_cmds::timezone(),
poise::Command {

View File

@ -9,7 +9,7 @@ use poise::serenity::{async_trait, model::id::UserId};
use crate::{
models::{channel_data::ChannelData, user_data::UserData},
CommandMacro, Context, Data, Error,
CommandMacro, Context, Data, Error, GuildId,
};
#[async_trait]
@ -49,13 +49,20 @@ impl CtxData for Context<'_> {
}
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
let guild_id = self.guild_id().unwrap();
self.data().command_macros(self.guild_id().unwrap()).await
}
}
impl Data {
pub(crate) async fn command_macros(
&self,
guild_id: GuildId,
) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
let rows = sqlx::query!(
"SELECT name, description FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
guild_id.0
)
.fetch_all(&self.data().database)
.fetch_all(&self.database)
.await?.iter().map(|row| CommandMacro {
guild_id,
name: row.name.clone(),

View File

@ -6,12 +6,15 @@ pub mod look_flags;
use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Tz;
use poise::serenity::model::id::{ChannelId, GuildId, UserId};
use poise::{
serenity::model::id::{ChannelId, GuildId, UserId},
serenity_prelude::Cache,
};
use sqlx::Executor;
use crate::{
models::reminder::look_flags::{LookFlags, TimeDisplayType},
Context, Data, Database,
Database,
};
#[derive(Debug, Clone)]
@ -70,7 +73,7 @@ WHERE
}
pub async fn from_channel<C: Into<ChannelId>>(
ctx: &Context<'_>,
pool: impl Executor<'_, Database = Database>,
channel_id: C,
flags: &LookFlags,
) -> Vec<Self> {
@ -111,18 +114,19 @@ ORDER BY
channel_id.as_u64(),
enabled,
)
.fetch_all(&ctx.data().database)
.fetch_all(pool)
.await
.unwrap()
}
pub async fn from_guild(
ctx: &Context<'_>,
cache: impl AsRef<Cache>,
pool: impl Executor<'_, Database = Database>,
guild_id: Option<GuildId>,
user: UserId,
) -> Vec<Self> {
if let Some(guild_id) = guild_id {
let guild_opt = guild_id.to_guild_cached(&ctx.discord());
let guild_opt = guild_id.to_guild_cached(cache);
if let Some(guild) = guild_opt {
let channels = guild
@ -163,7 +167,7 @@ WHERE
",
channels
)
.fetch_all(&ctx.data().database)
.fetch_all(pool)
.await
} else {
sqlx::query_as_unchecked!(
@ -196,7 +200,7 @@ WHERE
",
guild_id.as_u64()
)
.fetch_all(&ctx.data().database)
.fetch_all(pool)
.await
}
} else {
@ -230,7 +234,7 @@ WHERE
",
user.as_u64()
)
.fetch_all(&ctx.data().database)
.fetch_all(pool)
.await
}
.unwrap()

View File

@ -1,7 +1,10 @@
use poise::serenity::{
use poise::{
serenity::{
builder::CreateApplicationCommands,
http::CacheHttp,
model::id::{GuildId, UserId},
},
serenity_prelude as serenity,
};
use crate::{
@ -65,3 +68,40 @@ pub async fn check_guild_subscription(
false
}
}
/// Sends the message, specified via [`crate::CreateReply`], to the interaction initial response
/// endpoint
pub fn send_as_initial_response(
data: poise::CreateReply<'_>,
f: &mut serenity::CreateInteractionResponseData,
) {
let poise::CreateReply {
content,
embeds,
attachments: _, // serenity doesn't support attachments in initial response yet
components,
ephemeral,
allowed_mentions,
reference_message: _, // can't reply to a message in interactions
} = data;
if let Some(content) = content {
f.content(content);
}
f.embeds(embeds);
if let Some(allowed_mentions) = allowed_mentions {
f.allowed_mentions(|f| {
*f = allowed_mentions.clone();
f
});
}
if let Some(components) = components {
f.components(|f| {
f.0 = components.0;
f
});
}
if ephemeral {
f.flags(serenity::InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
}
}

View File

@ -4,17 +4,15 @@ extern crate rocket;
mod consts;
mod routes;
use rocket::fs::{relative, FileServer};
use std::collections::HashMap;
use std::{collections::HashMap, env};
use oauth2::basic::BasicClient;
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use crate::consts::{DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN};
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
use rocket::fs::FileServer;
use rocket_dyn_templates::Template;
use serenity::client::Context;
use sqlx::{MySql, Pool};
use std::env;
use crate::consts::{DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN};
type Database = MySql;

View File

@ -1,14 +1,13 @@
use rocket::State;
use crate::consts::DISCORD_CDN;
use rocket::{
serde::json::{json, Json, Value as JsonValue},
State,
};
use serde::Serialize;
use serenity::{client::Context, model::id::GuildId};
use sqlx::{MySql, Pool};
use super::Reminder;
use rocket::serde::json::{json, Json, Value as JsonValue};
use serenity::client::Context;
use serenity::http::CacheHttp;
use serenity::model::id::GuildId;
use crate::consts::DISCORD_CDN;
#[derive(Serialize)]
struct ChannelInfo {

View File

@ -1,22 +1,24 @@
use rocket::serde::json::{json, Json, Value as JsonValue};
use rocket::{http::CookieJar, State};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serenity::model::{
id::{GuildId, RoleId},
permissions::Permissions,
};
use sqlx::{MySql, Pool};
use std::env;
use super::Reminder;
use crate::consts::DISCORD_API;
use crate::routes::dashboard::DeleteReminder;
use chrono_tz::Tz;
use serenity::client::Context;
use serenity::model::id::UserId;
use reqwest::Client;
use rocket::{
http::CookieJar,
serde::json::{json, Json, Value as JsonValue},
State,
};
use serde::{Deserialize, Serialize};
use serenity::{
client::Context,
model::{
id::{GuildId, RoleId, UserId},
permissions::Permissions,
},
};
use sqlx::{MySql, Pool};
use super::Reminder;
use crate::{consts::DISCORD_API, routes::dashboard::DeleteReminder};
#[derive(Serialize)]
struct UserInfo {
@ -166,7 +168,7 @@ pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Cli
#[post("/api/user/reminders", data = "<reminder>")]
pub async fn create_reminder(
reminder: Json<Reminder>,
ctx: &State<Context>,
_ctx: &State<Context>,
pool: &State<Pool<MySql>>,
) -> JsonValue {
match sqlx::query!(