Added a route for importing templates
This commit is contained in:
		@@ -6,9 +6,12 @@ use rocket::{
 | 
			
		||||
    serde::json::{json, Json},
 | 
			
		||||
    State,
 | 
			
		||||
};
 | 
			
		||||
use serenity::all::GuildId;
 | 
			
		||||
use serenity::client::Context;
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
 | 
			
		||||
use crate::web::guards::transaction::Transaction;
 | 
			
		||||
use crate::web::routes::dashboard::create_reminder_template;
 | 
			
		||||
use crate::web::{
 | 
			
		||||
    check_authorization,
 | 
			
		||||
    consts::{
 | 
			
		||||
@@ -49,109 +52,32 @@ pub async fn get_reminder_templates(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
 | 
			
		||||
pub async fn create_reminder_template(
 | 
			
		||||
pub async fn create_guild_reminder_template(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    reminder_template: Json<ReminderTemplate>,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
    mut transaction: Transaction<'_>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    // validate lengths
 | 
			
		||||
    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
 | 
			
		||||
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
 | 
			
		||||
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
 | 
			
		||||
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
 | 
			
		||||
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
 | 
			
		||||
    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
 | 
			
		||||
    if let Some(fields) = &reminder_template.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_USERNAME_LENGTH, reminder_template.username);
 | 
			
		||||
    check_length_opt!(
 | 
			
		||||
        MAX_URL_LENGTH,
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.avatar
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // validate urls
 | 
			
		||||
    check_url_opt!(
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.avatar
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let name = if reminder_template.name.is_empty() {
 | 
			
		||||
        template_name_default()
 | 
			
		||||
    } else {
 | 
			
		||||
        reminder_template.name.clone()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match sqlx::query!(
 | 
			
		||||
        "INSERT INTO reminder_template
 | 
			
		||||
        (guild_id,
 | 
			
		||||
         name,
 | 
			
		||||
         attachment,
 | 
			
		||||
         attachment_name,
 | 
			
		||||
         avatar,
 | 
			
		||||
         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,
 | 
			
		||||
         interval_seconds,
 | 
			
		||||
         interval_days,
 | 
			
		||||
         interval_months,
 | 
			
		||||
         tts,
 | 
			
		||||
         username
 | 
			
		||||
        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
 | 
			
		||||
         ?, ?, ?, ?, ?, ?, ?)",
 | 
			
		||||
        id,
 | 
			
		||||
        name,
 | 
			
		||||
        reminder_template.attachment,
 | 
			
		||||
        reminder_template.attachment_name,
 | 
			
		||||
        reminder_template.avatar,
 | 
			
		||||
        reminder_template.content,
 | 
			
		||||
        reminder_template.embed_author,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_color,
 | 
			
		||||
        reminder_template.embed_description,
 | 
			
		||||
        reminder_template.embed_footer,
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_title,
 | 
			
		||||
        reminder_template.embed_fields,
 | 
			
		||||
        reminder_template.interval_seconds,
 | 
			
		||||
        reminder_template.interval_days,
 | 
			
		||||
        reminder_template.interval_months,
 | 
			
		||||
        reminder_template.tts,
 | 
			
		||||
        reminder_template.username,
 | 
			
		||||
    match create_reminder_template(
 | 
			
		||||
        ctx.inner(),
 | 
			
		||||
        &mut transaction,
 | 
			
		||||
        GuildId::new(id),
 | 
			
		||||
        reminder_template.into_inner(),
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(pool.inner())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(_) => Ok(json!({})),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not create template for {}: {:?}", id, e);
 | 
			
		||||
        Ok(r) => match transaction.commit().await {
 | 
			
		||||
            Ok(_) => Ok(r),
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                warn!("Couldn't commit transaction: {:?}", e);
 | 
			
		||||
                json_err!("Couldn't commit transaction.")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
            json_err!("Could not create template")
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => Err(e),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/web/routes/dashboard/export/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/web/routes/dashboard/export/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
pub mod reminder_templates;
 | 
			
		||||
pub mod reminders;
 | 
			
		||||
pub mod todos;
 | 
			
		||||
							
								
								
									
										181
									
								
								src/web/routes/dashboard/export/reminder_templates.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/web/routes/dashboard/export/reminder_templates.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
			
		||||
use crate::web::routes::dashboard::{create_reminder_template, ReminderTemplate};
 | 
			
		||||
use crate::web::{
 | 
			
		||||
    check_authorization,
 | 
			
		||||
    guards::transaction::Transaction,
 | 
			
		||||
    routes::{
 | 
			
		||||
        dashboard::{
 | 
			
		||||
            create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
 | 
			
		||||
        },
 | 
			
		||||
        JsonResult,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use crate::Database;
 | 
			
		||||
use base64::{prelude::BASE64_STANDARD, Engine};
 | 
			
		||||
use csv::{QuoteStyle, WriterBuilder};
 | 
			
		||||
use log::warn;
 | 
			
		||||
use rocket::{
 | 
			
		||||
    get,
 | 
			
		||||
    http::CookieJar,
 | 
			
		||||
    put,
 | 
			
		||||
    serde::json::{json, Json},
 | 
			
		||||
    State,
 | 
			
		||||
};
 | 
			
		||||
use serenity::{
 | 
			
		||||
    client::Context,
 | 
			
		||||
    model::id::{ChannelId, GuildId, UserId},
 | 
			
		||||
};
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
 | 
			
		||||
#[get("/api/guild/<id>/export/reminder_templates")]
 | 
			
		||||
pub async fn export(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
			
		||||
 | 
			
		||||
    match sqlx::query_as_unchecked!(
 | 
			
		||||
        ReminderTemplateCsv,
 | 
			
		||||
        "SELECT
 | 
			
		||||
         name,
 | 
			
		||||
         attachment,
 | 
			
		||||
         attachment_name,
 | 
			
		||||
         avatar,
 | 
			
		||||
         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,
 | 
			
		||||
         interval_seconds,
 | 
			
		||||
         interval_days,
 | 
			
		||||
         interval_months,
 | 
			
		||||
         tts,
 | 
			
		||||
         username
 | 
			
		||||
        FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
			
		||||
        id
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(pool.inner())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(templates) => {
 | 
			
		||||
            templates.iter().for_each(|template| {
 | 
			
		||||
                csv_writer.serialize(template).unwrap();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            match csv_writer.into_inner() {
 | 
			
		||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
			
		||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("Failed to write UTF-8")
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                    json_err!("Failed to extract CSV")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Failed to query templates")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[put("/api/guild/<id>/export/reminder_templates", data = "<body>")]
 | 
			
		||||
pub async fn import(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    body: Json<ImportBody>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    mut transaction: Transaction<'_>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    match BASE64_STANDARD.decode(&body.body) {
 | 
			
		||||
        Ok(body) => {
 | 
			
		||||
            let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
			
		||||
            let mut count = 0;
 | 
			
		||||
 | 
			
		||||
            for result in reader.deserialize::<ReminderTemplateCsv>() {
 | 
			
		||||
                match result {
 | 
			
		||||
                    Ok(record) => {
 | 
			
		||||
                        let reminder_template = ReminderTemplate {
 | 
			
		||||
                            id: 0,
 | 
			
		||||
                            guild_id: 0,
 | 
			
		||||
                            name: record.name,
 | 
			
		||||
                            attachment: record.attachment,
 | 
			
		||||
                            attachment_name: record.attachment_name,
 | 
			
		||||
                            avatar: record.avatar,
 | 
			
		||||
                            content: record.content,
 | 
			
		||||
                            embed_author: record.embed_author,
 | 
			
		||||
                            embed_author_url: record.embed_author_url,
 | 
			
		||||
                            embed_color: record.embed_color,
 | 
			
		||||
                            embed_description: record.embed_description,
 | 
			
		||||
                            embed_footer: record.embed_footer,
 | 
			
		||||
                            embed_footer_url: record.embed_footer_url,
 | 
			
		||||
                            embed_image_url: record.embed_image_url,
 | 
			
		||||
                            embed_thumbnail_url: record.embed_thumbnail_url,
 | 
			
		||||
                            embed_title: record.embed_title,
 | 
			
		||||
                            embed_fields: record
 | 
			
		||||
                                .embed_fields
 | 
			
		||||
                                .map(|s| serde_json::from_str(&s).ok())
 | 
			
		||||
                                .flatten(),
 | 
			
		||||
                            interval_seconds: record.interval_seconds,
 | 
			
		||||
                            interval_days: record.interval_days,
 | 
			
		||||
                            interval_months: record.interval_months,
 | 
			
		||||
                            tts: record.tts,
 | 
			
		||||
                            username: record.username,
 | 
			
		||||
                        };
 | 
			
		||||
 | 
			
		||||
                        create_reminder_template(
 | 
			
		||||
                            ctx.inner(),
 | 
			
		||||
                            &mut transaction,
 | 
			
		||||
                            GuildId::new(id),
 | 
			
		||||
                            reminder_template,
 | 
			
		||||
                        )
 | 
			
		||||
                        .await?;
 | 
			
		||||
 | 
			
		||||
                        count += 1;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Couldn't deserialize CSV row: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        return json_err!(format!("Deserialize error: {:?}", e));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            match transaction.commit().await {
 | 
			
		||||
                Ok(_) => Ok(json!({
 | 
			
		||||
                    "message": format!("Imported {} reminder templates", count)
 | 
			
		||||
                })),
 | 
			
		||||
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    warn!("Failed to commit transaction: {:?}", e);
 | 
			
		||||
                    json_err!("Couldn't commit transaction")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Err(_) => {
 | 
			
		||||
            json_err!("Malformed base64")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -26,7 +26,7 @@ use serenity::{
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
 | 
			
		||||
#[get("/api/guild/<id>/export/reminders")]
 | 
			
		||||
pub async fn export_reminders(
 | 
			
		||||
pub async fn export(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
@@ -127,7 +127,7 @@ pub async fn export_reminders(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[put("/api/guild/<id>/export/reminders", data = "<body>")]
 | 
			
		||||
pub(crate) async fn import_reminders(
 | 
			
		||||
pub async fn import(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    body: Json<ImportBody>,
 | 
			
		||||
@@ -228,224 +228,3 @@ pub(crate) async fn import_reminders(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/api/guild/<id>/export/todos")]
 | 
			
		||||
pub async fn export_todos(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
			
		||||
 | 
			
		||||
    match sqlx::query_as_unchecked!(
 | 
			
		||||
        TodoCsv,
 | 
			
		||||
        "SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
 | 
			
		||||
        LEFT JOIN channels ON todos.channel_id = channels.id
 | 
			
		||||
        INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
			
		||||
        WHERE guilds.guild = ?",
 | 
			
		||||
        id
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(pool.inner())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(todos) => {
 | 
			
		||||
            todos.iter().for_each(|todo| {
 | 
			
		||||
                csv_writer.serialize(todo).unwrap();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            match csv_writer.into_inner() {
 | 
			
		||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
			
		||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("Failed to write UTF-8")
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                    json_err!("Failed to extract CSV")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Failed to query templates")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[put("/api/guild/<id>/export/todos", data = "<body>")]
 | 
			
		||||
pub async fn import_todos(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    body: Json<ImportBody>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
 | 
			
		||||
 | 
			
		||||
    match channels_res {
 | 
			
		||||
        Ok(channels) => match BASE64_STANDARD.decode(&body.body) {
 | 
			
		||||
            Ok(body) => {
 | 
			
		||||
                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
			
		||||
 | 
			
		||||
                let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
 | 
			
		||||
                let mut query_params = vec![];
 | 
			
		||||
 | 
			
		||||
                for result in reader.deserialize::<TodoCsv>() {
 | 
			
		||||
                    match result {
 | 
			
		||||
                        Ok(record) => match record.channel_id {
 | 
			
		||||
                            Some(channel_id) => {
 | 
			
		||||
                                let channel_id = channel_id.split_at(1).1;
 | 
			
		||||
 | 
			
		||||
                                match channel_id.parse::<u64>() {
 | 
			
		||||
                                    Ok(channel_id) => {
 | 
			
		||||
                                        if channels.contains_key(&ChannelId::new(channel_id)) {
 | 
			
		||||
                                            query_params.push((record.value, Some(channel_id), id));
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            return json_err!(format!(
 | 
			
		||||
                                                "Invalid channel ID {}",
 | 
			
		||||
                                                channel_id
 | 
			
		||||
                                            ));
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    Err(_) => {
 | 
			
		||||
                                        return json_err!(format!(
 | 
			
		||||
                                            "Invalid channel ID {}",
 | 
			
		||||
                                            channel_id
 | 
			
		||||
                                        ));
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            None => {
 | 
			
		||||
                                query_params.push((record.value, None, id));
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            warn!("Couldn't deserialize CSV row: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                            return json_err!("Deserialize error. Aborted");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let query_str = format!(
 | 
			
		||||
                    "INSERT INTO todos (value, channel_id, guild_id) VALUES {}",
 | 
			
		||||
                    vec![query_placeholder].repeat(query_params.len()).join(",")
 | 
			
		||||
                );
 | 
			
		||||
                let mut query = sqlx::query(&query_str);
 | 
			
		||||
 | 
			
		||||
                for param in query_params {
 | 
			
		||||
                    query = query.bind(param.0).bind(param.1).bind(param.2);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let res = query.execute(pool.inner()).await;
 | 
			
		||||
 | 
			
		||||
                match res {
 | 
			
		||||
                    Ok(_) => Ok(json!({})),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Couldn't execute todo query: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("An unexpected error occured.")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                json_err!("Malformed base64")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Couldn't fetch channels for guild {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Couldn't fetch channels.")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/api/guild/<id>/export/reminder_templates")]
 | 
			
		||||
pub async fn export_reminder_templates(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
			
		||||
 | 
			
		||||
    match sqlx::query_as_unchecked!(
 | 
			
		||||
        ReminderTemplateCsv,
 | 
			
		||||
        "SELECT
 | 
			
		||||
         name,
 | 
			
		||||
         attachment,
 | 
			
		||||
         attachment_name,
 | 
			
		||||
         avatar,
 | 
			
		||||
         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,
 | 
			
		||||
         interval_seconds,
 | 
			
		||||
         interval_days,
 | 
			
		||||
         interval_months,
 | 
			
		||||
         tts,
 | 
			
		||||
         username
 | 
			
		||||
        FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
			
		||||
        id
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(pool.inner())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(templates) => {
 | 
			
		||||
            templates.iter().for_each(|template| {
 | 
			
		||||
                csv_writer.serialize(template).unwrap();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            match csv_writer.into_inner() {
 | 
			
		||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
			
		||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("Failed to write UTF-8")
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                    json_err!("Failed to extract CSV")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Failed to query templates")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										176
									
								
								src/web/routes/dashboard/export/todos.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/web/routes/dashboard/export/todos.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
			
		||||
use crate::web::{
 | 
			
		||||
    check_authorization,
 | 
			
		||||
    guards::transaction::Transaction,
 | 
			
		||||
    routes::{
 | 
			
		||||
        dashboard::{
 | 
			
		||||
            create_reminder, CreateReminder, ImportBody, ReminderCsv, ReminderTemplateCsv, TodoCsv,
 | 
			
		||||
        },
 | 
			
		||||
        JsonResult,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use crate::Database;
 | 
			
		||||
use base64::{prelude::BASE64_STANDARD, Engine};
 | 
			
		||||
use csv::{QuoteStyle, WriterBuilder};
 | 
			
		||||
use log::warn;
 | 
			
		||||
use rocket::{
 | 
			
		||||
    get,
 | 
			
		||||
    http::CookieJar,
 | 
			
		||||
    put,
 | 
			
		||||
    serde::json::{json, Json},
 | 
			
		||||
    State,
 | 
			
		||||
};
 | 
			
		||||
use serenity::{
 | 
			
		||||
    client::Context,
 | 
			
		||||
    model::id::{ChannelId, GuildId, UserId},
 | 
			
		||||
};
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
 | 
			
		||||
#[get("/api/guild/<id>/export/todos")]
 | 
			
		||||
pub async fn export(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]);
 | 
			
		||||
 | 
			
		||||
    match sqlx::query_as_unchecked!(
 | 
			
		||||
        TodoCsv,
 | 
			
		||||
        "SELECT value, CONCAT('#', channels.channel) AS channel_id FROM todos
 | 
			
		||||
        LEFT JOIN channels ON todos.channel_id = channels.id
 | 
			
		||||
        INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
			
		||||
        WHERE guilds.guild = ?",
 | 
			
		||||
        id
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(pool.inner())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(todos) => {
 | 
			
		||||
            todos.iter().for_each(|todo| {
 | 
			
		||||
                csv_writer.serialize(todo).unwrap();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            match csv_writer.into_inner() {
 | 
			
		||||
                Ok(inner) => match String::from_utf8(inner) {
 | 
			
		||||
                    Ok(encoded) => Ok(json!({ "body": encoded })),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Failed to write UTF-8: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("Failed to write UTF-8")
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    warn!("Failed to extract CSV: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                    json_err!("Failed to extract CSV")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Failed to query templates")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[put("/api/guild/<id>/export/todos", data = "<body>")]
 | 
			
		||||
pub async fn import(
 | 
			
		||||
    id: u64,
 | 
			
		||||
    cookies: &CookieJar<'_>,
 | 
			
		||||
    body: Json<ImportBody>,
 | 
			
		||||
    ctx: &State<Context>,
 | 
			
		||||
    pool: &State<Pool<MySql>>,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_authorization(cookies, ctx.inner(), id).await?;
 | 
			
		||||
 | 
			
		||||
    let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
 | 
			
		||||
 | 
			
		||||
    match channels_res {
 | 
			
		||||
        Ok(channels) => match BASE64_STANDARD.decode(&body.body) {
 | 
			
		||||
            Ok(body) => {
 | 
			
		||||
                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
			
		||||
 | 
			
		||||
                let query_placeholder = "(?, (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?))";
 | 
			
		||||
                let mut query_params = vec![];
 | 
			
		||||
 | 
			
		||||
                for result in reader.deserialize::<TodoCsv>() {
 | 
			
		||||
                    match result {
 | 
			
		||||
                        Ok(record) => match record.channel_id {
 | 
			
		||||
                            Some(channel_id) => {
 | 
			
		||||
                                let channel_id = channel_id.split_at(1).1;
 | 
			
		||||
 | 
			
		||||
                                match channel_id.parse::<u64>() {
 | 
			
		||||
                                    Ok(channel_id) => {
 | 
			
		||||
                                        if channels.contains_key(&ChannelId::new(channel_id)) {
 | 
			
		||||
                                            query_params.push((record.value, Some(channel_id), id));
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            return json_err!(format!(
 | 
			
		||||
                                                "Invalid channel ID {}",
 | 
			
		||||
                                                channel_id
 | 
			
		||||
                                            ));
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    Err(_) => {
 | 
			
		||||
                                        return json_err!(format!(
 | 
			
		||||
                                            "Invalid channel ID {}",
 | 
			
		||||
                                            channel_id
 | 
			
		||||
                                        ));
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            None => {
 | 
			
		||||
                                query_params.push((record.value, None, id));
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            warn!("Couldn't deserialize CSV row: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                            return json_err!("Deserialize error. Aborted");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let query_str = format!(
 | 
			
		||||
                    "INSERT INTO todos (value, channel_id, guild_id) VALUES {}",
 | 
			
		||||
                    vec![query_placeholder].repeat(query_params.len()).join(",")
 | 
			
		||||
                );
 | 
			
		||||
                let mut query = sqlx::query(&query_str);
 | 
			
		||||
 | 
			
		||||
                for param in query_params {
 | 
			
		||||
                    query = query.bind(param.0).bind(param.1).bind(param.2);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let res = query.execute(pool.inner()).await;
 | 
			
		||||
 | 
			
		||||
                match res {
 | 
			
		||||
                    Ok(_) => Ok(json!({})),
 | 
			
		||||
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        warn!("Couldn't execute todo query: {:?}", e);
 | 
			
		||||
 | 
			
		||||
                        json_err!("An unexpected error occured.")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Err(_) => {
 | 
			
		||||
                json_err!("Malformed base64")
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Couldn't fetch channels for guild {}: {:?}", id, e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Couldn't fetch channels.")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -593,12 +593,113 @@ pub(crate) async fn create_reminder(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn check_channel_matches_guild(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> bool {
 | 
			
		||||
    return match ctx.cache.guild(guild_id) {
 | 
			
		||||
        Some(guild) => guild.channels.get(&channel_id).is_some(),
 | 
			
		||||
pub(crate) async fn create_reminder_template(
 | 
			
		||||
    ctx: &Context,
 | 
			
		||||
    transaction: &mut Transaction<'_>,
 | 
			
		||||
    guild_id: GuildId,
 | 
			
		||||
    reminder_template: ReminderTemplate,
 | 
			
		||||
) -> JsonResult {
 | 
			
		||||
    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
 | 
			
		||||
    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
 | 
			
		||||
    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
 | 
			
		||||
    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
 | 
			
		||||
    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
 | 
			
		||||
    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
 | 
			
		||||
    if let Some(fields) = &reminder_template.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_USERNAME_LENGTH, reminder_template.username);
 | 
			
		||||
    check_length_opt!(
 | 
			
		||||
        MAX_URL_LENGTH,
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.avatar
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        None => false,
 | 
			
		||||
    // validate urls
 | 
			
		||||
    check_url_opt!(
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.avatar
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let name = if reminder_template.name.is_empty() {
 | 
			
		||||
        template_name_default()
 | 
			
		||||
    } else {
 | 
			
		||||
        reminder_template.name.clone()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match sqlx::query!(
 | 
			
		||||
        "INSERT INTO reminder_template
 | 
			
		||||
        (guild_id,
 | 
			
		||||
         name,
 | 
			
		||||
         attachment,
 | 
			
		||||
         attachment_name,
 | 
			
		||||
         avatar,
 | 
			
		||||
         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,
 | 
			
		||||
         interval_seconds,
 | 
			
		||||
         interval_days,
 | 
			
		||||
         interval_months,
 | 
			
		||||
         tts,
 | 
			
		||||
         username
 | 
			
		||||
        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
 | 
			
		||||
         ?, ?, ?, ?, ?, ?, ?)",
 | 
			
		||||
        guild_id.get(),
 | 
			
		||||
        name,
 | 
			
		||||
        reminder_template.attachment,
 | 
			
		||||
        reminder_template.attachment_name,
 | 
			
		||||
        reminder_template.avatar,
 | 
			
		||||
        reminder_template.content,
 | 
			
		||||
        reminder_template.embed_author,
 | 
			
		||||
        reminder_template.embed_author_url,
 | 
			
		||||
        reminder_template.embed_color,
 | 
			
		||||
        reminder_template.embed_description,
 | 
			
		||||
        reminder_template.embed_footer,
 | 
			
		||||
        reminder_template.embed_footer_url,
 | 
			
		||||
        reminder_template.embed_image_url,
 | 
			
		||||
        reminder_template.embed_thumbnail_url,
 | 
			
		||||
        reminder_template.embed_title,
 | 
			
		||||
        reminder_template.embed_fields,
 | 
			
		||||
        reminder_template.interval_seconds,
 | 
			
		||||
        reminder_template.interval_days,
 | 
			
		||||
        reminder_template.interval_months,
 | 
			
		||||
        reminder_template.tts,
 | 
			
		||||
        reminder_template.username,
 | 
			
		||||
    )
 | 
			
		||||
    .fetch_all(transaction.executor())
 | 
			
		||||
    .await
 | 
			
		||||
    {
 | 
			
		||||
        Ok(_) => Ok(json!({})),
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Could not create template for {}: {:?}", guild_id.get(), e);
 | 
			
		||||
 | 
			
		||||
            json_err!("Could not create template")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn check_channel_matches_guild(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> bool {
 | 
			
		||||
    match ctx.cache.guild(guild_id) {
 | 
			
		||||
        Some(guild) => guild.channels.get(&channel_id).is_some(),
 | 
			
		||||
        None => false,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn create_database_channel(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user