diff --git a/src/web/mod.rs b/src/web/mod.rs index 31b1a87..3439058 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -195,7 +195,7 @@ pub async fn initialize( routes::dashboard::api::guild::get_guild_roles, routes::dashboard::api::guild::get_guild_emojis, routes::dashboard::api::guild::get_reminder_templates, - routes::dashboard::api::guild::create_reminder_template, + routes::dashboard::api::guild::create_guild_reminder_template, routes::dashboard::api::guild::delete_reminder_template, routes::dashboard::api::guild::create_guild_reminder, routes::dashboard::api::guild::get_reminders, @@ -204,11 +204,12 @@ pub async fn initialize( routes::dashboard::api::guild::todos::get_todo, routes::dashboard::api::guild::todos::update_todo, routes::dashboard::api::guild::todos::delete_todo, - routes::dashboard::export::export_reminders, - routes::dashboard::export::export_reminder_templates, - routes::dashboard::export::export_todos, - routes::dashboard::export::import_reminders, - routes::dashboard::export::import_todos, + routes::dashboard::export::reminders::export, + routes::dashboard::export::reminders::import, + routes::dashboard::export::reminder_templates::export, + routes::dashboard::export::reminder_templates::import, + routes::dashboard::export::todos::export, + routes::dashboard::export::todos::import, ], ) .launch() diff --git a/src/web/routes/dashboard/api/guild/templates.rs b/src/web/routes/dashboard/api/guild/templates.rs index 92d61cb..63269d1 100644 --- a/src/web/routes/dashboard/api/guild/templates.rs +++ b/src/web/routes/dashboard/api/guild/templates.rs @@ -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//templates", data = "")] -pub async fn create_reminder_template( +pub async fn create_guild_reminder_template( id: u64, reminder_template: Json, cookies: &CookieJar<'_>, ctx: &State, - pool: &State>, + 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), } } diff --git a/src/web/routes/dashboard/export/mod.rs b/src/web/routes/dashboard/export/mod.rs new file mode 100644 index 0000000..2814582 --- /dev/null +++ b/src/web/routes/dashboard/export/mod.rs @@ -0,0 +1,3 @@ +pub mod reminder_templates; +pub mod reminders; +pub mod todos; diff --git a/src/web/routes/dashboard/export/reminder_templates.rs b/src/web/routes/dashboard/export/reminder_templates.rs new file mode 100644 index 0000000..56d0d10 --- /dev/null +++ b/src/web/routes/dashboard/export/reminder_templates.rs @@ -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//export/reminder_templates")] +pub async fn export( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//export/reminder_templates", data = "")] +pub async fn import( + id: u64, + cookies: &CookieJar<'_>, + body: Json, + ctx: &State, + 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::() { + 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") + } + } +} diff --git a/src/web/routes/dashboard/export.rs b/src/web/routes/dashboard/export/reminders.rs similarity index 54% rename from src/web/routes/dashboard/export.rs rename to src/web/routes/dashboard/export/reminders.rs index fbbaecb..7576fb8 100644 --- a/src/web/routes/dashboard/export.rs +++ b/src/web/routes/dashboard/export/reminders.rs @@ -26,7 +26,7 @@ use serenity::{ use sqlx::{MySql, Pool}; #[get("/api/guild//export/reminders")] -pub async fn export_reminders( +pub async fn export( id: u64, cookies: &CookieJar<'_>, ctx: &State, @@ -127,7 +127,7 @@ pub async fn export_reminders( } #[put("/api/guild//export/reminders", data = "")] -pub(crate) async fn import_reminders( +pub async fn import( id: u64, cookies: &CookieJar<'_>, body: Json, @@ -228,224 +228,3 @@ pub(crate) async fn import_reminders( } } } - -#[get("/api/guild//export/todos")] -pub async fn export_todos( - id: u64, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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//export/todos", data = "")] -pub async fn import_todos( - id: u64, - cookies: &CookieJar<'_>, - body: Json, - ctx: &State, - pool: &State>, -) -> 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::() { - match result { - Ok(record) => match record.channel_id { - Some(channel_id) => { - let channel_id = channel_id.split_at(1).1; - - match channel_id.parse::() { - 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//export/reminder_templates")] -pub async fn export_reminder_templates( - id: u64, - cookies: &CookieJar<'_>, - ctx: &State, - pool: &State>, -) -> 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") - } - } -} diff --git a/src/web/routes/dashboard/export/todos.rs b/src/web/routes/dashboard/export/todos.rs new file mode 100644 index 0000000..d291a56 --- /dev/null +++ b/src/web/routes/dashboard/export/todos.rs @@ -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//export/todos")] +pub async fn export( + id: u64, + cookies: &CookieJar<'_>, + ctx: &State, + pool: &State>, +) -> 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//export/todos", data = "")] +pub async fn import( + id: u64, + cookies: &CookieJar<'_>, + body: Json, + ctx: &State, + pool: &State>, +) -> 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::() { + match result { + Ok(record) => match record.channel_id { + Some(channel_id) => { + let channel_id = channel_id.split_at(1).1; + + match channel_id.parse::() { + 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.") + } + } +} diff --git a/src/web/routes/dashboard/mod.rs b/src/web/routes/dashboard/mod.rs index 3aadbdd..efaf41a 100644 --- a/src/web/routes/dashboard/mod.rs +++ b/src/web/routes/dashboard/mod.rs @@ -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(