use crate ::utils ::* ;
use serde_json ::Map ;
use crate ::serenity ;
use crate ::Error ;
use crate ::{ Context , PrefixContext } ;
use poise ::command ;
use slug ::slugify ;
use std ::fs ;
use std ::fs ::File ;
use std ::io ::Write ;
use std ::path ::Path ;
#[ command(
prefix_command ,
slash_command ,
ephemeral ,
description_localized ( "en-US" , "View the latest handwriting challenge info." )
) ]
pub async fn challenge ( ctx : Context < ' _ > ) -> Result < ( ) , Error > {
ctx . say ( format! (
"Tegaki Tuesday #{n}: <https://tegakituesday.com/{n}>" ,
n = get_challenge_number ( )
) )
. await ? ;
Ok ( ( ) )
}
#[ command(
prefix_command ,
slash_command ,
ephemeral ,
broadcast_typing ,
description_localized ( "en-US" , "Submit to the latest handwriting challenge." )
) ]
pub async fn submit ( ctx : Context < ' _ > , submission : serenity ::Attachment ) -> Result < ( ) , Error > {
// TODO: The code for this command needs to be refactored,
// there are large duplicated sections that need to be merged somehow.
match ctx {
Context ::Application ( _ ) = > ctx . defer_ephemeral ( ) . await ? ,
Context ::Prefix ( ctx ) = > {
if ctx . msg . attachments . len ( ) = = 0 {
ctx . msg
. reply ( & ctx . discord . http , "Please attach at least one image." )
. await ? ;
return Ok ( ( ) ) ;
}
}
}
let guild_data = get_guild_data ( ) ;
let guild = ctx . guild_id ( ) . unwrap ( ) . as_u64 ( ) . to_string ( ) ;
if ! guild_data . contains_key ( & guild )
| | ! & guild_data [ & guild ]
. as_object ( )
. unwrap ( )
. contains_key ( "submissionChannel" )
{
ctx . say ( "Submissions aren't enabled for this server yet." )
. await ? ;
return Ok ( ( ) ) ;
}
let current_guild_data = & guild_data [ & guild ] . as_object ( ) . unwrap ( ) ;
let submission_channel = current_guild_data [ "submissionChannel" ] . as_str ( ) . unwrap ( ) ;
if submission_channel ! = ctx . channel_id ( ) . as_u64 ( ) . to_string ( ) {
ctx . defer_ephemeral ( ) . await ? ;
ctx . say ( format! (
"Sorry, submissions aren't permitted here. Please go to <#{}>. Thanks!" ,
guild
) )
. await ? ;
return Ok ( ( ) ) ;
}
let challenge_number = get_challenge_number ( ) ;
let submission_images_dir = get_submission_images_dir ( ) ;
// Ensure that submission_images_dir exists
let path = Path ::new ( & submission_images_dir ) ;
std ::fs ::create_dir_all ( path ) ? ;
let mut submission_data = get_current_submission_data ( ) ;
let mut existing_submitter = false ;
let mut invalid_types = false ;
let mut requires_rebuild = false ;
let attachments = vec! [ submission ] ;
let attachments = match ctx {
Context ::Application ( _ ) = > & attachments ,
Context ::Prefix ( ctx ) = > & ctx . msg . attachments ,
} ;
let author = ctx . author ( ) ;
let mut images = Vec ::new ( ) ;
for ( i , submission ) in submission_data . iter_mut ( ) . enumerate ( ) {
if is_matching_submission ( & submission , author ) {
existing_submitter = true ;
images = submission [ "images" ] . as_array_mut ( ) . unwrap ( ) . clone ( ) . iter ( ) . map ( | value | value . to_string ( ) ) . collect ( ) ;
for attachment in attachments . iter ( ) {
let extension ;
if let Some ( content_type ) = & attachment . content_type {
if content_type = = "image/png" {
extension = "png" ;
} else if content_type = = "image/jpeg" {
extension = "jpg" ;
} else {
invalid_types = true ;
continue ;
}
} else {
invalid_types = true ;
continue ;
}
requires_rebuild = true ;
let file_name = format! (
"{}-{}-{}-{}.{}" ,
i + 1 ,
slugify ( & author . name ) ,
author . discriminator ,
images . len ( ) + 1 ,
extension
) ;
images . push ( file_name . clone ( ) . into ( ) ) ;
let image = reqwest ::get ( & attachment . url ) . await ? . bytes ( ) . await ? ;
let mut image_file =
File ::create ( format! ( "{}/{}" , submission_images_dir , file_name ) ) ? ;
image_file . write_all ( & image ) ? ;
image_file . flush ( ) ? ;
}
submission [ "images" ] = images . clone ( ) . into ( ) ;
break ;
}
}
if ! existing_submitter {
let mut submitter_data = Map ::new ( ) ;
submitter_data . insert (
String ::from ( "username" ) ,
format! ( "{}#{}" , author . name , author . discriminator ) . into ( ) ,
) ;
let mut images : Vec < String > = Vec ::new ( ) ;
for attachment in attachments . iter ( ) {
let extension ;
if let Some ( content_type ) = & attachment . content_type {
if content_type = = "image/png" {
extension = "png" ;
} else if content_type = = "image/jpeg" {
extension = "jpg" ;
} else {
invalid_types = true ;
continue ;
}
} else {
invalid_types = true ;
continue ;
}
requires_rebuild = true ;
let file_name = format! (
"{}-{}-{}{}.{}" ,
submission_data . len ( ) + 1 ,
slugify ( & author . name ) ,
author . discriminator ,
if images . len ( ) = = 0 {
String ::from ( "" )
} else {
format! ( "-{}" , images . len ( ) + 1 )
} ,
extension
) ;
images . push ( file_name . clone ( ) . into ( ) ) ;
let image = reqwest ::get ( & attachment . url ) . await ? . bytes ( ) . await ? ;
let mut image_file = File ::create ( format! ( "{}/{}" , submission_images_dir , file_name ) ) ? ;
image_file . write_all ( & image ) ? ;
image_file . flush ( ) ? ;
}
submitter_data . insert ( String ::from ( "images" ) , images . into ( ) ) ;
submitter_data . insert ( String ::from ( "id" ) , author . id . as_u64 ( ) . to_string ( ) . into ( ) ) ;
submission_data . push ( submitter_data . into ( ) ) ;
}
set_submission_data ( submission_data ) ;
let mut message = String ::new ( ) ;
if requires_rebuild {
let thank_you = & format! ( "Thank you for submitting! You can view your submission at <https://tegakituesday.com/{}>" , challenge_number ) ;
let mut repost_here = true ;
match ctx {
Context ::Application ( _ ) = > message . push_str ( thank_you ) ,
Context ::Prefix ( ctx ) = > match ctx . msg . delete ( & ctx . discord . http ) . await {
Ok ( _ ) = > { }
// don't repost image on this server if no manage messages perm
// (if we can't delete user's image it'll be displayed twice)
Err ( _ ) = > {
repost_here = false ;
message . push_str ( thank_you ) ;
}
} ,
} ;
if invalid_types {
message . push_str ( "\nSome of your attachments could not be uploaded; only **.png**, **.jpg**, and **.jpeg** files are permitted." ) ;
}
leaderboard ( & ctx ) . await ? ;
rebuild_site ( ) ;
let guild = ctx . guild ( ) . unwrap ( ) ;
let data = guild_data [ & guild . id . to_string ( ) ] . as_object ( ) . unwrap ( ) ;
let invite = if data . contains_key ( "invite" ) {
Some ( data [ "invite" ] . as_str ( ) . unwrap ( ) )
} else {
None } ;
for image in images . iter ( ) {
for ( other_guild_id , data ) in guild_data . iter ( ) {
let here = other_guild_id . eq ( & ctx . guild_id ( ) . unwrap ( ) . as_u64 ( ) . to_string ( ) ) ;
if ! repost_here & & here {
continue ;
}
let data = data . as_object ( ) . unwrap ( ) ;
if ! data . contains_key ( "submissionChannel" ) {
continue ;
}
let channel = serenity ::ChannelId (
data [ "submissionChannel" ]
. as_str ( )
. unwrap ( )
. parse ::< u64 > ( )
. unwrap ( ) ,
) ;
let accent_color = ctx
. discord ( )
. http
. get_user ( * author . id . as_u64 ( ) )
. await
. unwrap ( )
. accent_colour ;
let mut pathbuf = std ::path ::PathBuf ::new ( ) ;
pathbuf . push ( & submission_images_dir ) ;
pathbuf . push ( & image ) ;
channel . send_message ( & ctx . discord ( ) . http , | m | {
m . add_file ( & pathbuf ) ;
m . embed ( | e | {
let username = format! ( "{}#{}" , author . name , author . discriminator ) ;
let n = get_challenge_number ( ) ;
let mut description = format! ( "New submission to [Tegaki Tuesday #{n}](https://tegakituesday.com/{n})!" ) ;
if ! here {
description . push_str ( & if let Some ( invite ) = invite {
format! ( "\nCrossposted from [{}](https://discord.gg/{invite})" , guild . name )
} else {
format! ( "\nCrossposted from {}" , guild . name )
} ) ;
} ;
e . description ( description ) ;
let mut embed_author = serenity ::builder ::CreateEmbedAuthor ::default ( ) ;
embed_author
. icon_url ( get_avatar ( & author ) )
. name ( username )
. url ( format! ( "https://discord.com/users/{}" , author . id ) ) ;
e . set_author ( embed_author ) ;
e . image ( format! ( "attachment://{image}" ) ) ;
if let Some ( accent_color ) = accent_color {
e . color ( accent_color ) ;
}
e
} ) ;
m
} ) . await ? ;
}
}
} else if invalid_types {
message . push_str ( "Sorry, your submission could not be uploaded; only **.png**, **.jpg**, and **.jpeg** files are permitted." ) ;
}
if ! message . is_empty ( ) {
ctx . say ( message ) . await ? ;
}
Ok ( ( ) )
}
#[ command(
prefix_command ,
slash_command ,
ephemeral ,
description_localized ( "en-US" , "List images in your current submission, if available." )
) ]
pub async fn images ( ctx : Context < ' _ > ) -> Result < ( ) , Error > {
let submission_data = get_current_submission_data ( ) ;
let images : Vec < String > = {
let mut images = Vec ::new ( ) ;
for submission in submission_data . iter ( ) {
if is_matching_submission ( & submission , & ctx . author ( ) ) {
for image in submission [ "images" ] . as_array ( ) . unwrap ( ) . iter ( ) {
images . push ( String ::from ( image . as_str ( ) . unwrap ( ) ) ) ;
}
break ;
}
}
images
} ;
let challenge_number = get_challenge_number ( ) ;
if images . len ( ) = = 0 {
ctx . say ( format! (
"You haven't submitted anything for Tegaki Tuesday #{}." ,
challenge_number
) )
. await ? ;
return Ok ( ( ) ) ;
}
let mut message = String ::from ( format! (
"Your submission images for Tegaki Tuesday #{}:\n" ,
challenge_number
) ) ;
for ( i , image ) in images . iter ( ) . enumerate ( ) {
message . push_str ( & format! (
"( {}) <https://tegakituesday.com/{}/{}>\n" ,
to_fullwidth ( & ( i + 1 ) . to_string ( ) ) ,
challenge_number ,
image
) ) ;
}
ctx . say ( message ) . await ? ;
Ok ( ( ) )
}
#[ command(
prefix_command ,
slash_command ,
ephemeral ,
description_localized (
"en-US" ,
"Delete images from your current submission using image numbers from the images command."
)
) ]
pub async fn imagedelete ( ctx : Context < ' _ > , number : i32 ) -> Result < ( ) , Error > {
if number < 1 {
ctx . say ( "That isn't a valid image number. Image numbers start at 1." )
. await ? ;
return Ok ( ( ) ) ;
}
let challenge_number = get_challenge_number ( ) ;
let mut submission_data = get_current_submission_data ( ) ;
for ( i , submission ) in submission_data . iter_mut ( ) . enumerate ( ) {
if ! is_matching_submission ( & submission , & ctx . author ( ) ) {
continue ;
}
let mut images = submission [ "images" ] . as_array ( ) . unwrap ( ) . clone ( ) ;
let image_count = images . len ( ) ;
if image_count < number . try_into ( ) . unwrap ( ) {
ctx . say ( if image_count = = 0 {
// This is an edge case that should never happen.
// In this scenario, there is submission data with an empty image list.
// Submission data should be deleted uppon imageDelete if there are no images remaining.
format! (
"You haven't submitted anything for Tegaki Tuesday #{}." ,
challenge_number
)
} else {
format! (
"That image number doesn't exist, you only have {} image{}." ,
image_count ,
if image_count = = 1 { "" } else { "s" }
)
} )
. await ? ;
return Ok ( ( ) ) ;
}
let index = number as usize - 1 ;
let image = images [ index ] . as_str ( ) . unwrap ( ) . to_owned ( ) ;
let submission_images_dir = get_submission_images_dir ( ) ;
let image_path = format! ( "{}/{}" , submission_images_dir , image ) ;
match fs ::remove_file ( image_path ) {
Ok ( _ ) = > ( ) ,
// No need to worry about if the file is already missing
Err ( ref e ) if e . kind ( ) = = std ::io ::ErrorKind ::NotFound = > ( ) ,
Err ( _ ) = > panic! ( "Failed to remove file" ) ,
} ;
let mut message = String ::from ( format! ( "Deleted **{}** from your submission." , image ) ) ;
if images . len ( ) = = 1 {
message . push_str ( " As there are no more images left attached to your submission, it has been deleted." ) ;
submission_data . remove ( i ) ;
} else {
images . remove ( index ) ;
// One must rename all of the submitted images to prevent overwrites.
// Consider the following scenario:
// The submissions are ["bob-1234.png", "bob-1234-2.png"]
// Bob uses the command imageDelete 1
// The submissions become ["bob-1234-2.png"]
// Bob makes a second submission
// The submissions become ["bob-1234-2.png", "bob-1234-2.png"]
// The original bob-1234-2.png gets overwriten,
// and both submissions end up pointing to the same image.
// In order to prevent this, images need to be renamed according to their index.
for ( j , image ) in images . iter_mut ( ) . enumerate ( ) {
let old = image . as_str ( ) . unwrap ( ) ;
let from = format! ( "{}/{}" , submission_images_dir , old ) ;
let author = ctx . author ( ) ;
let new = format! (
"{}-{}-{}{}.{}" ,
i + 1 ,
slugify ( & author . name ) ,
author . discriminator ,
if j = = 0 {
String ::from ( "" )
} else {
format! ( "-{}" , j + 1 )
} ,
Path ::new ( old ) . extension ( ) . unwrap ( ) . to_str ( ) . unwrap ( )
) ;
let to = format! ( "{}/{}" , submission_images_dir , new ) ;
fs_extra ::file ::move_file ( from , to , & fs_extra ::file ::CopyOptions ::default ( ) )
. unwrap ( ) ;
* image = new . into ( ) ;
}
submission [ "images" ] = images . into ( ) ;
}
set_submission_data ( submission_data ) ;
rebuild_site ( ) ;
ctx . say ( message ) . await ? ;
return Ok ( ( ) ) ;
}
ctx . say ( format! (
"You haven't submitted anything for Tegaki Tuesday #{}." ,
challenge_number
) )
. await ? ;
Ok ( ( ) )
}
// TODO: make also slash command
#[ command(
prefix_command ,
description_localized ( "en-US" , "Make a suggestion for future challenge prompts!" )
) ]
pub async fn suggest (
ctx : PrefixContext < ' _ > ,
#[ description = " Suggestion text. Please include passage and source. " ] suggestion : String ,
) -> Result < ( ) , Error > {
let guild_data = get_guild_data ( ) ;
let channel = serenity ::ChannelId (
guild_data [ "suggestionChannel" ]
. as_str ( )
. unwrap ( )
. parse ::< u64 > ( )
. unwrap ( ) ,
) ;
// User::accent_colour is only available via the REST API
// If we just do msg.author.accent_colour here, we will get None
let author = & ctx . msg . author ;
let accent_color = ctx
. discord
. http
. get_user ( * author . id . as_u64 ( ) )
. await
. unwrap ( )
. accent_colour ;
channel
. send_message ( & ctx . discord . http , | m | {
m . allowed_mentions ( | am | {
am . empty_parse ( ) ;
am
} ) ;
m . embed ( | e | {
let username = format! ( "{}#{}" , author . name , author . discriminator ) ;
e . title ( "New suggestion" ) ;
e . description ( format! (
"{}\n\n[See original message]({})" ,
suggestion ,
ctx . msg . link ( ) ,
) ) ;
let mut embed_author = serenity ::builder ::CreateEmbedAuthor ::default ( ) ;
embed_author
. icon_url ( get_avatar ( & author ) )
. name ( username )
. url ( format! ( "https://discord.com/users/{}" , author . id ) ) ;
e . set_author ( embed_author ) ;
if let Some ( accent_color ) = accent_color {
e . color ( accent_color ) ;
}
e
} ) ;
m
} )
. await
. unwrap ( ) ;
ctx . msg . reply ( & ctx . discord . http , "Suggestion sent! Thank you for making a suggestion. If it is chosen to be used in a future challenge, you will be mentioned in the challenge description!" ) . await ? ;
Ok ( ( ) )
}