Add routes for getting/posting user reminders
This commit is contained in:
		
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -942,6 +942,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "futures-channel",
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-executor",
 | 
				
			||||||
 "futures-io",
 | 
					 "futures-io",
 | 
				
			||||||
 "futures-sink",
 | 
					 "futures-sink",
 | 
				
			||||||
 "futures-task",
 | 
					 "futures-task",
 | 
				
			||||||
@@ -2366,6 +2367,7 @@ dependencies = [
 | 
				
			|||||||
 "dotenv",
 | 
					 "dotenv",
 | 
				
			||||||
 "env_logger",
 | 
					 "env_logger",
 | 
				
			||||||
 "extract_derive",
 | 
					 "extract_derive",
 | 
				
			||||||
 | 
					 "futures",
 | 
				
			||||||
 "lazy-regex",
 | 
					 "lazy-regex",
 | 
				
			||||||
 "lazy_static",
 | 
					 "lazy_static",
 | 
				
			||||||
 "levenshtein",
 | 
					 "levenshtein",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,7 @@ levenshtein = "1.0"
 | 
				
			|||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
 | 
				
			||||||
base64 = "0.21"
 | 
					base64 = "0.21"
 | 
				
			||||||
secrecy = "0.8.0"
 | 
					secrecy = "0.8.0"
 | 
				
			||||||
 | 
					futures = "0.3.30"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.postman]
 | 
					[dependencies.postman]
 | 
				
			||||||
