From 6ea30897b4b2c71363f1180f5e2ebb20f56b50b4 Mon Sep 17 00:00:00 2001 From: Lemongrass3110 Date: Fri, 15 Jan 2021 13:38:03 +0100 Subject: [PATCH] Refactored automatic mail deletion (#4569) Added a configuration that allows you to delete mails without attachment instantly after the given interval. Introduced a define for the iteration size of how many mails will be deleted in each circle. It his highly recommended to not increase it too high or your char server might lag, if you have a lot of mails on your server. Always delete all mails sent by the server after the given interval in a single statement and ignoring the iteration size. Added transaction safety for mail deletion. With this a mail and it's attachments will always be safely deleted together. Fixed a duplication issue, when deletion of the mail would have failed, but the mail was still returned. Fixed notification of online characters in certain rate circumstances. --- conf/char_athena.conf | 4 + src/char/char.cpp | 6 +- src/char/char.hpp | 1 + src/char/int_mail.cpp | 182 ++++++++++++++++++++++++++++-------------- src/common/mmo.hpp | 3 + 5 files changed, 132 insertions(+), 64 deletions(-) diff --git a/conf/char_athena.conf b/conf/char_athena.conf index 2091a05d49..d448d5fb61 100644 --- a/conf/char_athena.conf +++ b/conf/char_athena.conf @@ -290,4 +290,8 @@ mail_delete_days: 15 // Default: yes mail_retrieve: yes +// Should mails without any attachments be returned to their sender? +// Default: yes +mail_return_empty: yes + import: conf/import/char_conf.txt diff --git a/src/char/char.cpp b/src/char/char.cpp index 18adc2ca2b..4711a15ba1 100644 --- a/src/char/char.cpp +++ b/src/char/char.cpp @@ -3070,6 +3070,8 @@ bool char_config_read(const char* cfgName, bool normal){ charserv_config.mail_return_days = atoi(w2); } else if (strcmpi(w1, "mail_delete_days") == 0) { charserv_config.mail_delete_days = atoi(w2); + } else if (strcmpi(w1, "mail_return_empty") == 0) { + charserv_config.mail_return_empty = config_switch(w2); } else if (strcmpi(w1, "allowed_job_flag") == 0) { charserv_config.allowed_job_flag = atoi(w2); } else if (strcmpi(w1, "import") == 0) { @@ -3245,11 +3247,11 @@ int do_init(int argc, char **argv) // periodically check if mails need to be returned to their sender add_timer_func_list(mail_return_timer, "mail_return_timer"); - add_timer_interval(gettick() + 1000, mail_return_timer, 0, 0, 5 * 60 * 1000); // every 5 minutes + add_timer_interval(gettick() + 1000, mail_return_timer, 0, 0, 1 * 60 * 1000); // every minute // periodically check if mails need to be deleted completely add_timer_func_list(mail_delete_timer, "mail_delete_timer"); - add_timer_interval(gettick() + 1000, mail_delete_timer, 0, 0, 5 * 60 * 1000); // every 5 minutes + add_timer_interval(gettick() + 1000, mail_delete_timer, 0, 0, 1 * 60 * 1000); // every minute //check db tables if(charserv_config.char_check_db && char_checkdb() == 0){ diff --git a/src/char/char.hpp b/src/char/char.hpp index 5d9a6d23a5..6db2ea75f1 100644 --- a/src/char/char.hpp +++ b/src/char/char.hpp @@ -188,6 +188,7 @@ struct CharServ_Config { int mail_return_days; int mail_delete_days; int mail_retrieve; + int mail_return_empty; int allowed_job_flag; }; diff --git a/src/char/int_mail.cpp b/src/char/int_mail.cpp index 4a9a30d6bc..caf28edc62 100644 --- a/src/char/int_mail.cpp +++ b/src/char/int_mail.cpp @@ -17,8 +17,8 @@ #include "inter.hpp" bool mail_loadmessage(int mail_id, struct mail_message* msg); -void mapif_Mail_return(int fd, uint32 char_id, int mail_id); -void mapif_Mail_delete(int fd, uint32 char_id, int mail_id, bool deleted); +void mapif_Mail_return( int fd, uint32 char_id, int mail_id, uint32 account_id_receiver = 0, uint32 account_id_sender = 0 ); +bool mapif_Mail_delete( int fd, uint32 char_id, int mail_id, uint32 account_id = 0 ); int mail_fromsql(uint32 char_id, struct mail_data* md) { @@ -82,6 +82,11 @@ int mail_savemessage(struct mail_message* msg) int i, j; bool found = false; + if( SQL_ERROR == Sql_QueryStr( sql_handle, "START TRANSACTION" ) ){ + Sql_ShowDebug( sql_handle ); + return 0; + } + // build message save query StringBuf_Init(&buf); StringBuf_Printf(&buf, "INSERT INTO `%s` (`send_name`, `send_id`, `dest_name`, `dest_id`, `title`, `message`, `time`, `status`, `zeny`,`type`", schema_config.mail_db); @@ -99,6 +104,7 @@ int mail_savemessage(struct mail_message* msg) { SqlStmt_ShowDebug(stmt); StringBuf_Destroy(&buf); + Sql_QueryStr( sql_handle, "ROLLBACK" ); return msg->id = 0; } else msg->id = (int)SqlStmt_LastInsertId(stmt); @@ -140,10 +146,17 @@ int mail_savemessage(struct mail_message* msg) if( found && SQL_ERROR == Sql_QueryStr(sql_handle, StringBuf_Value(&buf)) ){ Sql_ShowDebug(sql_handle); + msg->id = 0; + Sql_QueryStr( sql_handle, "ROLLBACK" ); } StringBuf_Destroy(&buf); + if( msg->id && SQL_ERROR == Sql_QueryStr( sql_handle, "COMMIT" ) ){ + Sql_ShowDebug( sql_handle ); + return 0; + } + return msg->id; } @@ -236,14 +249,18 @@ bool mail_loadmessage(int mail_id, struct mail_message* msg) } int mail_timer_sub( int limit, enum mail_inbox_type type ){ + // Start by deleting all expired mails sent by the server + if( SQL_ERROR == Sql_Query( sql_handle, "DELETE FROM `%s`WHERE `type` = '%d' AND `send_id` = '0' AND `time` <= UNIX_TIMESTAMP( NOW() - INTERVAL %d DAY )", schema_config.mail_db, type, limit ) ){ + Sql_ShowDebug( sql_handle ); + return 0; + } + struct{ int mail_id; int char_id; int account_id; - }mails[MAIL_MAX_INBOX]; - int i, map_fd; - char* data; - struct online_char_data* character; + int account_id_sender; + }mails[MAIL_ITERATION_SIZE]; if( limit <= 0 ){ return 0; @@ -251,7 +268,14 @@ int mail_timer_sub( int limit, enum mail_inbox_type type ){ memset(mails, 0, sizeof(mails)); - if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `id`,`char_id`,`account_id` FROM `%s` `m` INNER JOIN `%s` `c` ON `c`.`char_id`=`m`.`dest_id` WHERE `type` = '%d' AND `time` <= UNIX_TIMESTAMP( NOW() - INTERVAL %d DAY ) ORDER BY `id` LIMIT %d", schema_config.mail_db, schema_config.char_db, type, limit, MAIL_MAX_INBOX + 1) ){ + if( SQL_ERROR == Sql_Query( sql_handle, + "SELECT `m`.`id`, `c`.`char_id`, `c`.`account_id`, `c2`.`account_id` " + "FROM `%s` `m` " + "INNER JOIN `%s` `c` ON `c`.`char_id`=`m`.`dest_id` " + "INNER JOIN `%s` `c2` ON `c2`.`char_id`=`m`.`send_id` " + "WHERE `type` = '%d' AND `time` <= UNIX_TIMESTAMP( NOW() - INTERVAL %d DAY ) " + "ORDER BY `id` LIMIT %d", + schema_config.mail_db, schema_config.char_db, schema_config.char_db, type, limit, MAIL_ITERATION_SIZE + 1 ) ){ Sql_ShowDebug(sql_handle); return 0; } @@ -260,31 +284,26 @@ int mail_timer_sub( int limit, enum mail_inbox_type type ){ return 0; } - for( i = 0; i < MAIL_MAX_INBOX && SQL_SUCCESS == Sql_NextRow(sql_handle); i++ ){ + for( int i = 0; i < MAIL_ITERATION_SIZE && SQL_SUCCESS == Sql_NextRow( sql_handle ); i++ ){ + char* data; + Sql_GetData(sql_handle, 0, &data, NULL); mails[i].mail_id = atoi(data); Sql_GetData(sql_handle, 1, &data, NULL); mails[i].char_id = atoi(data); Sql_GetData(sql_handle, 2, &data, NULL); mails[i].account_id = atoi(data); + Sql_GetData( sql_handle, 3, &data, NULL ); mails[i].account_id_sender = atoi( data ); } Sql_FreeResult(sql_handle); - for( i = 0; i < MAIL_MAX_INBOX; i++ ){ + for( int i = 0; i < MAIL_ITERATION_SIZE; i++ ){ if( mails[i].mail_id == 0 ){ break; } - // Check for online players - if( ( character = (struct online_char_data*)idb_get(char_get_onlinedb(), mails[i].account_id) ) != NULL && character->server >= 0 ){ - map_fd = map_server[character->server].fd; - }else{ - map_fd = 0; - } - if( type == MAIL_INBOX_NORMAL ){ - mapif_Mail_return( 0, mails[i].char_id, mails[i].mail_id ); - mapif_Mail_delete( map_fd, mails[i].char_id, mails[i].mail_id, true ); + mapif_Mail_return( 0, mails[i].char_id, mails[i].mail_id, mails[i].account_id, mails[i].account_id_sender ); }else if( type == MAIL_INBOX_RETURNED ){ - mapif_Mail_delete( map_fd, mails[i].char_id, mails[i].mail_id, false ); + mapif_Mail_delete( 0, mails[i].char_id, mails[i].mail_id, mails[i].account_id ); }else{ // Should not happen continue; @@ -420,22 +439,36 @@ void mapif_parse_Mail_getattach(int fd) /*========================================== * Delete Mail *------------------------------------------*/ -void mapif_Mail_delete(int fd, uint32 char_id, int mail_id, bool deleted) -{ +bool mapif_Mail_delete( int fd, uint32 char_id, int mail_id, uint32 account_id ){ bool failed = false; - if( !deleted ){ - if ( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id) || - SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id) ) - { - Sql_ShowDebug(sql_handle); - failed = true; + if( SQL_ERROR == Sql_QueryStr( sql_handle, "START TRANSACTION" ) || + SQL_ERROR == Sql_Query( sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id ) || + SQL_ERROR == Sql_Query( sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id ) || + SQL_ERROR == Sql_QueryStr( sql_handle, "COMMIT" ) ){ + + Sql_ShowDebug( sql_handle ); + Sql_QueryStr( sql_handle, "ROLLBACK" ); + + // We do not want to trigger failure messages, if the map server did not send a request + if( fd <= 0 ){ + return false; } + + failed = true; } - // Only if the request came from a map-server and was not timer triggered for an offline character + // If the char server triggered this, check if we have to notify a map server if( fd <= 0 ){ - return; + struct online_char_data* character; + + // Check for online players + if( ( character = (struct online_char_data*)idb_get( char_get_onlinedb(), account_id ) ) != nullptr && character->server >= 0 ){ + fd = map_server[character->server].fd; + }else{ + // The request was triggered inside the character server or the player is offline now + return !failed; + } } WFIFOHEAD(fd,11); @@ -444,11 +477,13 @@ void mapif_Mail_delete(int fd, uint32 char_id, int mail_id, bool deleted) WFIFOL(fd,6) = mail_id; WFIFOB(fd,10) = failed; WFIFOSET(fd,11); + + return !failed; } void mapif_parse_Mail_delete(int fd) { - mapif_Mail_delete(fd, RFIFOL(fd,2), RFIFOL(fd,6), false); + mapif_Mail_delete( fd, RFIFOL( fd, 2 ), RFIFOL( fd, 6 ) ); } /*========================================== @@ -473,45 +508,68 @@ void mapif_Mail_new(struct mail_message *msg) /*========================================== * Return Mail *------------------------------------------*/ -void mapif_Mail_return(int fd, uint32 char_id, int mail_id) -{ +void mapif_Mail_return( int fd, uint32 char_id, int mail_id, uint32 account_id_receiver, uint32 account_id_sender ){ struct mail_message msg; - int new_mail = 0; - if( mail_loadmessage(mail_id, &msg) ) - { - if( msg.dest_id != char_id) + if( !mail_loadmessage( mail_id, &msg ) ){ + return; + } + + if( msg.dest_id != char_id ){ + return; + } + + if( !mapif_Mail_delete( 0, char_id, mail_id, account_id_receiver ) ){ + // Stop processing to not duplicate the mail + return; + } + + // If it was sent by the server we do not want to return the mail + if( msg.send_id == 0 ){ + return; + } + + // If we do not want to return mails without any attachments and the request was not sent by a user + if( fd <= 0 && !charserv_config.mail_return_empty ){ + int i; + + ARR_FIND( 0, MAIL_MAX_ITEM, i, msg.item[i].nameid > 0 ); + + if( i == MAIL_MAX_ITEM && msg.zeny == 0 ){ return; - else if( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id) - || SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id) ) - Sql_ShowDebug(sql_handle); - // If it was not sent by the server, since we do not want to return mails to the server - else if( msg.send_id != 0 ) - { - char temp_[MAIL_TITLE_LENGTH + 3]; - - // swap sender and receiver - SWAP(msg.send_id, msg.dest_id); - safestrncpy(temp_, msg.send_name, NAME_LENGTH); - safestrncpy(msg.send_name, msg.dest_name, NAME_LENGTH); - safestrncpy(msg.dest_name, temp_, NAME_LENGTH); - - // set reply message title - snprintf(temp_, sizeof(temp_), "RE:%s", msg.title); - safestrncpy(msg.title, temp_, sizeof(temp_)); - - msg.status = MAIL_NEW; - msg.type = MAIL_INBOX_RETURNED; - msg.timestamp = time(NULL); - - new_mail = mail_savemessage(&msg); - mapif_Mail_new(&msg); } } - // Only if the request came from a map-server and was not timer triggered for an offline character + char temp_[MAIL_TITLE_LENGTH + 3]; + + // swap sender and receiver + SWAP( msg.send_id, msg.dest_id ); + safestrncpy( temp_, msg.send_name, NAME_LENGTH ); + safestrncpy( msg.send_name, msg.dest_name, NAME_LENGTH ); + safestrncpy( msg.dest_name, temp_, NAME_LENGTH ); + + // set reply message title + snprintf( temp_, sizeof( temp_ ), "RE:%s", msg.title ); + safestrncpy( msg.title, temp_, sizeof( temp_ ) ); + + msg.status = MAIL_NEW; + msg.type = MAIL_INBOX_RETURNED; + msg.timestamp = time( NULL ); + + int new_mail = mail_savemessage( &msg ); + mapif_Mail_new( &msg ); + + // If the char server triggered this, check if we have to notify a map server if( fd <= 0 ){ - return; + struct online_char_data* character; + + // Check for online players + if( ( character = (struct online_char_data*)idb_get( char_get_onlinedb(), account_id_sender ) ) != nullptr && character->server >= 0 ){ + fd = map_server[character->server].fd; + }else{ + // The request was triggered inside the character server or the player is offline now + return; + } } WFIFOHEAD(fd,11); diff --git a/src/common/mmo.hpp b/src/common/mmo.hpp index 8e99be553a..e688eafc3f 100644 --- a/src/common/mmo.hpp +++ b/src/common/mmo.hpp @@ -150,6 +150,9 @@ const t_itemid WEDDING_RING_F = 2635; #define MAIL_MAX_ITEM 5 #define MAIL_PAGE_SIZE 7 #endif +#ifndef MAIL_ITERATION_SIZE + #define MAIL_ITERATION_SIZE 100 +#endif //Mercenary System #define MC_SKILLBASE 8201