More work on todo list
This commit is contained in:
		@@ -54,6 +54,11 @@ export type Todo = {
 | 
				
			|||||||
    value: string;
 | 
					    value: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CreateTodo = {
 | 
				
			||||||
 | 
					    channel_id: string;
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ChannelInfo = {
 | 
					export type ChannelInfo = {
 | 
				
			||||||
    id: string;
 | 
					    id: string;
 | 
				
			||||||
    name: string;
 | 
					    name: string;
 | 
				
			||||||
@@ -209,17 +214,12 @@ export const patchGuildTodo = (guild: string) => ({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const postGuildTodo = (guild: string) => ({
 | 
					export const postGuildTodo = (guild: string) => ({
 | 
				
			||||||
    mutationFn: (reminder: Reminder) =>
 | 
					    mutationFn: (todo: CreateTodo) =>
 | 
				
			||||||
        axios.post(`/dashboard/api/guild/${guild}/todos`, reminder).then((resp) => resp.data),
 | 
					        axios.post(`/dashboard/api/guild/${guild}/todos`, todo).then((resp) => resp.data),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deleteGuildTodo = () => ({
 | 
					export const deleteGuildTodo = (guild: string) => ({
 | 
				
			||||||
    mutationFn: (todo: Todo) =>
 | 
					    mutationFn: (todoId: number) => axios.delete(`/dashboard/api/guild/${guild}/todos/${todoId}`),
 | 
				
			||||||
        axios.delete(`/dashboard/api/todos`, {
 | 
					 | 
				
			||||||
            data: {
 | 
					 | 
				
			||||||
                id: todo.id,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const fetchUserReminders = () => ({
 | 
					export const fetchUserReminders = () => ({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import { createPortal, PropsWithChildren } from "preact/compat";
 | 
				
			|||||||
import { Import } from "../Import";
 | 
					import { Import } from "../Import";
 | 
				
			||||||
import { useGuild } from "../App/useGuild";
 | 
					import { useGuild } from "../App/useGuild";
 | 
				
			||||||
import { Link } from "wouter";
 | 
					import { Link } from "wouter";
 | 
				
			||||||
 | 
					import { usePathname } from "wouter/use-browser-location";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "./index.scss";
 | 
					import "./index.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,13 +19,14 @@ export const Guild = ({ children }: PropsWithChildren) => {
 | 
				
			|||||||
        return <GuildError />;
 | 
					        return <GuildError />;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        const importModal = createPortal(<Import />, document.getElementById("bottom-sidebar"));
 | 
					        const importModal = createPortal(<Import />, document.getElementById("bottom-sidebar"));
 | 
				
			||||||
 | 
					        const path = usePathname();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <>
 | 
					            <>
 | 
				
			||||||
                {importModal}
 | 
					                {importModal}
 | 
				
			||||||
                <div class="page-links">
 | 
					                <div class="page-links">
 | 
				
			||||||
                    <Link
 | 
					                    <Link
 | 
				
			||||||
                        class="button is-outlined is-success is-small"
 | 
					                        class={`button is-outlined is-success is-small ${path.endsWith("reminders") && "is-focused"}`}
 | 
				
			||||||
                        href={`/${guild}/reminders`}
 | 
					                        href={`/${guild}/reminders`}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                        <span>Reminders</span>
 | 
					                        <span>Reminders</span>
 | 
				
			||||||
@@ -32,7 +34,10 @@ export const Guild = ({ children }: PropsWithChildren) => {
 | 
				
			|||||||
                            <i class="fa fa-chevron-right"></i>
 | 
					                            <i class="fa fa-chevron-right"></i>
 | 
				
			||||||
                        </span>
 | 
					                        </span>
 | 
				
			||||||
                    </Link>
 | 
					                    </Link>
 | 
				
			||||||
                    <Link class="button is-outlined is-success is-small" href={`/${guild}/todos`}>
 | 
					                    <Link
 | 
				
			||||||
 | 
					                        class={`button is-outlined is-success is-small ${path.endsWith("todos") && "is-focused"}`}
 | 
				
			||||||
 | 
					                        href={`/${guild}/todos`}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
                        <span>Todo lists</span>
 | 
					                        <span>Todo lists</span>
 | 
				
			||||||
                        <span class="icon">
 | 
					                        <span class="icon">
 | 
				
			||||||
                            <i class="fa fa-chevron-right"></i>
 | 
					                            <i class="fa fa-chevron-right"></i>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,64 @@
 | 
				
			|||||||
import { useQuery } from "react-query";
 | 
					import { useMutation, useQuery, useQueryClient } from "react-query";
 | 
				
			||||||
import { fetchGuildChannels } from "../../api";
 | 
					import { fetchGuildChannels, postGuildTodo } from "../../api";
 | 
				
			||||||
import { useGuild } from "../App/useGuild";
 | 
					import { useGuild } from "../App/useGuild";
 | 
				
			||||||
 | 
					import { useState } from "preact/hooks";
 | 
				
			||||||
 | 
					import { useFlash } from "../App/FlashContext";
 | 
				
			||||||
 | 
					import { ICON_FLASH_TIME } from "../../consts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CreateTodo = () => {
 | 
					export const CreateTodo = () => {
 | 
				
			||||||
    const guild = useGuild();
 | 
					    const guild = useGuild();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [recentlyCreated, setRecentlyCreated] = useState(false);
 | 
				
			||||||
 | 
					    const [newTodo, setNewTodo] = useState({ value: "", channel_id: null });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const flash = useFlash();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const queryClient = useQueryClient();
 | 
				
			||||||
    const { isSuccess, data: channels } = useQuery(fetchGuildChannels(guild));
 | 
					    const { isSuccess, data: channels } = useQuery(fetchGuildChannels(guild));
 | 
				
			||||||
 | 
					    const mutation = useMutation({
 | 
				
			||||||
 | 
					        ...postGuildTodo(guild),
 | 
				
			||||||
 | 
					        onSuccess: (data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                flash({
 | 
				
			||||||
 | 
					                    message: data.error,
 | 
				
			||||||
 | 
					                    type: "error",
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                flash({
 | 
				
			||||||
 | 
					                    message: "Todo created",
 | 
				
			||||||
 | 
					                    type: "success",
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                queryClient.invalidateQueries({
 | 
				
			||||||
 | 
					                    queryKey: ["GUILD_TODO"],
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                setRecentlyCreated(true);
 | 
				
			||||||
 | 
					                setTimeout(() => {
 | 
				
			||||||
 | 
					                    setRecentlyCreated(false);
 | 
				
			||||||
 | 
					                }, ICON_FLASH_TIME);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(newTodo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <div class="todo">
 | 
					        <div class="todo">
 | 
				
			||||||
            <textarea class="input todo-input" onInput={() => null} />
 | 
					            <textarea
 | 
				
			||||||
 | 
					                class="input todo-input"
 | 
				
			||||||
 | 
					                onInput={(ev) => setNewTodo((todo) => ({ ...todo, value: ev.currentTarget.value }))}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
            <div class="control has-icons-left">
 | 
					            <div class="control has-icons-left">
 | 
				
			||||||
                <div class="select">
 | 
					                <div class="select">
 | 
				
			||||||
                    <select name="channel" class="channel-selector" onInput={() => {}}>
 | 
					                    <select
 | 
				
			||||||
 | 
					                        name="channel"
 | 
				
			||||||
 | 
					                        class="channel-selector"
 | 
				
			||||||
 | 
					                        onInput={(ev) =>
 | 
				
			||||||
 | 
					                            setNewTodo((todo) => ({
 | 
				
			||||||
 | 
					                                ...todo,
 | 
				
			||||||
 | 
					                                channel_id: ev.currentTarget.value || null,
 | 
				
			||||||
 | 
					                            }))
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
                        <option value="">(None)</option>
 | 
					                        <option value="">(None)</option>
 | 
				
			||||||
                        {isSuccess && channels.map((c) => <option value={c.id}>{c.name}</option>)}
 | 
					                        {isSuccess && channels.map((c) => <option value={c.id}>{c.name}</option>)}
 | 
				
			||||||
                    </select>
 | 
					                    </select>
 | 
				
			||||||
@@ -21,9 +67,21 @@ export const CreateTodo = () => {
 | 
				
			|||||||
                    <i class="fas fa-hashtag"></i>
 | 
					                    <i class="fas fa-hashtag"></i>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <button onClick={() => null} class="button is-success save-btn">
 | 
					            <button onClick={() => mutation.mutate(newTodo)} class="button is-success save-btn">
 | 
				
			||||||
                <span class="icon">
 | 
					                <span class="icon">
 | 
				
			||||||
                    <i class="fa fa-sparkles"></i>
 | 
					                    {mutation.isLoading ? (
 | 
				
			||||||
 | 
					                        <span class="icon">
 | 
				
			||||||
 | 
					                            <i class="fas fa-spin fa-cog"></i>
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    ) : recentlyCreated ? (
 | 
				
			||||||
 | 
					                        <span class="icon">
 | 
				
			||||||
 | 
					                            <i class="fas fa-check"></i>
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                        <span class="icon">
 | 
				
			||||||
 | 
					                            <i class="fas fa-sparkles"></i>
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,9 +29,9 @@ pub mod string {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub mod string_opt {
 | 
					pub mod string_opt {
 | 
				
			||||||
    use std::fmt::Display;
 | 
					    use std::{fmt::Display, str::FromStr};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use serde::{Deserializer, Serializer};
 | 
					    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
 | 
					    pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
 | 
				
			||||||
    where
 | 
					    where
 | 
				
			||||||
@@ -44,6 +44,17 @@ pub mod string_opt {
 | 
				
			|||||||
            serializer.serialize_none()
 | 
					            serializer.serialize_none()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: FromStr,
 | 
				
			||||||
 | 
					        T::Err: Display,
 | 
				
			||||||
 | 
					        D: Deserializer<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Option::<String>::deserialize(deserializer)?
 | 
				
			||||||
 | 
					            .map(|s| s.parse().map_err(de::Error::custom))
 | 
				
			||||||
 | 
					            .transpose()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{env, path::Path};
 | 
					use std::{env, path::Path};
 | 
				
			||||||
@@ -185,10 +196,10 @@ pub async fn initialize(
 | 
				
			|||||||
                routes::dashboard::api::guild::create_guild_reminder,
 | 
					                routes::dashboard::api::guild::create_guild_reminder,
 | 
				
			||||||
                routes::dashboard::api::guild::get_reminders,
 | 
					                routes::dashboard::api::guild::get_reminders,
 | 
				
			||||||
                routes::dashboard::api::guild::edit_reminder,
 | 
					                routes::dashboard::api::guild::edit_reminder,
 | 
				
			||||||
                routes::dashboard::api::guild::create_todo,
 | 
					                routes::dashboard::api::guild::todos::create_todo,
 | 
				
			||||||
                routes::dashboard::api::guild::get_todo,
 | 
					                routes::dashboard::api::guild::todos::get_todo,
 | 
				
			||||||
                routes::dashboard::api::guild::update_todo,
 | 
					                routes::dashboard::api::guild::todos::update_todo,
 | 
				
			||||||
                routes::dashboard::api::guild::delete_todo,
 | 
					                routes::dashboard::api::guild::todos::delete_todo,
 | 
				
			||||||
                routes::dashboard::export::export_reminders,
 | 
					                routes::dashboard::export::export_reminders,
 | 
				
			||||||
                routes::dashboard::export::export_reminder_templates,
 | 
					                routes::dashboard::export::export_reminder_templates,
 | 
				
			||||||
                routes::dashboard::export::export_todos,
 | 
					                routes::dashboard::export::export_todos,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ mod emojis;
 | 
				
			|||||||
mod reminders;
 | 
					mod reminders;
 | 
				
			||||||
mod roles;
 | 
					mod roles;
 | 
				
			||||||
mod templates;
 | 
					mod templates;
 | 
				
			||||||
mod todos;
 | 
					pub mod todos;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -17,7 +17,6 @@ use serenity::{
 | 
				
			|||||||
    model::id::{GuildId, RoleId},
 | 
					    model::id::{GuildId, RoleId},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
pub use templates::*;
 | 
					pub use templates::*;
 | 
				
			||||||
pub use todos::{create_todo, delete_todo, get_todo, update_todo};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::web::{check_authorization, routes::JsonResult};
 | 
					use crate::web::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,26 +7,38 @@ use rocket::{
 | 
				
			|||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serenity::prelude::Context;
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    all::{ChannelId, GuildId},
 | 
				
			||||||
 | 
					    prelude::Context,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::web::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_authorization, guards::transaction::Transaction, routes::JsonResult, string_opt,
 | 
					    check_authorization,
 | 
				
			||||||
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
 | 
					    routes::{dashboard::check_channel_matches_guild, JsonResult},
 | 
				
			||||||
 | 
					    string_opt,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize)]
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
pub struct CreateTodo {
 | 
					pub struct CreateTodo {
 | 
				
			||||||
 | 
					    #[serde(with = "string_opt")]
 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					    channel_id: Option<u64>,
 | 
				
			||||||
    value: String,
 | 
					    value: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
struct GetTodo {
 | 
					pub struct GetTodo {
 | 
				
			||||||
    id: u32,
 | 
					    id: u32,
 | 
				
			||||||
    #[serde(with = "string_opt")]
 | 
					    #[serde(with = "string_opt")]
 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					    channel_id: Option<u64>,
 | 
				
			||||||
    value: String,
 | 
					    value: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct UpdateTodo {
 | 
				
			||||||
 | 
					    value: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[post("/api/guild/<id>/todos", data = "<todo>")]
 | 
					#[post("/api/guild/<id>/todos", data = "<todo>")]
 | 
				
			||||||
pub async fn create_todo(
 | 
					pub async fn create_todo(
 | 
				
			||||||
    id: u64,
 | 
					    id: u64,
 | 
				
			||||||
@@ -37,6 +49,67 @@ pub async fn create_todo(
 | 
				
			|||||||
) -> JsonResult {
 | 
					) -> JsonResult {
 | 
				
			||||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let guild_id = GuildId::new(id);
 | 
				
			||||||
 | 
					    if todo.value.len() > 2000 {
 | 
				
			||||||
 | 
					        return json_err!("Value too long");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match todo.channel_id {
 | 
				
			||||||
 | 
					        Some(channel_id) => {
 | 
				
			||||||
 | 
					            if !check_channel_matches_guild(ctx, ChannelId::new(channel_id), guild_id) {
 | 
				
			||||||
 | 
					                warn!("Channel {} not found for guild {}", channel_id, guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return json_err!("Channel not found");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
 | 
					                VALUES (
 | 
				
			||||||
 | 
					                    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
 | 
					                    (SELECT id FROM channels WHERE channel = ?),
 | 
				
			||||||
 | 
					                    ?
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                id,
 | 
				
			||||||
 | 
					                channel_id,
 | 
				
			||||||
 | 
					                todo.value
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(transaction.executor())
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| {
 | 
				
			||||||
 | 
					                warn!("Error creating todo: {:?}", e);
 | 
				
			||||||
 | 
					                json!({"errors": vec!["Unknown error"]})
 | 
				
			||||||
 | 
					            })?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
 | 
					                VALUES (
 | 
				
			||||||
 | 
					                    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
 | 
					                    NULL,
 | 
				
			||||||
 | 
					                    ?
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                id,
 | 
				
			||||||
 | 
					                todo.value
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(transaction.executor())
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| {
 | 
				
			||||||
 | 
					                warn!("Error creating todo: {:?}", e);
 | 
				
			||||||
 | 
					                json!({"errors": vec!["Unknown error"]})
 | 
				
			||||||
 | 
					            })?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Err(e) = transaction.commit().await {
 | 
				
			||||||
 | 
					        warn!("Couldn't commit transaction: {:?}", e);
 | 
				
			||||||
 | 
					        return json_err!("Couldn't commit transaction.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(json!({}))
 | 
					    Ok(json!({}))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,26 +148,67 @@ pub async fn get_todo(
 | 
				
			|||||||
    Ok(json!(todos))
 | 
					    Ok(json!(todos))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[patch("/api/guild/<id>/todos")]
 | 
					#[patch("/api/guild/<guild_id>/todos/<todo_id>", data = "<todo>")]
 | 
				
			||||||
pub async fn update_todo(
 | 
					pub async fn update_todo(
 | 
				
			||||||
    id: u64,
 | 
					    guild_id: u64,
 | 
				
			||||||
 | 
					    todo_id: u64,
 | 
				
			||||||
 | 
					    todo: Json<UpdateTodo>,
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    mut transaction: Transaction<'_>,
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonResult {
 | 
				
			||||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
					    check_authorization(cookies, ctx.inner(), guild_id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if todo.value.len() > 2000 {
 | 
				
			||||||
 | 
					        return json_err!("Value too long");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        UPDATE todos
 | 
				
			||||||
 | 
					        SET value = ?
 | 
				
			||||||
 | 
					        WHERE guild_id = ?
 | 
				
			||||||
 | 
					            AND id = ?
 | 
				
			||||||
 | 
					        ",
 | 
				
			||||||
 | 
					        todo.value,
 | 
				
			||||||
 | 
					        guild_id,
 | 
				
			||||||
 | 
					        todo_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(transaction.executor())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .map_err(|e| {
 | 
				
			||||||
 | 
					        warn!("Error updating todo: {:?}", e);
 | 
				
			||||||
 | 
					        json!({"errors": vec!["Unknown error"]})
 | 
				
			||||||
 | 
					    })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(json!({}))
 | 
					    Ok(json!({}))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[delete("/api/guild/<id>/todos")]
 | 
					#[delete("/api/guild/<guild_id>/todos/<todo_id>")]
 | 
				
			||||||
pub async fn delete_todo(
 | 
					pub async fn delete_todo(
 | 
				
			||||||
    id: u64,
 | 
					    guild_id: u64,
 | 
				
			||||||
 | 
					    todo_id: u64,
 | 
				
			||||||
    cookies: &CookieJar<'_>,
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
    ctx: &State<Context>,
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
    mut transaction: Transaction<'_>,
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
) -> JsonResult {
 | 
					) -> JsonResult {
 | 
				
			||||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
					    check_authorization(cookies, ctx.inner(), guild_id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					        DELETE FROM todos
 | 
				
			||||||
 | 
					        WHERE guild_id = ?
 | 
				
			||||||
 | 
					            AND id = ?
 | 
				
			||||||
 | 
					        ",
 | 
				
			||||||
 | 
					        guild_id,
 | 
				
			||||||
 | 
					        todo_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(transaction.executor())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .map_err(|e| {
 | 
				
			||||||
 | 
					        warn!("Error deleting todo: {:?}", e);
 | 
				
			||||||
 | 
					        json!({"errors": vec!["Unknown error"]})
 | 
				
			||||||
 | 
					    })?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(json!({}))
 | 
					    Ok(json!({}))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -388,22 +388,13 @@ pub(crate) async fn create_reminder(
 | 
				
			|||||||
        _ => {}
 | 
					        _ => {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {
 | 
					    if !check_channel_matches_guild(ctx, ChannelId::new(reminder.channel), guild_id) {
 | 
				
			||||||
        // validate channel
 | 
					        warn!(
 | 
				
			||||||
        let channel = ChannelId::new(reminder.channel).to_channel_cached(&ctx.cache);
 | 
					            "Error in `create_reminder`: channel {} not found for guild {}",
 | 
				
			||||||
        let channel_exists = channel.is_some();
 | 
					            reminder.channel, guild_id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let channel_matches_guild =
 | 
					        return Err(json!({"error": "Channel not found"}));
 | 
				
			||||||
            channel.map_or(false, |c| c.guild(&ctx.cache).map_or(false, |c| c.id == guild_id));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if !channel_matches_guild || !channel_exists {
 | 
					 | 
				
			||||||
            warn!(
 | 
					 | 
				
			||||||
                "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
 | 
					 | 
				
			||||||
                reminder.channel, guild_id, channel_exists
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return Err(json!({"error": "Channel not found"}));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let channel =
 | 
					    let channel =
 | 
				
			||||||
@@ -601,6 +592,21 @@ pub(crate) async fn create_reminder(
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn check_channel_matches_guild(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> bool {
 | 
				
			||||||
 | 
					    // validate channel
 | 
				
			||||||
 | 
					    let channel = channel_id.to_channel_cached(&ctx.cache);
 | 
				
			||||||
 | 
					    let channel_exists = channel.is_some();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !channel_exists {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channel_matches_guild =
 | 
				
			||||||
 | 
					        channel.map_or(false, |c| c.guild(&ctx.cache).map_or(false, |g| g.id == guild_id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel_matches_guild
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn create_database_channel(
 | 
					async fn create_database_channel(
 | 
				
			||||||
    ctx: impl CacheHttp,
 | 
					    ctx: impl CacheHttp,
 | 
				
			||||||
    channel: ChannelId,
 | 
					    channel: ChannelId,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user