path = "postman"
 | 
					path = "postman"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -182,3 +182,8 @@ export const fetchUserReminders = () => ({
 | 
				
			|||||||
        axios.get(`/dashboard/api/user/reminders`).then((resp) => resp.data) as Promise<Reminder[]>,
 | 
					        axios.get(`/dashboard/api/user/reminders`).then((resp) => resp.data) as Promise<Reminder[]>,
 | 
				
			||||||
    staleTime: OTHER_STALE_TIME,
 | 
					    staleTime: OTHER_STALE_TIME,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const postUserReminder = () => ({
 | 
				
			||||||
 | 
					    mutationFn: (reminder: Reminder) =>
 | 
				
			||||||
 | 
					        axios.post(`/dashboard/api/user/reminders`, reminder).then((resp) => resp.data),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,15 @@
 | 
				
			|||||||
import { LoadTemplate } from "../LoadTemplate";
 | 
					import { LoadTemplate } from "../LoadTemplate";
 | 
				
			||||||
import { useReminder } from "../ReminderContext";
 | 
					import { useReminder } from "../ReminderContext";
 | 
				
			||||||
import { useMutation, useQueryClient } from "react-query";
 | 
					import { useMutation, useQueryClient } from "react-query";
 | 
				
			||||||
import { postGuildReminder, postGuildTemplate } from "../../../api";
 | 
					import { postGuildReminder, postGuildTemplate, postUserReminder } from "../../../api";
 | 
				
			||||||
import { useParams } from "wouter";
 | 
					import { useParams } from "wouter";
 | 
				
			||||||
import { useState } from "preact/hooks";
 | 
					import { useState } from "preact/hooks";
 | 
				
			||||||
import { ICON_FLASH_TIME } from "../../../consts";
 | 
					import { ICON_FLASH_TIME } from "../../../consts";
 | 
				
			||||||
import { useFlash } from "../../App/FlashContext";
 | 
					import { useFlash } from "../../App/FlashContext";
 | 
				
			||||||
 | 
					import { useGuild } from "../../App/useGuild";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CreateButtonRow = () => {
 | 
					export const CreateButtonRow = () => {
 | 
				
			||||||
    const { guild } = useParams();
 | 
					    const guild = useGuild();
 | 
				
			||||||
    const [reminder] = useReminder();
 | 
					    const [reminder] = useReminder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [recentlyCreated, setRecentlyCreated] = useState(false);
 | 
					    const [recentlyCreated, setRecentlyCreated] = useState(false);
 | 
				
			||||||
@@ -17,7 +18,7 @@ export const CreateButtonRow = () => {
 | 
				
			|||||||
    const flash = useFlash();
 | 
					    const flash = useFlash();
 | 
				
			||||||
    const queryClient = useQueryClient();
 | 
					    const queryClient = useQueryClient();
 | 
				
			||||||
    const mutation = useMutation({
 | 
					    const mutation = useMutation({
 | 
				
			||||||
        ...postGuildReminder(guild),
 | 
					        ...(guild ? postGuildReminder(guild) : postUserReminder()),
 | 
				
			||||||
        onSuccess: (data) => {
 | 
					        onSuccess: (data) => {
 | 
				
			||||||
            if (data.error) {
 | 
					            if (data.error) {
 | 
				
			||||||
                flash({
 | 
					                flash({
 | 
				
			||||||
@@ -29,9 +30,15 @@ export const CreateButtonRow = () => {
 | 
				
			|||||||
                    message: "Reminder created",
 | 
					                    message: "Reminder created",
 | 
				
			||||||
                    type: "success",
 | 
					                    type: "success",
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                queryClient.invalidateQueries({
 | 
					                if (guild) {
 | 
				
			||||||
                    queryKey: ["GUILD_REMINDERS", guild],
 | 
					                    queryClient.invalidateQueries({
 | 
				
			||||||
                });
 | 
					                        queryKey: ["GUILD_REMINDERS", guild],
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    queryClient.invalidateQueries({
 | 
				
			||||||
 | 
					                        queryKey: ["USER_REMINDERS"],
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                setRecentlyCreated(true);
 | 
					                setRecentlyCreated(true);
 | 
				
			||||||
                setTimeout(() => {
 | 
					                setTimeout(() => {
 | 
				
			||||||
                    setRecentlyCreated(false);
 | 
					                    setRecentlyCreated(false);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import { ReminderContext } from "./ReminderContext";
 | 
				
			|||||||
import { useQuery } from "react-query";
 | 
					import { useQuery } from "react-query";
 | 
				
			||||||
import { useParams } from "wouter";
 | 
					import { useParams } from "wouter";
 | 
				
			||||||
import "./styles.scss";
 | 
					import "./styles.scss";
 | 
				
			||||||
 | 
					import { useGuild } from "../App/useGuild";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function defaultReminder(): Reminder {
 | 
					function defaultReminder(): Reminder {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
@@ -42,7 +43,7 @@ function defaultReminder(): Reminder {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CreateReminder = () => {
 | 
					export const CreateReminder = () => {
 | 
				
			||||||
    const { guild } = useParams();
 | 
					    const guild = useGuild();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [reminder, setReminder] = useState(defaultReminder());
 | 
					    const [reminder, setReminder] = useState(defaultReminder());
 | 
				
			||||||
    const [collapsed, setCollapsed] = useState(false);
 | 
					    const [collapsed, setCollapsed] = useState(false);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,8 +7,10 @@ import { useReminder } from "./ReminderContext";
 | 
				
			|||||||
import { Attachment } from "./Attachment";
 | 
					import { Attachment } from "./Attachment";
 | 
				
			||||||
import { TTS } from "./TTS";
 | 
					import { TTS } from "./TTS";
 | 
				
			||||||
import { TimeInput } from "./TimeInput";
 | 
					import { TimeInput } from "./TimeInput";
 | 
				
			||||||
 | 
					import { useGuild } from "../App/useGuild";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Settings = () => {
 | 
					export const Settings = () => {
 | 
				
			||||||
 | 
					    const guild = useGuild();
 | 
				
			||||||
    const { isSuccess: userFetched, data: userInfo } = useQuery(fetchUserInfo());
 | 
					    const { isSuccess: userFetched, data: userInfo } = useQuery(fetchUserInfo());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [reminder, setReminder] = useReminder();
 | 
					    const [reminder, setReminder] = useReminder();
 | 
				
			||||||
@@ -19,22 +21,24 @@ export const Settings = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <div class="column settings">
 | 
					        <div class="column settings">
 | 
				
			||||||
            <div class="field channel-field">
 | 
					            {guild && (
 | 
				
			||||||
                <div class="collapses">
 | 
					                <div class="field channel-field">
 | 
				
			||||||
                    <label class="label" for="channelOption">
 | 
					                    <div class="collapses">
 | 
				
			||||||
                        Channel*
 | 
					                        <label class="label" for="channelOption">
 | 
				
			||||||
                    </label>
 | 
					                            Channel*
 | 
				
			||||||
 | 
					                        </label>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <ChannelSelector
 | 
				
			||||||
 | 
					                        channel={reminder.channel}
 | 
				
			||||||
 | 
					                        setChannel={(channel: string) => {
 | 
				
			||||||
 | 
					                            setReminder((reminder) => ({
 | 
				
			||||||
 | 
					                                ...reminder,
 | 
				
			||||||
 | 
					                                channel: channel,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <ChannelSelector
 | 
					            )}
 | 
				
			||||||
                    channel={reminder.channel}
 | 
					 | 
				
			||||||
                    setChannel={(channel: string) => {
 | 
					 | 
				
			||||||
                        setReminder((reminder) => ({
 | 
					 | 
				
			||||||
                            ...reminder,
 | 
					 | 
				
			||||||
                            channel: channel,
 | 
					 | 
				
			||||||
                        }));
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div class="field">
 | 
					            <div class="field">
 | 
				
			||||||
                <div class="control">
 | 
					                <div class="control">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,8 +35,10 @@ type Database = MySql;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
enum Error {
 | 
					enum Error {
 | 
				
			||||||
    SQLx,
 | 
					    #[allow(unused)]
 | 
				
			||||||
    Serenity,
 | 
					    SQLx(sqlx::Error),
 | 
				
			||||||
 | 
					    #[allow(unused)]
 | 
				
			||||||
 | 
					    Serenity(serenity::Error),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn initialize(
 | 
					pub async fn initialize(
 | 
				
			||||||
@@ -132,6 +134,8 @@ pub async fn initialize(
 | 
				
			|||||||
                routes::dashboard::api::user::get_user_info,
 | 
					                routes::dashboard::api::user::get_user_info,
 | 
				
			||||||
                routes::dashboard::api::user::update_user_info,
 | 
					                routes::dashboard::api::user::update_user_info,
 | 
				
			||||||
                routes::dashboard::api::user::get_user_guilds,
 | 
					                routes::dashboard::api::user::get_user_guilds,
 | 
				
			||||||
 | 
					                routes::dashboard::api::user::get_reminders,
 | 
				
			||||||
 | 
					                routes::dashboard::api::user::create_user_reminder,
 | 
				
			||||||
                routes::dashboard::api::guild::get_guild_info,
 | 
					                routes::dashboard::api::guild::get_guild_info,
 | 
				
			||||||
                routes::dashboard::api::guild::get_guild_channels,
 | 
					                routes::dashboard::api::guild::get_guild_channels,
 | 
				
			||||||
                routes::dashboard::api::guild::get_guild_roles,
 | 
					                routes::dashboard::api::guild::get_guild_roles,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -106,7 +106,7 @@ pub async fn get_reminders(
 | 
				
			|||||||
                 reminders.username,
 | 
					                 reminders.username,
 | 
				
			||||||
                 reminders.utc_time
 | 
					                 reminders.utc_time
 | 
				
			||||||
                FROM reminders
 | 
					                FROM reminders
 | 
				
			||||||
                LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
					                INNER JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
                WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
 | 
					                WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
 | 
				
			||||||
                channels
 | 
					                channels
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
mod guilds;
 | 
					mod guilds;
 | 
				
			||||||
 | 
					mod models;
 | 
				
			||||||
 | 
					mod reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
pub use guilds::*;
 | 
					pub use guilds::*;
 | 
				
			||||||
 | 
					pub use reminders::*;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,230 @@
 | 
				
			|||||||
use std::env;
 | 
					use chrono::{naive::NaiveDateTime, Utc};
 | 
				
			||||||
 | 
					use futures::TryFutureExt;
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use rocket::serde::json::json;
 | 
				
			||||||
use reqwest::Client;
 | 
					 | 
				
			||||||
use rocket::{
 | 
					 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					 | 
				
			||||||
    State,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{client::Context, futures, model::id::UserId};
 | 
				
			||||||
    client::Context,
 | 
					use sqlx::types::Json;
 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        id::{GuildId, RoleId},
 | 
					 | 
				
			||||||
        permissions::Permissions,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::DISCORD_API, routes::JsonResult};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    check_subscription,
 | 
				
			||||||
 | 
					    consts::{
 | 
				
			||||||
 | 
					        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_NAME_LENGTH, MAX_URL_LENGTH,
 | 
				
			||||||
 | 
					        MIN_INTERVAL,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
 | 
					    routes::{
 | 
				
			||||||
 | 
					        dashboard::{create_database_channel, generate_uid, name_default, Attachment, EmbedField},
 | 
				
			||||||
 | 
					        JsonResult,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct Reminder {
 | 
				
			||||||
 | 
					    pub attachment: Option<Attachment>,
 | 
				
			||||||
 | 
					    pub attachment_name: Option<String>,
 | 
				
			||||||
 | 
					    pub content: String,
 | 
				
			||||||
 | 
					    pub embed_author: String,
 | 
				
			||||||
 | 
					    pub embed_author_url: Option<String>,
 | 
				
			||||||
 | 
					    pub embed_color: u32,
 | 
				
			||||||
 | 
					    pub embed_description: String,
 | 
				
			||||||
 | 
					    pub embed_footer: String,
 | 
				
			||||||
 | 
					    pub embed_footer_url: Option<String>,
 | 
				
			||||||
 | 
					    pub embed_image_url: Option<String>,
 | 
				
			||||||
 | 
					    pub embed_thumbnail_url: Option<String>,
 | 
				
			||||||
 | 
					    pub embed_title: String,
 | 
				
			||||||
 | 
					    pub embed_fields: Option<Json<Vec<EmbedField>>>,
 | 
				
			||||||
 | 
					    pub enabled: bool,
 | 
				
			||||||
 | 
					    pub expires: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    pub interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    pub interval_days: Option<u32>,
 | 
				
			||||||
 | 
					    pub interval_months: Option<u32>,
 | 
				
			||||||
 | 
					    #[serde(default = "name_default")]
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub tts: bool,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub uid: String,
 | 
				
			||||||
 | 
					    pub utc_time: NaiveDateTime,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn create_reminder(
 | 
				
			||||||
 | 
					    ctx: &Context,
 | 
				
			||||||
 | 
					    transaction: &mut Transaction<'_>,
 | 
				
			||||||
 | 
					    user_id: UserId,
 | 
				
			||||||
 | 
					    reminder: Reminder,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    let channel = user_id
 | 
				
			||||||
 | 
					        .create_dm_channel(&ctx)
 | 
				
			||||||
 | 
					        .map_err(|e| Error::Serenity(e))
 | 
				
			||||||
 | 
					        .and_then(|dm_channel| create_database_channel(&ctx, dm_channel.id, transaction))
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Err(e) = channel {
 | 
				
			||||||
 | 
					        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Err(json!({"error": "Failed to configure channel for reminders."}));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channel = channel.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate lengths
 | 
				
			||||||
 | 
					    check_length!(MAX_NAME_LENGTH, reminder.name);
 | 
				
			||||||
 | 
					    check_length!(MAX_CONTENT_LENGTH, reminder.content);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer);
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields);
 | 
				
			||||||
 | 
					    if let Some(fields) = &reminder.embed_fields {
 | 
				
			||||||
 | 
					        for field in &fields.0 {
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    check_length_opt!(
 | 
				
			||||||
 | 
					        MAX_URL_LENGTH,
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate urls
 | 
				
			||||||
 | 
					    check_url_opt!(
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate time and interval
 | 
				
			||||||
 | 
					    if reminder.utc_time < Utc::now().naive_utc() {
 | 
				
			||||||
 | 
					        return Err(json!({"error": "Time must be in the future"}));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if reminder.interval_seconds.is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_days.is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_months.is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
 | 
				
			||||||
 | 
					            + reminder.interval_days.unwrap_or(0) * DAY as u32
 | 
				
			||||||
 | 
					            + reminder.interval_seconds.unwrap_or(0)
 | 
				
			||||||
 | 
					            < *MIN_INTERVAL
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return Err(json!({"error": "Interval too short"}));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check patreon if necessary
 | 
				
			||||||
 | 
					    if reminder.interval_seconds.is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_days.is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_months.is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if !check_subscription(&ctx, user_id).await {
 | 
				
			||||||
 | 
					            return Err(json!({"error": "Patreon is required to set intervals"}));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() };
 | 
				
			||||||
 | 
					    let new_uid = generate_uid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // write to db
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO reminders (
 | 
				
			||||||
 | 
					         uid,
 | 
				
			||||||
 | 
					         attachment,
 | 
				
			||||||
 | 
					         attachment_name,
 | 
				
			||||||
 | 
					         channel_id,
 | 
				
			||||||
 | 
					         content,
 | 
				
			||||||
 | 
					         embed_author,
 | 
				
			||||||
 | 
					         embed_author_url,
 | 
				
			||||||
 | 
					         embed_color,
 | 
				
			||||||
 | 
					         embed_description,
 | 
				
			||||||
 | 
					         embed_footer,
 | 
				
			||||||
 | 
					         embed_footer_url,
 | 
				
			||||||
 | 
					         embed_image_url,
 | 
				
			||||||
 | 
					         embed_thumbnail_url,
 | 
				
			||||||
 | 
					         embed_title,
 | 
				
			||||||
 | 
					         embed_fields,
 | 
				
			||||||
 | 
					         enabled,
 | 
				
			||||||
 | 
					         expires,
 | 
				
			||||||
 | 
					         interval_seconds,
 | 
				
			||||||
 | 
					         interval_days,
 | 
				
			||||||
 | 
					         interval_months,
 | 
				
			||||||
 | 
					         name,
 | 
				
			||||||
 | 
					         tts,
 | 
				
			||||||
 | 
					         `utc_time`
 | 
				
			||||||
 | 
					        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
 | 
					        new_uid,
 | 
				
			||||||
 | 
					        reminder.attachment,
 | 
				
			||||||
 | 
					        reminder.attachment_name,
 | 
				
			||||||
 | 
					        channel,
 | 
				
			||||||
 | 
					        reminder.content,
 | 
				
			||||||
 | 
					        reminder.embed_author,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_color,
 | 
				
			||||||
 | 
					        reminder.embed_description,
 | 
				
			||||||
 | 
					        reminder.embed_footer,
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_title,
 | 
				
			||||||
 | 
					        reminder.embed_fields,
 | 
				
			||||||
 | 
					        reminder.enabled,
 | 
				
			||||||
 | 
					        reminder.expires,
 | 
				
			||||||
 | 
					        reminder.interval_seconds,
 | 
				
			||||||
 | 
					        reminder.interval_days,
 | 
				
			||||||
 | 
					        reminder.interval_months,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        reminder.tts,
 | 
				
			||||||
 | 
					        reminder.utc_time,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(transaction.executor())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Reminder,
 | 
				
			||||||
 | 
					            "SELECT
 | 
				
			||||||
 | 
					             reminders.attachment,
 | 
				
			||||||
 | 
					             reminders.attachment_name,
 | 
				
			||||||
 | 
					             reminders.content,
 | 
				
			||||||
 | 
					             reminders.embed_author,
 | 
				
			||||||
 | 
					             reminders.embed_author_url,
 | 
				
			||||||
 | 
					             reminders.embed_color,
 | 
				
			||||||
 | 
					             reminders.embed_description,
 | 
				
			||||||
 | 
					             reminders.embed_footer,
 | 
				
			||||||
 | 
					             reminders.embed_footer_url,
 | 
				
			||||||
 | 
					             reminders.embed_image_url,
 | 
				
			||||||
 | 
					             reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					             reminders.embed_title,
 | 
				
			||||||
 | 
					             reminders.embed_fields,
 | 
				
			||||||
 | 
					             reminders.enabled,
 | 
				
			||||||
 | 
					             reminders.expires,
 | 
				
			||||||
 | 
					             reminders.interval_seconds,
 | 
				
			||||||
 | 
					             reminders.interval_days,
 | 
				
			||||||
 | 
					             reminders.interval_months,
 | 
				
			||||||
 | 
					             reminders.name,
 | 
				
			||||||
 | 
					             reminders.tts,
 | 
				
			||||||
 | 
					             reminders.uid,
 | 
				
			||||||
 | 
					             reminders.utc_time
 | 
				
			||||||
 | 
					            FROM reminders
 | 
				
			||||||
 | 
					            WHERE uid = ?",
 | 
				
			||||||
 | 
					            new_uid
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(transaction.executor())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map(|r| Ok(json!(r)))
 | 
				
			||||||
 | 
					        .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					            warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(json!({"error": "Could not load reminder"}))
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error in `create_reminder`: Could not execute query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(json!({"error": "Unknown error"}))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,48 @@
 | 
				
			|||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use reqwest::Client;
 | 
					 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serenity::{client::Context, model::id::UserId};
 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        id::{GuildId, RoleId},
 | 
					 | 
				
			||||||
        permissions::Permissions,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::DISCORD_API, routes::JsonResult};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
 | 
					    routes::{
 | 
				
			||||||
 | 
					        dashboard::api::user::models::{create_reminder, Reminder},
 | 
				
			||||||
 | 
					        JsonResult,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/api/user/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn create_user_reminder(
 | 
				
			||||||
 | 
					    reminder: Json<Reminder>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    let user_id =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match create_reminder(
 | 
				
			||||||
 | 
					        ctx.inner(),
 | 
				
			||||||
 | 
					        &mut transaction,
 | 
				
			||||||
 | 
					        UserId::new(user_id),
 | 
				
			||||||
 | 
					        reminder.into_inner(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(r) => match transaction.commit().await {
 | 
				
			||||||
 | 
					            Ok(_) => Ok(r),
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Couldn't commit transaction: {:?}", e);
 | 
				
			||||||
 | 
					                json_err!("Couldn't commit transaction.")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => Err(e),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[get("/api/user/reminders")]
 | 
					#[get("/api/user/reminders")]
 | 
				
			||||||
pub async fn get_reminders(
 | 
					pub async fn get_reminders(
 | 
				
			||||||
@@ -25,5 +50,56 @@ pub async fn get_reminders(
 | 
				
			|||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    pool: &State<Pool<MySql>>,
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonResult {
 | 
				
			||||||
    Ok(json! {})
 | 
					    let user_id =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					    let channel = UserId::new(user_id).create_dm_channel(ctx.inner()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match channel {
 | 
				
			||||||
 | 
					        Ok(channel) => sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Reminder,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					            SELECT
 | 
				
			||||||
 | 
					                reminders.attachment,
 | 
				
			||||||
 | 
					                reminders.attachment_name,
 | 
				
			||||||
 | 
					                reminders.content,
 | 
				
			||||||
 | 
					                reminders.embed_author,
 | 
				
			||||||
 | 
					                reminders.embed_author_url,
 | 
				
			||||||
 | 
					                reminders.embed_color,
 | 
				
			||||||
 | 
					                reminders.embed_description,
 | 
				
			||||||
 | 
					                reminders.embed_footer,
 | 
				
			||||||
 | 
					                reminders.embed_footer_url,
 | 
				
			||||||
 | 
					                reminders.embed_image_url,
 | 
				
			||||||
 | 
					                reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					                reminders.embed_title,
 | 
				
			||||||
 | 
					                IFNULL(reminders.embed_fields, '[]') AS embed_fields,
 | 
				
			||||||
 | 
					                reminders.enabled,
 | 
				
			||||||
 | 
					                reminders.expires,
 | 
				
			||||||
 | 
					                reminders.interval_seconds,
 | 
				
			||||||
 | 
					                reminders.interval_days,
 | 
				
			||||||
 | 
					                reminders.interval_months,
 | 
				
			||||||
 | 
					                reminders.name,
 | 
				
			||||||
 | 
					                reminders.tts,
 | 
				
			||||||
 | 
					                reminders.uid,
 | 
				
			||||||
 | 
					                reminders.utc_time
 | 
				
			||||||
 | 
					            FROM reminders
 | 
				
			||||||
 | 
					            INNER JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					            WHERE `status` = 'pending' AND channels.channel = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            channel.id.get()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map(|r| Ok(json!(r)))
 | 
				
			||||||
 | 
					        .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					            warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not load reminders")
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Couldn't get DM channel: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not find a DM channel")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@ fn interval_default() -> Unset<Option<u32>> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#[derive(sqlx::Type)]
 | 
					#[derive(sqlx::Type)]
 | 
				
			||||||
#[sqlx(transparent)]
 | 
					#[sqlx(transparent)]
 | 
				
			||||||
struct Attachment(Vec<u8>);
 | 
					pub struct Attachment(Vec<u8>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'de> Deserialize<'de> for Attachment {
 | 
					impl<'de> Deserialize<'de> for Attachment {
 | 
				
			||||||
    fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
 | 
					    fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
 | 
				
			||||||
@@ -605,11 +605,13 @@ async fn create_database_channel(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    match row {
 | 
					    match row {
 | 
				
			||||||
        Ok(row) => {
 | 
					        Ok(row) => {
 | 
				
			||||||
            if row.webhook_token.is_none() || row.webhook_id.is_none() {
 | 
					            let is_dm =
 | 
				
			||||||
 | 
					                channel.to_channel(&ctx).await.map_err(|e| Error::Serenity(e))?.private().is_some();
 | 
				
			||||||
 | 
					            if !is_dm && (row.webhook_token.is_none() || row.webhook_id.is_none()) {
 | 
				
			||||||
                let webhook = channel
 | 
					                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(|_| Error::Serenity)?;
 | 
					                    .map_err(|e| Error::Serenity(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let token = webhook.token.unwrap();
 | 
					                let token = webhook.token.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -623,7 +625,7 @@ async fn create_database_channel(
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
                .execute(transaction.executor())
 | 
					                .execute(transaction.executor())
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .map_err(|_| Error::SQLx)?;
 | 
					                .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(())
 | 
					            Ok(())
 | 
				
			||||||
@@ -634,7 +636,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(|_| Error::Serenity)?;
 | 
					                .map_err(|e| Error::Serenity(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let token = webhook.token.unwrap();
 | 
					            let token = webhook.token.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -653,18 +655,18 @@ async fn create_database_channel(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            .execute(transaction.executor())
 | 
					            .execute(transaction.executor())
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            .map_err(|_| Error::SQLx)?;
 | 
					            .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(())
 | 
					            Ok(())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Err(_) => Err(Error::SQLx),
 | 
					        Err(e) => Err(Error::SQLx(e)),
 | 
				
			||||||
    }?;
 | 
					    }?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.get())
 | 
					    let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.get())
 | 
				
			||||||
        .fetch_one(transaction.executor())
 | 
					        .fetch_one(transaction.executor())
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .map_err(|_| Error::SQLx)?;
 | 
					        .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(row.id)
 | 
					    Ok(row.id)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user