Files
reminder-bot/src/models/reminder/builder.rs
T
2026-05-21 20:09:06 +01:00

326 lines
11 KiB
Rust

use std::collections::HashSet;
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use poise::serenity_prelude::{
model::id::{ChannelId, GuildId, UserId},
ChannelType,
};
use sqlx::MySqlPool;
use crate::{
consts::{DAY, MAX_TIME, MIN_INTERVAL},
interval_parser::Interval,
models::{
channel_data::ChannelData,
reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
user_data::UserData,
},
Context,
};
#[derive(Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelWithThread {
pub channel_id: u64,
pub thread_id: Option<u64>,
}
#[derive(Hash, PartialEq, Eq)]
pub enum ReminderScope {
User(u64),
Channel(ChannelWithThread),
}
impl ReminderScope {
pub fn mention(&self) -> String {
match self {
Self::User(id) => format!("<@{}>", id),
Self::Channel(c) => format!("<#{}>", c.channel_id),
}
}
}
pub struct ReminderBuilder {
pool: MySqlPool,
uid: String,
channel: u32,
thread_id: Option<u64>,
utc_time: NaiveDateTime,
timezone: String,
interval_seconds: Option<i64>,
interval_days: Option<i64>,
interval_months: Option<i64>,
expires: Option<NaiveDateTime>,
content: String,
tts: bool,
attachment_name: Option<String>,
attachment: Option<Vec<u8>>,
set_by: Option<u64>,
}
impl ReminderBuilder {
pub async fn build(self) -> Result<Reminder, ReminderError> {
let queried_time = sqlx::query!(
"
SELECT DATE_ADD(?, INTERVAL (SELECT nudge FROM channels WHERE id = ?) SECOND) AS `utc_time`
",
self.utc_time,
self.channel,
)
.fetch_one(&self.pool)
.await
.unwrap();
match queried_time.utc_time {
Some(utc_time) => {
if utc_time < (Utc::now() - TimeDelta::try_minutes(1).unwrap()).naive_local() {
Err(ReminderError::PastTime)
} else {
sqlx::query!(
"
INSERT INTO reminders (
`uid`,
`channel_id`,
`thread_id`,
`utc_time`,
`timezone`,
`interval_seconds`,
`interval_days`,
`interval_months`,
`expires`,
`content`,
`tts`,
`attachment_name`,
`attachment`,
`set_by`
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
",
self.uid,
self.channel,
self.thread_id,
utc_time,
self.timezone,
self.interval_seconds,
self.interval_days,
self.interval_months,
self.expires,
self.content,
self.tts,
self.attachment_name,
self.attachment,
self.set_by
)
.execute(&self.pool)
.await
.unwrap();
Ok(Reminder::from_uid(&self.pool, &self.uid).await.unwrap())
}
}
None => Err(ReminderError::LongTime),
}
}
}
pub struct MultiReminderBuilder<'a> {
scopes: Vec<ReminderScope>,
utc_time: NaiveDateTime,
timezone: Tz,
interval: Option<Interval>,
expires: Option<NaiveDateTime>,
content: Content,
set_by: Option<u64>,
ctx: &'a Context<'a>,
guild_id: Option<GuildId>,
}
impl<'a> MultiReminderBuilder<'a> {
pub fn new(ctx: &'a Context, guild_id: Option<GuildId>) -> Self {
MultiReminderBuilder {
scopes: vec![],
utc_time: Utc::now().naive_utc(),
timezone: Tz::UTC,
interval: None,
expires: None,
content: Content::new(),
set_by: None,
ctx,
guild_id,
}
}
pub fn timezone(mut self, timezone: Tz) -> Self {
self.timezone = timezone;
self
}
pub fn content(mut self, content: Content) -> Self {
self.content = content;
self
}
pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
if let Some(utc_time) = DateTime::from_timestamp(time.into(), 0).map(|d| d.naive_utc()) {
self.utc_time = utc_time;
}
self
}
pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
self.expires =
time.map(|t| DateTime::from_timestamp(t.into(), 0)).flatten().map(|d| d.naive_utc());
self
}
pub fn author(mut self, user: UserData) -> Self {
self.set_by = Some(user.id);
self.timezone = user.timezone();
self
}
pub fn interval(mut self, interval: Option<Interval>) -> Self {
self.interval = interval;
self
}
pub fn set_scopes(&mut self, scopes: Vec<ReminderScope>) {
self.scopes = scopes;
}
pub async fn build(self) -> (HashSet<ReminderError>, HashSet<(Reminder, ReminderScope)>) {
let mut errors = HashSet::new();
let mut ok_locs = HashSet::new();
if self
.interval
.map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) < *MIN_INTERVAL)
{
errors.insert(ReminderError::ShortInterval);
} else if self
.interval
.map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) > *MAX_TIME)
{
errors.insert(ReminderError::LongInterval);
} else {
for scope in self.scopes {
let db_channel_id = match scope {
ReminderScope::User(user_id) => {
let user_id = UserId::new(user_id);
match UserData::from_user(
&user_id,
&self.ctx.serenity_context(),
&self.ctx.data().database,
)
.await
{
Ok(user_data) => {
if let Some(guild_id) = self.guild_id {
if guild_id.member(&self.ctx, user_id).await.is_err() {
Err(ReminderError::InvalidTag)
} else if self.set_by.map_or(true, |i| i != user_data.id)
&& !user_data.allowed_dm
{
Err(ReminderError::UserBlockedDm)
} else {
Ok((user_data.dm_channel, None))
}
} else {
Ok((user_data.dm_channel, None))
}
}
Err(_) => Err(ReminderError::InvalidTag),
}
}
ReminderScope::Channel(channel_with_thread) => {
let channel = ChannelId::new(channel_with_thread.channel_id)
.to_channel(&self.ctx)
.await
.unwrap();
if let Some(guild_channel) = channel.clone().guild() {
if Some(guild_channel.guild_id) != self.guild_id {
Err(ReminderError::InvalidTag)
} else {
let mut channel_data = if guild_channel.kind
== ChannelType::PublicThread
{
let parent = guild_channel
.parent_id
.unwrap()
.to_channel(&self.ctx)
.await
.unwrap();
ChannelData::from_channel(&parent, &self.ctx.data().database)
.await
.unwrap()
} else {
ChannelData::from_channel(&channel, &self.ctx.data().database)
.await
.unwrap()
};
match channel_data
.ensure_webhook(&self.ctx, &self.ctx.data().database)
.await
.map_err(|e| ReminderError::DiscordError(e.to_string()))
{
Ok(()) => Ok((channel_data.id, channel_with_thread.thread_id)),
Err(e) => Err(e),
}
}
} else {
Err(ReminderError::InvalidTag)
}
}
};
match db_channel_id {
Ok((channel, thread_id)) => {
let builder = ReminderBuilder {
pool: self.ctx.data().database.clone(),
uid: generate_uid(),
channel,
thread_id,
utc_time: self.utc_time,
timezone: self.timezone.to_string(),
interval_seconds: self.interval.map(|i| i.sec as i64),
interval_days: self.interval.map(|i| i.day as i64),
interval_months: self.interval.map(|i| i.month as i64),
expires: self.expires,
content: self.content.content.clone(),
tts: self.content.tts,
attachment_name: self.content.attachment_name.clone(),
attachment: self.content.attachment.clone(),
set_by: self.set_by,
};
match builder.build().await {
Ok(r) => {
ok_locs.insert((r, scope));
}
Err(e) => {
errors.insert(e);
}
}
}
Err(e) => {
errors.insert(e);
}
}
}
}
(errors, ok_locs)
}
}