diff options
author | Ratakor <ratakor@disroot.org> | 2023-05-28 09:19:31 +0200 |
---|---|---|
committer | Ratakor <ratakor@disroot.org> | 2023-05-28 09:19:31 +0200 |
commit | afd1bd5a1c6acefeabc9b0179c6b1bd31f33d2b0 (patch) | |
tree | e13f0b7424e46337c430a984d039beaeba5d1fb5 | |
parent | 69e1dbc29758d8ab3bda6c9e2f40ddd962f20515 (diff) |
! Move everything to separate files + add / cmdsv0.0.5
-rw-r--r-- | README.md | 9 | ||||
-rw-r--r-- | config.def.h | 10 | ||||
-rw-r--r-- | src/cmd_help.c | 54 | ||||
-rw-r--r-- | src/cmd_info.c | 189 | ||||
-rw-r--r-- | src/cmd_leaderboard.c | 243 | ||||
-rw-r--r-- | src/cmd_source.c | 89 | ||||
-rw-r--r-- | src/init.c | 196 | ||||
-rw-r--r-- | src/main.c | 69 | ||||
-rw-r--r-- | src/nolan.c | 1108 | ||||
-rw-r--r-- | src/nolan.h | 83 | ||||
-rw-r--r-- | src/ocr.c | 68 | ||||
-rw-r--r-- | src/raids.c | 31 | ||||
-rw-r--r-- | src/stats.c | 429 | ||||
-rw-r--r-- | src/util.c | 8 | ||||
-rw-r--r-- | src/util.h | 1 |
15 files changed, 1461 insertions, 1126 deletions
@@ -7,16 +7,13 @@ A discord bot for Orna. ## TODO: -- use / instead of prefix -> rewrite on_help() -- separate everything in multiple files -- store images and source in a sane directory - +- raids +- improve tesseract recognition by binarzing the image with opencv and adding +usernames to tesseract dictionary - refactor parseline() -- improve tesseract recognition smh - add player with name in event->content for ham - add option to correct stats -- raids - automatic roles attribution (for Orna FR) -> with updatemsg - ascensions (track mats) - detect wrong screenshot diff --git a/config.def.h b/config.def.h index 186a817..3484f5d 100644 --- a/config.def.h +++ b/config.def.h @@ -1,4 +1,3 @@ -#define RAIDS_ID 1109604402102276217 /* Channel ID for raids */ #define DELIM ',' /* Delimiter for the save file */ #define FILENAME "source.csv" /* Filename of the save file */ #define LB_MAX 10 /* Max players to be shown with !lb */ @@ -7,14 +6,19 @@ #define PREFIX "?" static const int kingdom_verification = 0; /* 0 means no verification */ -static const int use_embed = 1; /* 0 means no embed on info */ +static const int use_embed = 0; /* 0 means no embed on info */ -/* list of ids to check for stats image */ +/* list of channel ids to check for stats screenshots */ static const u64snowflake stats_ids[] = { 1110185440285302855, 1110767040890941590, }; +/* list of channel ids to check for raids screenshots */ +static const u64snowflake raids_ids[] = { + 1109604402102276217, +}; + /* list of kingdom to accept if verification is on */ static const char *kingdoms[] = { "Scream of Terra", diff --git a/src/cmd_help.c b/src/cmd_help.c new file mode 100644 index 0000000..67d8c9c --- /dev/null +++ b/src/cmd_help.c @@ -0,0 +1,54 @@ +#include <string.h> +#include "nolan.h" + +void +create_slash_help(struct discord *client) +{ + struct discord_create_global_application_command cmd = { + .name = "help", + .description = "Shows help", + }; + discord_create_global_application_command(client, APP_ID, &cmd, NULL); +} + +void +help(char *buf, size_t siz) +{ + char *p; + unsigned long i, len = LENGTH(stats_ids); + + cpstr(buf, "Post a screenshot of your stats to ", siz); + for (i = 0; i < len; i++) { + p = strchr(buf, '\0');; + snprintf(p, siz, "<#%lu> ", stats_ids[i]); + if (i < len - 1) + catstr(buf, "or ", siz); + } + catstr(buf, "to enter the database.\n", siz); + catstr(buf, "Commands:\n", siz); + catstr(buf, "\t?info *[[@]user]*\n", siz); + catstr(buf, "\t?leaderboard or ?lb *category*\n", siz); + /* catstr(buf, "\t?correct [category] [corrected value]\n", siz); */ + catstr(buf, "\t?source or ?src *[kingdom]*\n", siz); + catstr(buf, "\t/help\n\n", siz); + catstr(buf, "[...] means optional.\n", siz); + catstr(buf, "Also works with /", siz); +} + +void +on_help(struct discord * client, const struct discord_message * event) +{ + size_t siz = DISCORD_MAX_MESSAGE_LEN; + char buf[DISCORD_MAX_MESSAGE_LEN]; + +#ifdef DEVEL + if (event->channel_id != DEVEL) + return; +#endif + + help(buf, siz); + struct discord_create_message msg = { + .content = buf + }; + discord_create_message(client, event->channel_id, &msg, NULL); +} diff --git a/src/cmd_info.c b/src/cmd_info.c new file mode 100644 index 0000000..3cb06b8 --- /dev/null +++ b/src/cmd_info.c @@ -0,0 +1,189 @@ +#include <stdlib.h> +#include "string.h" +#include "nolan.h" + +#define ICON_URL "https://orna.guide/static/orna/img/npcs/master_gnome.png" + +static u64snowflake str_to_uid(char *id); +static void write_invalid(char *buf, size_t siz); +static void write_info_embed(struct discord *client, char *buf, size_t siz, int index); +static void write_info(char *buf, size_t siz, int index); + +void +create_slash_info(struct discord *client) +{ + struct discord_application_command_option options[] = { + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "user", + .description = "discord username or @", + }, + }; + struct discord_create_global_application_command cmd = { + .name = "info", + .description = "Shows Orna information about a user", + .options = &(struct discord_application_command_options) + { + .size = LENGTH(options), + .array = options + } + }; + discord_create_global_application_command(client, APP_ID, &cmd, NULL); +} + +static u64snowflake +str_to_uid(char *id) +{ + char *start = id, *end = strchr(id, '\0') - 1; + + if (strncmp(start, "<@", 2) == 0 && strncmp(end, ">", 1) == 0) + return strtoul(start + 2, NULL, 10); + return 0; +} + +static void +write_invalid(char *buf, size_t siz) +{ + cpstr(buf, "This player does not exist in the database.\n", siz); + catstr(buf, "To check a player's info type /info ", siz); + catstr(buf, "@username or /info username.\n", siz); + catstr(buf, "To check your info just type ?info.", siz); +} + +/* FIXME */ +/* static void */ +/* write_info_embed(struct discord *client, char *buf, size_t siz, int index) */ +/* { */ +/* unsigned long i; */ +/* char *p; */ + +/* struct discord_embed embed = { */ +/* .color = 0x3498DB, */ +/* .timestamp = discord_timestamp(client), */ +/* .title = players[index].name */ +/* }; */ +/* discord_embed_set_footer(&embed, "Nolan", ICON_URL, NULL); */ +/* discord_embed_add_field( */ +/* &embed, (char *)fields[1], players[index].kingdom, true); */ +/* for (i = 2; i < LENGTH(fields) - 1; i++) { */ +/* if (i == 7) { /1* playtime *1/ */ +/* p = playtime_to_str(((long *)&players[index])[i]); */ +/* discord_embed_add_field( */ +/* &embed, (char *)fields[i], p, true); */ +/* free(p); */ +/* } else { */ +/* sprintf(buf, "%'ld", ((long *)&players[index])[i]); */ +/* if (i == 18) /1* distance *1/ */ +/* strcat(buf, "m"); */ +/* discord_embed_add_field( */ +/* &embed, (char *)fields[index], buf, true); */ +/* } */ +/* } */ +/* struct discord_create_message msg = { */ +/* .embeds = &(struct discord_embeds) */ +/* { */ +/* .size = 1, */ +/* .array = &embed, */ +/* } */ +/* }; */ +/* } */ + +static void +write_info(char *buf, size_t siz, int index) +{ + unsigned long i; + char *p; + + buf[0] = '\0'; + for (i = 0; i < LENGTH(fields) - 1; i++) { + catstr(buf, fields[i], siz); + catstr(buf, ": ", siz); + if (i <= 1) { /* name and kingdom */ + catstr(buf, ((char **)&players[index])[i], siz); + } else if (i == 7) { /* playtime */ + p = playtime_to_str(((long *)&players[index])[i]); + catstr(buf, p, siz); + free(p); + } else { + p = strchr(buf, '\0'); + snprintf(p, siz, "%'ld", ((long *)&players[index])[i]); + if (i == 18) /* distance */ + catstr(buf, "m", siz); + } + catstr(buf, "\n", siz); + } +} + +void +info_from_uid(char *buf, size_t siz, u64snowflake userid) +{ + unsigned long i = 0; + + while (i < nplayers && players[i].userid != userid) + i++; + + if (i == nplayers) + write_invalid(buf, siz); + else + write_info(buf, siz, i); +} + +void +info_from_txt(char *buf, size_t siz, char *txt) +{ + unsigned long i = 0; + u64snowflake userid; + + userid = str_to_uid(txt); + if (userid > 0) { + info_from_uid(buf, siz, userid); + return; + } + + while (i < nplayers && strcmp(players[i].name, txt) != 0) + i++; + if (i == nplayers) + write_invalid(buf, siz); + else + write_info(buf, siz, i); +} + +void +on_info(struct discord *client, const struct discord_message *event) +{ + size_t siz = DISCORD_MAX_MESSAGE_LEN; + char buf[siz]; + + if (event->author->bot) + return; + +#ifdef DEVEL + if (event->channel_id != DEVEL) + return; +#endif + + if (strlen(event->content) == 0) + info_from_uid(buf, siz, event->author->id); + else + info_from_txt(buf, siz, event->content); + + /* + if (use_embed) { + write_info_embed(client, buf, siz, i); + struct discord_create_message msg = { + .embeds = &(struct discord_embeds) + { + .size = 1, + .array = &embed, + } + }; + discord_create_message(client, event->channel_id, &msg, NULL); + discord_embed_cleanup(&embed); + } + */ + + struct discord_create_message msg = { + .content = buf + }; + discord_create_message(client, event->channel_id, &msg, NULL); +} diff --git a/src/cmd_leaderboard.c b/src/cmd_leaderboard.c new file mode 100644 index 0000000..daac18c --- /dev/null +++ b/src/cmd_leaderboard.c @@ -0,0 +1,243 @@ +#include <string.h> +#include <stdlib.h> +#include "nolan.h" + +static void write_invalid(char *buf, size_t siz); +static int compare(const void *p1, const void *p2); +static void write_player(char *buf, size_t siz, int i); +static void write_leaderboard(char *buf, size_t siz, u64snowflake userid); + +static int category = 0; + +void +create_slash_leaderboard(struct discord *client) +{ + struct discord_application_command_option_choice category_choices[] = { + { + .name = "Level", + .value = "\"Level\"", + }, + { + .name = "Ascension", + .value = "\"Ascension\"", + }, + { + .name = "Global Rank", + .value = "\"Global Rank\"", + }, + { + .name = "Competitive Rank", + .value = "\"Competitive Rank\"", + }, + { + .name = "Playtime", + .value = "\"Playtime\"", + }, + { + .name = "Monsters Slain", + .value = "\"Monsters Slain\"", + }, + { + .name = "Bosses Slain", + .value = "\"Bosses Slain\"", + }, + { + .name = "Players Defeated", + .value = "\"Players Defeated\"", + }, + { + .name = "Quests Completed", + .value = "\"Quests Completed\"", + }, + { + .name = "Areas Explored", + .value = "\"Areas Explored\"", + }, + { + .name = "Areas Taken", + .value = "\"Areas Taken\"", + }, + { + .name = "Dungeons Cleared", + .value = "\"Dungeons Cleared\"", + }, + { + .name = "Coliseum Wins", + .value = "\"Coliseum Wins\"", + }, + { + .name = "Items Upgraded", + .value = "\"Items Upgraded\"", + }, + { + .name = "Fish Caught", + .value = "\"Fish Caught\"", + }, + { + .name = "Distance Travelled", + .value = "\"Distance Travelled\"", + }, + { + .name = "Reputation", + .value = "\"Reputation\"", + }, + { + .name = "Endless Record", + .value = "\"Endless Record\"", + }, + { + .name = "Codex", + .value = "\"Codex\"", + }, + }; + + struct discord_application_command_option options[] = { + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "category", + .description = "category name", + .choices = &(struct discord_application_command_option_choices) + { + .size = LENGTH(category_choices), + .array = category_choices, + }, + .required = true + }, + }; + struct discord_create_global_application_command cmd = { + .name = "leaderboard", + .description = "Shows the leaderboard based on a category", + .options = &(struct discord_application_command_options) + { + .size = LENGTH(options), + .array = options + }, + }; + discord_create_global_application_command(client, APP_ID, &cmd, NULL); +} + +static void +write_invalid(char *buf, size_t siz) +{ + unsigned long i; + + cpstr(buf, "NO WRONG, this is not a valid category.\n", siz); + catstr(buf, "Valid categories are:\n", siz); + for (i = 2; i < LENGTH(fields) - 1; i++) { + if (i == 5) /* regional rank */ + continue; + catstr(buf, fields[i], siz); + catstr(buf, "\n", siz); + } +} + +static int +compare(const void *p1, const void *p2) +{ + const long l1 = ((long *)(Player *)p1)[category]; + const long l2 = ((long *)(Player *)p2)[category]; + + /* ranks */ + if (category == 4 || category == 6) { + if (l1 == 0) + return 1; + if (l2 == 0) + return -1; + return l1 - l2; + } + + return l2 - l1; +} + +static void +write_player(char *buf, size_t siz, int i) +{ + size_t ssiz = 32; + char *plt, stat[ssiz]; + + /* *buf = '\0'; */ + snprintf(buf, siz, "%d. %s: ", i + 1, players[i].name); + if (category == 7) { /* playtime */ + plt = playtime_to_str(((long *)&players[i])[category]); + catstr(buf, plt, siz); + free(plt); + } else { + snprintf(stat, ssiz, "%'ld", ((long *)&players[i])[category]); + catstr(buf, stat, siz); + if (i == 18) /* distance */ + catstr(buf, "m", siz); + } + catstr(buf, "\n", siz); +} + +static void +write_leaderboard(char *buf, size_t siz, u64snowflake userid) +{ + int in_lb = 0; + unsigned long i, lb_max = MIN(nplayers, LB_MAX); + size_t psiz = 128; + char player[psiz]; + /* siz = (lb_max + 2) * 64; */ + + cpstr(buf, fields[category], siz); + catstr(buf, ":\n", siz); + for (i = 0; i < lb_max ; i++) { + if (userid == players[i].userid) + in_lb = 1; + write_player(player, psiz, i); + catstr(buf, player, siz); + } + + if (!in_lb) { + catstr(buf, "...\n", siz); + i = lb_max; + while (i < nplayers && players[i].userid != userid) + i++; + write_player(player, psiz, i); + catstr(buf, player, siz); + } +} + +void +leaderboard(char *buf, size_t siz, char *txt, u64snowflake userid) +{ + unsigned long i = 2; /* ignore name and kingdom */ + + while (i < LENGTH(fields) - 1 && + strcasecmp(fields[i], txt) != 0) + i++; + + if (i == LENGTH(fields) - 1 || i == 5) { /* 5 = regional rank */ + write_invalid(buf, siz); + return; + } + + category = i; + qsort(players, nplayers, sizeof(players[0]), compare); + write_leaderboard(buf, siz, userid); +} + +void +on_leaderboard(struct discord *client, const struct discord_message *event) +{ + size_t siz = DISCORD_MAX_MESSAGE_LEN; + char buf[siz]; + + if (event->author->bot) + return; + +#ifdef DEVEL + if (event->channel_id != DEVEL) + return; +#endif + + if (strlen(event->content) == 0) + write_invalid(buf, siz); + else + leaderboard(buf, siz, event->content, event->author->id); + + struct discord_create_message msg = { + .content = buf + }; + discord_create_message(client, event->channel_id, &msg, NULL); +} diff --git a/src/cmd_source.c b/src/cmd_source.c new file mode 100644 index 0000000..f48a27f --- /dev/null +++ b/src/cmd_source.c @@ -0,0 +1,89 @@ +#include <string.h> +#include <stdlib.h> +#include "nolan.h" + +void +create_slash_source(struct discord *client) +{ + struct discord_application_command_option options[] = { + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "kingdom", + .description = "optional kingdom name to sort the file", + }, + }; + struct discord_create_global_application_command cmd = { + .name = "source", + .description = "Returns the source file with everyone stats", + .options = &(struct discord_application_command_options) + { + .size = LENGTH(options), + .array = options + }, + }; + discord_create_global_application_command(client, APP_ID, &cmd, NULL); +} + +char * +sort_source(char *kingdom, size_t *fszp) +{ + FILE *fp; + char *res, line[LINE_SIZE], *kd, *endkd; + size_t mfsz = LINE_SIZE + nplayers * LINE_SIZE; + + if ((fp = fopen(STATS_FILE, "r")) == NULL) + die("nolan: Failed to open %s (read)\n", STATS_FILE); + + res = malloc(mfsz); + fgets(line, LINE_SIZE, fp); /* fields name */ + cpstr(res, line, mfsz); + while (fgets(line, LINE_SIZE, fp) != NULL) { + kd = strchr(line, DELIM) + 1; + endkd = nstrchr(line, DELIM, 2); + if (endkd) + *endkd = '\0'; + if (strcmp(kd, kingdom) == 0) { + *endkd = DELIM; + catstr(res, line, mfsz); + } + } + + fclose(fp); + *fszp = strlen(res); + return res; +} + +void +on_source(struct discord *client, const struct discord_message *event) +{ + size_t fsize = 0; + char *fbuf = NULL; + + if (event->author->bot) + return; + +#ifdef DEVEL + if (event->channel_id != DEVEL) + return; +#endif + + if (strlen(event->content) == 0) + fbuf = cog_load_whole_file(STATS_FILE, &fsize); + else + fbuf = sort_source(event->content, &fsize); + + struct discord_attachment attachment = { + .filename = STATS_FILE, + .content = fbuf, + .size = fsize + }; + struct discord_attachments attachments = { + .size = 1, + .array = &attachment + }; + struct discord_create_message msg = { + .attachments = &attachments + }; + discord_create_message(client, event->channel_id, &msg, NULL); + free(fbuf); +} diff --git a/src/init.c b/src/init.c new file mode 100644 index 0000000..09af70e --- /dev/null +++ b/src/init.c @@ -0,0 +1,196 @@ +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include "nolan.h" + +static int file_exists(char *filename); + +static int +file_exists(char *filename) +{ + struct stat buf; + return (stat(filename, &buf) == 0); +} + +void +create_folders(void) +{ + if (!file_exists(SAVE_FOLDER)) { + if (mkdir(SAVE_FOLDER, 0755) == -1) + die("nolan: Failed to create %s\n", SAVE_FOLDER); + } + if (!file_exists(IMAGE_FOLDER)) { + if (mkdir(IMAGE_FOLDER, 0755) == -1) + die("nolan: Failed to create %s\n", IMAGE_FOLDER); + } +} + + +void +create_stats_file(void) +{ + FILE *fp; + unsigned long i; + long size = 0; + + fp = fopen(STATS_FILE, "r"); + + if (fp != NULL) { + fseek(fp, 0, SEEK_END); + size = ftell(fp); + } + + if (size == 0 && (fp = fopen(STATS_FILE, "w")) != NULL) { + for (i = 0; i < LENGTH(fields) - 1; i++) + fprintf(fp, "%s%c", fields[i], DELIM); + fprintf(fp, "%s\n", fields[LENGTH(fields) - 1]); + } + + fclose(fp); +} + +void +create_slash_commands(struct discord *client) +{ + create_slash_help(client); + create_slash_info(client); + create_slash_leaderboard(client); + /* create_slash_source(client); */ +} + +void +init_players(void) +{ + FILE *fp; + char buf[LINE_SIZE]; + unsigned long i; + + if ((fp = fopen(STATS_FILE, "r")) == NULL) + die("nolan: Failed to open %s (read)\n", STATS_FILE); + + while (fgets(buf, LINE_SIZE, fp)) + nplayers++; + nplayers--; /* first line is not a player */ + + if (nplayers > MAX_PLAYERS) + die("nolan: There is too much players to load (max:%d)\n", + MAX_PLAYERS); + + for (i = 0; i < nplayers; i++) + players[i] = create_player(i + 2); +} + +/* TODO: handle files and embed (and no opt/opt better), ephemeral... */ +void +on_interaction(struct discord *client, const struct discord_interaction *event) +{ + size_t siz = DISCORD_MAX_MESSAGE_LEN; + char buf[DISCORD_MAX_MESSAGE_LEN] = ""; + + if (event->type != DISCORD_INTERACTION_APPLICATION_COMMAND) + return; + /* if (!event->data || !event->data->options) */ + /* return; */ + +#ifdef DEVEL + if (event->channel_id != DEVEL) + return; +#endif + + if (strcmp(event->data->name, "help") == 0) { + help(buf, siz); + } else if (strcmp(event->data->name, "info") == 0) { + if (!event->data || !event->data->options) { + info_from_uid(buf, siz, event->member->user->id); + } else { + info_from_txt(buf, siz, + event->data->options->array[0].value); + } + } else if (strcmp(event->data->name, "leaderboard") == 0) { + if (event->data && event->data->options) { + leaderboard(buf, siz, + event->data->options->array[0].value, + event->member->user->id); + } + } /*else if (strcmp(event->data->name, "source") == 0) { + if (!event->data || !event->data->options) { + source(buf, siz); + } else { + source_sorted(buf, siz, + event->data->options->array[0].value); + } + }*/ + + struct discord_interaction_response params = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) + { + .content = buf, + /* .flags = DISCORD_MESSAGE_EPHEMERAL */ + } + }; + + discord_create_interaction_response(client, event->id, event->token, + ¶ms, NULL); +} + +void +on_ready(struct discord *client, const struct discord_ready *event) +{ + struct discord_activity activities[] = { + { + .name = "/help", + .type = DISCORD_ACTIVITY_LISTENING, + }, + }; + + struct discord_presence_update status = { + .activities = + &(struct discord_activities) + { + .size = LENGTH(activities), + .array = activities + }, + .status = "online", + .afk = false, + .since = discord_timestamp(client), + }; + + discord_update_presence(client, &status); +} + + +void +on_message(struct discord *client, const struct discord_message *event) +{ + unsigned long i; + + if (event->author->bot) + return; + if (event->attachments->size == 0) + return; + if (strchr(event->attachments->array->filename, '.') == NULL) + return; + if (strncmp(event->attachments->array->content_type, "image/jpeg", + sizeof("image/jpeg") - 1) != 0) + return; + +#ifdef DEVEL + if (event->channel_id == DEVEL) + stats(client, event); + return; +#endif + + for (i = 0; i < LENGTH(stats_ids); i++) { + if (event->channel_id == stats_ids[i]) { + stats(client, event); + break; + } + } + for (i = 0; i < LENGTH(raids_ids); i++) { + if (event->channel_id == raids_ids[i]) { + raids(client, event); + break; + } + } +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..76f6be0 --- /dev/null +++ b/src/main.c @@ -0,0 +1,69 @@ +#include <stdlib.h> +#include <locale.h> +#include "nolan.h" + +Player players[MAX_PLAYERS]; +size_t nplayers; +const char *fields[] = { + "Name", + "Kingdom", + "Level", + "Ascension", + "Global Rank", + "Regional Rank", + "Competitive Rank", + "Playtime", + "Monsters Slain", + "Bosses Slain", + "Players Defeated", + "Quests Completed", + "Areas Explored", + "Areas Taken", + "Dungeons Cleared", + "Coliseum Wins", + "Items Upgraded", + "Fish Caught", + "Distance Travelled", + "Reputation", + "Endless Record", + "Codex", + "User ID", +}; + +int +main(void) +{ + char *src[] = { "src", "source" }; + char *lb[] = { "lb", "leaderboard" }; + struct discord *client; + +#ifndef DEVEL + if (getuid() != 0) + die("Please run nolan as root\n"); +#endif + + setlocale(LC_NUMERIC, ""); + create_folders(); + create_stats_file(); + init_players(); + + ccord_global_init(); + client = discord_init(TOKEN); + discord_add_intents(client, DISCORD_GATEWAY_MESSAGE_CONTENT); + discord_set_prefix(client, PREFIX); + create_slash_commands(client); + discord_set_on_ready(client, on_ready); + discord_set_on_interaction_create(client, &on_interaction); + discord_set_on_message_create(client, on_message); + discord_set_on_commands(client, lb, LENGTH(lb), on_leaderboard); + discord_set_on_command(client, "info", on_info); + discord_set_on_commands(client, src, LENGTH(src), on_source); + discord_set_on_command(client, "help", on_help); + + discord_run(client); + + discord_cleanup(client); + ccord_global_cleanup(); + + return EXIT_SUCCESS; +} diff --git a/src/nolan.c b/src/nolan.c deleted file mode 100644 index b5505b2..0000000 --- a/src/nolan.c +++ /dev/null @@ -1,1108 +0,0 @@ -#include <stdio.h> -#include <string.h> -#include <stdlib.h> -#include <locale.h> -#include <sys/stat.h> -#include <unistd.h> -#include <leptonica/allheaders.h> -#include <tesseract/capi.h> -#include <curl/curl.h> -#include <concord/discord.h> - -#include "util.h" -#include "../config.h" - -#define SAVE_FOLDER "/var/lib/nolan/" -#define IMAGE_FOLDER SAVE_FOLDER "images/" -#define STATS_FILE SAVE_FOLDER FILENAME -#define LINE_SIZE 300 + 1 -#define LEN(X) (sizeof X - 1) -#define MAX_PLAYERS LENGTH(kingdoms) * 50 -#define ICON_URL "https://orna.guide/static/orna/img/npcs/master_gnome.png" - -/* ALL FIELDS MUST HAVE THE SAME SIZE */ -typedef struct { - char *name; - char *kingdom; - long level; - long ascension; - long global; - long regional; - long competitive; - long playtime; - long monsters; - long bosses; - long players; - long quests; - long explored; - long taken; - long dungeons; - long coliseum; - long items; - long fish; - long distance; - long reputation; - long endless; - long codex; - u64snowflake userid; -} Player; - -static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream); -static void dlimg(char *url, char *fname); -static char *ocr(char *fname); -static Player loadplayer(unsigned int line); -static void initplayers(void); -static void updateplayers(Player *player); -static long playtimetolong(char *playtime, char *str); -static char *playtimetostr(long playtime); -static void trimall(char *str); -static void parseline(Player *player, char *line); -static void forline(Player *player, char *src); -static void createfile(void); -static int savetofile(Player *player); -static char *updatemsg(Player *player, int iplayer); -static char *loadfilekd(char *kingdom, size_t *fszp); -static int compare(const void *e1, const void *e2); -static char *playerinlb(int i); -static char *leaderboard(u64snowflake userid); -static char *invalidlb(void); -static u64snowflake useridtoul(char *id); -static void on_ready(struct discord *client, const struct discord_ready *event); -static void stats(struct discord *client, const struct discord_message *event); -static void raids(struct discord *client, const struct discord_message *event); -static void on_message(struct discord *client, const struct discord_message *event); -static void on_source(struct discord *client, const struct discord_message *event); -static void on_lb(struct discord *client, const struct discord_message *event); -static void on_info(struct discord *client, const struct discord_message *event); -static void on_help(struct discord *client, const struct discord_message *event); - -static Player players[MAX_PLAYERS]; -static size_t nplayers = 0; -static int category = 0; -static const char *fields[] = { - "Name", - "Kingdom", - "Level", - "Ascension", - "Global Rank", - "Regional Rank", - "Competitive Rank", - "Playtime", - "Monsters Slain", - "Bosses Slain", - "Players Defeated", - "Quests Completed", - "Areas Explored", - "Areas Taken", - "Dungeons Cleared", - "Coliseum Wins", - "Items Upgraded", - "Fish Caught", - "Distance Travelled", - "Reputation", - "Endless Record", - "Codex", - "User ID", -}; - -static size_t -write_data(void *ptr, size_t size, size_t nmemb, void *stream) -{ - size_t written = fwrite(ptr, size, nmemb, (FILE *)stream); - return written; -} - -void -dlimg(char *url, char *fname) -{ - CURL *handle; - FILE *fp; - - curl_global_init(CURL_GLOBAL_ALL); - - handle = curl_easy_init(); - - curl_easy_setopt(handle, CURLOPT_URL, url); - curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_data); - - if ((fp = fopen(fname, "wb")) == NULL) - die("nolan: Failed to open %s\n", fname); - - curl_easy_setopt(handle, CURLOPT_WRITEDATA, fp); - curl_easy_perform(handle); - - fclose(fp); - curl_easy_cleanup(handle); - curl_global_cleanup(); -} - -char * -ocr(char *fname) -{ - TessBaseAPI *handle; - PIX *img; - char *txt_ocr, *txt_out; - - if ((img = pixRead(fname)) == NULL) - die("nolan: Error reading image\n"); - - handle = TessBaseAPICreate(); - if (TessBaseAPIInit3(handle, NULL, "eng") != 0) - die("nolan: Error initialising tesseract\n"); - - TessBaseAPISetImage2(handle, img); - if (TessBaseAPIRecognize(handle, NULL) != 0) - die("nolan: Error in tesseract recognition\n"); - - txt_ocr = TessBaseAPIGetUTF8Text(handle); - txt_out = strdup(txt_ocr); - - TessDeleteText(txt_ocr); - TessBaseAPIEnd(handle); - TessBaseAPIDelete(handle); - pixDestroy(&img); - - return txt_out; -} - -Player -loadplayer(unsigned int line) -{ - FILE *fp; - Player player; - char buf[LINE_SIZE], *p = NULL, *end; - unsigned int i = 0; - - if (line <= 1) - die("nolan: Tried to load the description line as a player\n"); - if ((fp = fopen(STATS_FILE, "r")) == NULL) - die("nolan: Failed to open %s (read)\n", STATS_FILE); - - while (i++ < line && (p = fgets(buf, LINE_SIZE, fp)) != NULL); - fclose(fp); - if (p == NULL) - die("nolan: Line %d is not present in %s\n", line, STATS_FILE); - - player.name = malloc(32 + 1); - player.kingdom = malloc(32 + 1); - i = 0; - end = p; - - /* -1 because the last field in the file finish with a '\n' */ - while (i < LENGTH(fields) - 1 && *++end != '\0') { - if (*end != DELIM) - continue; - *end = '\0'; - if (i <= 1) /* name and kingdom */ - cpstr(((char **)&player)[i], p, 32 + 1); - else - ((long *)&player)[i] = atol(p); - p = end + 1; - i++; - } - if (i != LENGTH(fields) - 1) - die("nolan: Player on line %d is missing a field\n", line); - player.userid = strtoul(p, NULL, 10); - - return player; -} - -void -initplayers(void) -{ - FILE *fp; - char buf[LINE_SIZE]; - unsigned long i; - - if ((fp = fopen(STATS_FILE, "r")) == NULL) - die("nolan: Failed to open %s (read)\n", STATS_FILE); - - while (fgets(buf, LINE_SIZE, fp)) - nplayers++; - nplayers--; /* first line is not a player */ - - if (nplayers > MAX_PLAYERS) - die("nolan: There is too much players to load (max:%d)\n", - MAX_PLAYERS); - - for (i = 0; i < nplayers; i++) - players[i] = loadplayer(i + 2); -} - -void -updateplayers(Player *player) -{ - unsigned long i = 0, j; - - /* while (i < nplayers && strcmp(players[i].name, player->name) != 0) */ - while (i < nplayers && players[i].userid != player->userid) - i++; - - if (i == nplayers) { /* new player */ - if (nplayers > MAX_PLAYERS) - die("nolan: There is too much players (max:%d)\n", - MAX_PLAYERS); - players[nplayers] = loadplayer(nplayers + 2); - nplayers++; - } else { - if (player->name) - cpstr(players[i].name, player->name, 32 + 1); - if (player->kingdom) - cpstr(players[i].kingdom, player->kingdom, 32 + 1); - /* keep original userid */ - for (j = 2; j < LENGTH(fields) - 1; j++) - ((long *)&players[i])[j] = ((long *)player)[j]; - } -} - -long -playtimetolong(char *playtime, char str[]) -{ - char *p; - long days, hours; - - days = atol(playtime); - if ((p = strchr(playtime, str[0])) == 0) - return days; /* less than a day of playtime */ - while (*str && (*p++ == *str++)); - hours = atol(p); - - return days * 24 + hours; -} - -char * -playtimetostr(long playtime) -{ - long days, hours; - char *buf; - - days = playtime / 24; - hours = playtime % 24; - buf = malloc(32); - - switch (hours) { - case 0: - if (days <= 1) - snprintf(buf, 32, "%ld day", days); - else - snprintf(buf, 32, "%ld days", days); - break; - case 1: - if (days == 0) - snprintf(buf, 32, "%ld hour", hours); - else if (days == 1) - snprintf(buf, 32, "%ld day, %ld hour", days, hours); - else - snprintf(buf, 32, "%ld days, %ld hour", days, hours); - break; - default: - if (days == 0) - snprintf(buf, 32, "%ld hours", hours); - else if (days == 1) - snprintf(buf, 32, "%ld day, %ld hours", days, hours); - else - snprintf(buf, 32, "%ld days, %ld hours", days, hours); - break; - } - - return buf; -} - -/* trim everything that is not a number or a left parenthesis */ -void -trimall(char *str) -{ - const char *r = str; - char *w = str; - - do { - if ((*r >= 48 && *r <= 57) || *r == '(') - *w++ = *r; - } while (*r++); - - *w = '\0'; -} - -void -parseline(Player *player, char *line) -{ - char *str; - - if (strncmp(line, "KINGDOM", LEN("KINGDOM")) == 0) { - str = "KINGDOM "; - while (*str && (*line++ == *str++)); - player->kingdom = line; - return; - } - if (strncmp(line, "ROYAUME", LEN("ROYAUME")) == 0) { - str = "ROYAUME "; - while (*str && (*line++ == *str++)); - player->kingdom = line; - return; - } - - if (strncmp(line, "PLAYTIME", LEN("PLAYTIME")) == 0) { - str = "PLAYTIME "; - while (*str && (*line++ == *str++)); - player->playtime = playtimetolong(line, "days, "); - return; - } - if (strncmp(line, "TEMPS DE JEU", LEN("TEMPS DE JEU")) == 0) { - str = "TEMPS DE JEU "; - while (*str && (*line++ == *str++)); - player->playtime = playtimetolong(line, "jours, "); - return; - } - - if (strncmp(line, "ASCENSION LEVEL", LEN("ASCENSION LEVEL")) == 0 || - strncmp(line, "NIVEAU D'ELEVATION", LEN("NIVEAU D'ELEVATION")) == 0) { - trimall(line); - player->ascension = atol(line); - } else if (strncmp(line, "LEVEL", LEN("LEVEL")) == 0 || - strncmp(line, "NIVEAU", LEN("NIVEAU")) == 0) { - trimall(line); - player->level = atol(line); - } else if (strncmp(line, "GLOBAL RANK", LEN("GLOBAL RANK")) == 0 || - strncmp(line, "RANG GLOBAL", LEN("RANG GLOBAL")) == 0) { - trimall(line); - player->global = atol(line); - } else if (strncmp(line, "REGIONAL RANK", LEN("REGIONAL RANK")) == 0 || - strncmp(line, "RANG REGIONAL", LEN("RANG REGIONAL")) == 0) { - trimall(line); - player->regional = atol(line); - } else if (strncmp(line, "COMPETITIVE RANK", LEN("COMPETITIVE RANK")) == 0 || - strncmp(line, "RANG COMPETITIF", LEN("RANG COMPETITIF")) == 0) { - trimall(line); - player->competitive = atol(line); - } else if (strncmp(line, "MONSTERS SLAIN", LEN("MONSTERS SLAIN")) == 0 || - strncmp(line, "MONSTRES TUES", LEN("MONSTRES TUES")) == 0) { - trimall(line); - player->monsters = atol(line); - } else if (strncmp(line, "BOSSES SLAIN", LEN("BOSSES SLAIN")) == 0 || - strncmp(line, "BOSS TUES", LEN("BOSS TUES")) == 0) { - trimall(line); - player->bosses = atol(line); - } else if (strncmp(line, "PLAYERS DEFEATED", LEN("PLAYERS DEFEATED")) == 0 || - strncmp(line, "JOUEURS VAINCUS", LEN("JOUEURS VAINCUS")) == 0) { - trimall(line); - player->players = atol(line); - } else if (strncmp(line, "QUESTS COMPLETED", LEN("QUESTS COMPLETED")) == 0 || - strncmp(line, "QUETES TERMINEES", LEN("QUETES TERMINEES")) == 0) { - trimall(line); - player->quests = atol(line); - } else if (strncmp(line, "AREAS EXPLORED", LEN("AREAS EXPLORED")) == 0 || - strncmp(line, "TERRES EXPLOREES", LEN("TERRES EXPLOREES")) == 0) { - trimall(line); - player->explored = atol(line); - } else if (strncmp(line, "AREAS TAKEN", LEN("AREAS TAKEN")) == 0 || - strncmp(line, "TERRES PRISES", LEN("TERRES PRISES")) == 0) { - trimall(line); - player->taken = atol(line); - } else if (strncmp(line, "DUNGEONS CLEARED", LEN("DUNGEONS CLEARED")) == 0 || - strncmp(line, "DONJONS TERMINES", LEN("DONJONS TERMINES")) == 0) { - trimall(line); - player->dungeons = atol(line); - } else if (strncmp(line, "COLISEUM WINS", LEN("COLISEUM WINS")) == 0 || - strncmp(line, "VICTOIRES DANS LE COLISEE", LEN("VICTOIRES DANS LE COLISEE")) == 0) { - trimall(line); - player->coliseum = atol(line); - } else if (strncmp(line, "ITEMS UPGRADED", LEN("ITEMS UPGRADED")) == 0 || - strncmp(line, "OBJETS AMELIORES", LEN("OBJETS AMELIORES")) == 0) { - trimall(line); - player->items = atol(line); - } else if (strncmp(line, "FISH CAUGHT", LEN("FISH CAUGHT")) == 0 || - strncmp(line, "POISSONS ATTRAPES", LEN("POISSONS ATTRAPES")) == 0) { - trimall(line); - player->fish = atol(line); - } else if (strncmp(line, "DISTANCE TRAVELLED", LEN("DISTANCE TRAVELLED")) == 0 || - strncmp(line, "DISTANCE VOYAGEE", LEN("DISTANCE VOYAGEE")) == 0) { - trimall(line); - player->distance = atol(line); - } else if (strncmp(line, "REPUTATION", LEN("REPUTATION")) == 0) { - trimall(line); - player->reputation = atol(line); - } else if (strncmp(line, "ENDLESS RECORD", LEN("ENDLESS RECORD")) == 0 || - strncmp(line, "RECORD DU MODE SANS-FIN", LEN("RECORD DU MODE SANS-FIN")) == 0) { - trimall(line); - player->endless = atol(line); - } else if (strncmp(line, "ENTRIES COMPLETED", LEN("ENTRIES COMPLETED")) == 0 || - strncmp(line, "RECHERCHES TERMINEES", LEN("RECHERCHES TERMINEES")) == 0) { - trimall(line); - player->codex = atol(line); - } -} - -void -forline(Player *player, char *txt) -{ - char *line = txt, *endline; - - while (line) { - endline = strchr(line, '\n'); - if (endline) - *endline = '\0'; - parseline(player, line); - line = endline ? (endline + 1) : 0; - } -} - -void -createfile(void) -{ - FILE *fp; - unsigned long i; - long size = 0; - - fp = fopen(STATS_FILE, "r"); - - if (fp != NULL) { - fseek(fp, 0, SEEK_END); - size = ftell(fp); - } - - if (size == 0 && (fp = fopen(STATS_FILE, "w")) != NULL) { - for (i = 0; i < LENGTH(fields) - 1; i++) - fprintf(fp, "%s%c", fields[i], DELIM); - fprintf(fp, "%s\n", fields[LENGTH(fields) - 1]); - } - - fclose(fp); -} - -/* Save player to file and return player's index in file if it was found */ -int -savetofile(Player *player) -{ - FILE *w, *r; - char buf[LINE_SIZE], *p, *endname; - unsigned long iplayer = 0, cpt = 1, i; - - if ((r = fopen(STATS_FILE, "r")) == NULL) - die("nolan: Failed to open %s (read)\n", STATS_FILE); - if ((w = fopen("tmpfile", "w")) == NULL) - die("nolan: Failed to open %s (write)\n", STATS_FILE); - - while ((p = fgets(buf, LINE_SIZE, r)) != NULL) { - endname = strchr(p, DELIM); - if (endname) - *endname = 0; - if (strcmp(player->name, p) == 0) { - iplayer = cpt; - fprintf(w, "%s%c", player->name, DELIM); - fprintf(w, "%s%c", player->kingdom, DELIM); - for (i = 2; i < LENGTH(fields) - 1; i++) - fprintf(w, "%ld%c", ((long *)player)[i], DELIM); - fprintf(w, "%lu\n", player->userid); - } else { - if (endname) - *endname = DELIM; - fprintf(w, "%s", p); - } - cpt++; - } - if (!iplayer) { - fprintf(w, "%s%c", player->name, DELIM); - fprintf(w, "%s%c", player->kingdom, DELIM); - for (i = 2; i < LENGTH(fields) - 1; i++) - fprintf(w, "%ld%c", ((long *)player)[i], DELIM); - fprintf(w, "%lu\n", player->userid); - } - - fclose(r); - fclose(w); - remove(STATS_FILE); - rename("tmpfile", STATS_FILE); - - return iplayer; -} - -char * -updatemsg(Player *player, int iplayer) -{ - size_t sz = 1024; - char *buf = malloc(sz + 1), *p, *plto, *pltn, *pltd; - unsigned long i; - long old, new, diff; - - sz -= snprintf(buf, sz, "%s's profile has been updated.\n\n", - player->name); - p = strchr(buf, '\0'); - - if (strcmp(players[iplayer].kingdom, player->kingdom) != 0) { - sz -= snprintf(p, sz, "%s: %s -> %s\n", fields[1], - players[iplayer].kingdom, player->kingdom); - p = strchr(buf, '\0'); - } - - for (i = 2; i < LENGTH(fields) - 1; i++) { - if (sz <= 0) - die("nolan: truncation in updatemsg\n"); - old = ((long *)&players[iplayer])[i]; - new = ((long *)player)[i]; - diff = new - old; - if (diff == 0) - continue; - - if (i == 7) { /* playtime */ - plto = playtimetostr(old); - pltn = playtimetostr(new); - pltd = playtimetostr(diff); - sz -= snprintf(p, sz, "%s: %s -> %s (+ %s)\n", - fields[7], plto, pltn, pltd); - free(plto); - free(pltn); - free(pltd); - } else { - sz -= snprintf(p, sz, "%s: %'ld -> %'ld (%'+ld)\n", - fields[i], old, new, diff); - } - p = strchr(buf, '\0'); - } - - /* TODO */ - /* Last update was xxx ago */ - - return buf; -} - -char * -loadfilekd(char *kingdom, size_t *fszp) -{ - FILE *fp; - char *res, line[LINE_SIZE], *kd, *endkd; - size_t mfsz = LINE_SIZE + nplayers * LINE_SIZE; - - if ((fp = fopen(STATS_FILE, "r")) == NULL) - die("nolan: Failed to open %s (read)\n", STATS_FILE); - - res = malloc(mfsz); - fgets(line, LINE_SIZE, fp); /* fields name */ - cpstr(res, line, mfsz); - while (fgets(line, LINE_SIZE, fp) != NULL) { - kd = strchr(line, DELIM) + 1; - endkd = nstrchr(line, DELIM, 2); - if (endkd) - *endkd = '\0'; - if (strcmp(kd, kingdom) == 0) { - *endkd = DELIM; - catstr(res, line, mfsz); - } - } - - fclose(fp); - *fszp = strlen(res); - return res; -} - -int -compare(const void *e1, const void *e2) -{ - const long l1 = ((long *)(Player *)e1)[category]; - const long l2 = ((long *)(Player *)e2)[category]; - - /* ranks */ - if (category == 4 || category == 6) { - if (l1 == 0) - return 1; - if (l2 == 0) - return -1; - return l1 - l2; - } - - return l2 - l1; -} - -char * -playerinlb(int i) -{ - size_t siz = 64, ssiz = 16; - char *buf = malloc(siz), *plt, stat[ssiz]; - - snprintf(buf, siz, "%d. %s: ", i + 1, players[i].name); - if (category == 7) { /* playtime */ - plt = playtimetostr(((long *)&players[i])[category]); - catstr(buf, plt, siz); - free(plt); - } else { - snprintf(stat, ssiz, "%'ld", ((long *)&players[i])[category]); - catstr(buf, stat, siz); - if (i == 18) /* distance */ - catstr(buf, "m", siz); - } - catstr(buf, "\n", siz); - - return buf; -} - -char * -leaderboard(u64snowflake userid) -{ - int in_lb = 0; - unsigned long i, lb_max; - size_t siz; - char *buf, *player; - - qsort(players, nplayers, sizeof(players[0]), compare); - lb_max = MIN(nplayers, LB_MAX); - siz = (lb_max + 2) * 64; - buf = malloc(siz); - - cpstr(buf, fields[category], siz); - catstr(buf, ":\n", siz); - for (i = 0; i < lb_max ; i++) { - if (userid == players[i].userid) - in_lb = 1; - player = playerinlb(i); - catstr(buf, player, siz); - free(player); - } - - if (!in_lb) { - catstr(buf, "...\n", siz); - i = lb_max; - while (i < nplayers && players[i].userid != userid) - i++; - player = playerinlb(i); - catstr(buf, player, siz); - free(player); - } - - return buf; -} - -char * -invalidlb(void) -{ - unsigned long i; - size_t siz = 512; - char *buf = malloc(siz); - - cpstr(buf, "NO WRONG, this is not a valid category.\n", siz); - catstr(buf, "Valid categories are:\n", siz); - for (i = 2; i < LENGTH(fields) - 1; i++) { - if (i == 5) /* regional rank */ - continue; - catstr(buf, fields[i], siz); - catstr(buf, "\n", siz); - } - - return buf; -} - -u64snowflake -useridtoul(char *id) -{ - char *start = id, *end = strchr(id, '\0') - 1; - - if (strncmp(start, "<@", 2) == 0 && strncmp(end, ">", 1) == 0) - return strtoul(start + 2, NULL, 10); - return 0; -} - -void -on_ready(struct discord *client, const struct discord_ready *event) -{ - struct discord_activity activities[] = { - { - .name = "?help", - .type = DISCORD_ACTIVITY_LISTENING, - }, - }; - - struct discord_presence_update status = { - .activities = - &(struct discord_activities) - { - .size = LENGTH(activities), - .array = activities - }, - .status = "online", - .afk = false, - .since = discord_timestamp(client), - }; - - discord_update_presence(client, &status); -} - -void -stats(struct discord *client, const struct discord_message *event) -{ - int i, iplayer; - char *txt, *fname = malloc(64); - Player player; - - snprintf(fname, 64, "./images/%s.jpg", event->author->username); - dlimg(event->attachments->array->url, fname); - txt = ocr(fname); - free(fname); - - if (txt == NULL) { - struct discord_create_message msg = { - .content = "Error: Failed to read image" - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); - return; - } - - memset(&player, 0, sizeof(player)); - player.name = event->author->username; - player.userid = event->author->id; - forline(&player, txt); - free(txt); - - if (player.kingdom == NULL) - player.kingdom = "(null)"; - - if (kingdom_verification) { - i = LENGTH(kingdoms); - - while (i > 0 && strcmp(player.kingdom, kingdoms[i++]) != 0); - - if (i == 0) { - struct discord_create_message msg = { - .content = "Sorry you're not part of the kingdom :/" - }; - discord_create_message(client, event->channel_id, &msg, NULL); - return; - } - } - - if ((iplayer = savetofile(&player))) { - txt = updatemsg(&player, iplayer - 2); - } else { - txt = malloc(128); - snprintf(txt, 128, "**%s** has been registrated in the database.", - player.name); - } - struct discord_create_message msg = { - .content = txt - }; - discord_create_message(client, event->channel_id, &msg, NULL); - updateplayers(&player); - free(txt); -} - -void -raids(struct discord *client, const struct discord_message *event) -{ - char *txt, *line, *endline; - - dlimg(event->attachments->array->url, "./images/raids.jpg"); - txt = ocr("./images/raids.jpg"); - - /* TODO */ - line = txt; - while (line) { - endline = strchr(line, '\n'); - if (endline) - *endline = '\0'; - if (strncmp(line, "+ Raid options", 14) == 0) { - line = endline + 1; - break; - } - line = endline ? (endline + 1) : 0; - } - - struct discord_create_message msg = { - .content = line - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); -} - - -void -on_message(struct discord *client, const struct discord_message *event) -{ - int i; - - if (event->author->bot) - return; - if (event->attachments->size == 0) - return; - if (strchr(event->attachments->array->filename, '.') == NULL) - return; - if (strncmp(event->attachments->array->content_type, "image/jpeg", - sizeof("image/jpeg") - 1) != 0) - return; - -#ifdef DEVEL - if (event->channel_id == DEVEL) - stats(client, event); - return; -#endif - - for (i = 0; i < (int)LENGTH(stats_ids); i++) { - if (event->channel_id == stats_ids[i]) { - stats(client, event); - break; - } - } - - if (event->channel_id == RAIDS_ID) - raids(client, event); -} - -void -on_source(struct discord *client, const struct discord_message *event) -{ - size_t fsize = 0; - char *fbuf = NULL; - - if (event->author->bot) - return; - -#ifdef DEVEL - if (event->channel_id != DEVEL) - return; -#endif - - if (strlen(event->content) == 0) - fbuf = cog_load_whole_file(STATS_FILE, &fsize); - else - fbuf = loadfilekd(event->content, &fsize); - - struct discord_attachment attachment = { - .filename = STATS_FILE, - .content = fbuf, - .size = fsize - }; - struct discord_attachments attachments = { - .size = 1, - .array = &attachment - }; - struct discord_create_message msg = { - .attachments = &attachments - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(fbuf); -} - - -void -on_lb(struct discord *client, const struct discord_message *event) -{ - unsigned long i = 2; /* ignore name and kingdom */ - char *txt; - - if (event->author->bot) - return; - -#ifdef DEVEL - if (event->channel_id != DEVEL) - return; -#endif - - if (strlen(event->content) == 0) { - txt = invalidlb(); - struct discord_create_message msg = { - .content = txt - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); - return; - } - - while (i < LENGTH(fields) - 1 && strcasecmp(fields[i], event->content) != 0) - i++; - - if (i == LENGTH(fields) - 1 || i == 5) { - txt = invalidlb(); - struct discord_create_message msg = { - .content = txt - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); - return; - } - - category = i; - txt = leaderboard(event->author->id); - struct discord_create_message msg = { - .content = txt - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); -} - -void -on_info(struct discord *client, const struct discord_message *event) -{ - unsigned long i = 0, j; - size_t sz = 512; - char buf[sz], *p; - u64snowflake userid; - - if (event->author->bot) - return; - -#ifdef DEVEL - if (event->channel_id != DEVEL) - return; -#endif - - if (strlen(event->content) == 0) - userid = event->author->id; - else - userid = useridtoul(event->content); - - if (userid > 0) { - while (i < nplayers && players[i].userid != userid) - i++; - } else { - while (i < nplayers && strcmp(players[i].name, event->content) != 0) - i++; - } - - if (i == nplayers) { - struct discord_create_message msg = { - .content = "This player does not exist in the database." - "\nTo check a player's info type " - "?info @username or ?info username.\n" - "To check your info just type ?info." - }; - discord_create_message(client, event->channel_id, &msg, NULL); - return; - } - - if (use_embed) { - struct discord_embed embed = { - .color = 0x3498DB, - .timestamp = discord_timestamp(client), - .title = players[i].name, - }; - discord_embed_set_footer(&embed, "Nolan", ICON_URL, NULL); - discord_embed_add_field( - &embed, (char *)fields[1], players[i].kingdom, true); - for (j = 2; j < LENGTH(fields) - 1; j++) { - if (j == 7) { /* playtime */ - p = playtimetostr(((long *)&players[i])[j]); - discord_embed_add_field( - &embed, (char *)fields[j], p, true); - free(p); - } else { - sprintf(buf, "%'ld", ((long *)&players[i])[j]); - if (j == 18) /* distance */ - strcat(buf, "m"); - discord_embed_add_field( - &embed, (char *)fields[j], buf, true); - } - } - struct discord_create_message msg = { - .embeds = &(struct discord_embeds) - { - .size = 1, - .array = &embed, - } - }; - discord_create_message(client, event->channel_id, &msg, NULL); - discord_embed_cleanup(&embed); - } else { - buf[0] = '\0'; - for (j = 0; j < LENGTH(fields) - 1; j++) { - catstr(buf, fields[j], sz); - catstr(buf, ": ", sz); - if (j <= 1) { /* name and kingdom */ - catstr(buf, ((char **)&players[i])[j], sz); - } else if (j == 7) { /* playtime */ - p = playtimetostr(((long *)&players[i])[j]); - catstr(buf, p, sz); - free(p); - } else { - p = strchr(buf, '\0'); - snprintf(p, sz, "%'ld", ((long *)&players[i])[j]); - if (j == 18) /* distance */ - catstr(buf, "m", sz); - } - catstr(buf, "\n", sz); - } - struct discord_create_message msg = { - .content = buf - }; - discord_create_message(client, event->channel_id, &msg, NULL); - } -} - -void -on_help(struct discord * client, const struct discord_message * event) -{ - char *txt, *p; - unsigned long i, len = LENGTH(stats_ids); - size_t sz = 512; - -#ifdef DEVEL - if (event->channel_id != DEVEL) - return; -#endif - - txt = malloc(sz); - cpstr(txt, "Post a screenshot of your stats to ", sz); - for (i = 0; i < len; i++) { - p = strchr(txt, '\0');; - snprintf(p, sz, "<#%lu> ", stats_ids[i]); - if (i < len - 1) - catstr(txt, "or ", sz); - } - catstr(txt, "to enter the database.\n", sz); - /* TODO: add PREFIX */ - catstr(txt, "Commands:\n", sz); - catstr(txt, "\t?info *[[@]user]*\n", sz); - catstr(txt, "\t?leaderboard or ?lb *category*\n", sz); - /* catstr(txt, "\t?correct [category] [corrected value]\n", sz); */ - catstr(txt, "\t?source or ?src [kingdom]\n", sz); - catstr(txt, "\t?help\n\n", sz); - catstr(txt, "[...] means optional.\n", sz); - catstr(txt, "Try them or ask Ratakor to know what they do.", sz); - - struct discord_create_message msg = { - .content = txt - }; - discord_create_message(client, event->channel_id, &msg, NULL); - free(txt); -} - -int -main(int argc, char *argv[]) -{ - char *src[] = { "src", "source" }; - char *lb[] = { "lb", "leaderboard" }; - struct discord *client; - -#ifndef DEVEL - if (getuid() != 0) - die("Please run %s as root\n", argv[0]); -#endif - - setlocale(LC_NUMERIC, ""); - ccord_global_init(); - client = discord_init(TOKEN); - - discord_add_intents(client, DISCORD_GATEWAY_MESSAGE_CONTENT); - discord_set_prefix(client, PREFIX); - /* create_slash_commands(client); */ - discord_set_on_ready(client, on_ready); - /* TODO: create and interaction for each command */ - /* discord_set_on_interaction_create(client, &on_interaction_create); */ - discord_set_on_message_create(client, on_message); - discord_set_on_commands(client, lb, LENGTH(lb), on_lb); - discord_set_on_command(client, "info", on_info); - discord_set_on_commands(client, src, LENGTH(src), on_source); - discord_set_on_command(client, "help", on_help); - - if (!file_exists(SAVE_FOLDER)) { - if (mkdir(SAVE_FOLDER, 0755) == -1) - die("nolan: Failed to create %s\n", SAVE_FOLDER); - if (mkdir(IMAGE_FOLDER, 0755) == -1) - die("nolan: Failed to create %s\n", IMAGE_FOLDER); - } - createfile(); - initplayers(); - - discord_run(client); - - discord_cleanup(client); - ccord_global_cleanup(); - - return EXIT_SUCCESS; -} diff --git a/src/nolan.h b/src/nolan.h new file mode 100644 index 0000000..a793378 --- /dev/null +++ b/src/nolan.h @@ -0,0 +1,83 @@ +#include <concord/discord.h> + +#include "../config.h" +#include "util.h" + +#define MAX_PLAYERS LENGTH(kingdoms) * 50 +#define LINE_SIZE 300 + 1 +#define SAVE_FOLDER "/var/lib/nolan/" +#define IMAGE_FOLDER SAVE_FOLDER "images/" +#define STATS_FILE SAVE_FOLDER FILENAME + +/* ALL FIELDS MUST HAVE THE SAME SIZE */ +typedef struct { + char *name; + char *kingdom; + long level; + long ascension; + long global; + long regional; + long competitive; + long playtime; + long monsters; + long bosses; + long players; + long quests; + long explored; + long taken; + long dungeons; + long coliseum; + long items; + long fish; + long distance; + long reputation; + long endless; + long codex; + u64snowflake userid; +} Player; + +extern Player players[MAX_PLAYERS]; +extern size_t nplayers; +extern const char *fields[23]; + +/* init.c */ +void create_folders(void); +void create_stats_file(void); +void init_players(void); +void create_slash_commands(struct discord *client); +void on_interaction(struct discord *client, const struct discord_interaction *event); +void on_ready(struct discord *client, const struct discord_ready *event); +void on_message(struct discord *client, const struct discord_message *event); + +/* ocr.c */ +void curl(char *url, char *fname); +char *ocr(char *fname); + +/* stats.c */ +char *playtime_to_str(long playtime); +Player create_player(unsigned int line); +void stats(struct discord *client, const struct discord_message *event); + +/* raids.c */ +void raids(struct discord *client, const struct discord_message *event); + +/* cmd_help.c */ +void create_slash_help(struct discord *client); +void help(char *buf, size_t siz); +void on_help(struct discord *client, const struct discord_message *event); + +/* cmd_info.c */ +void create_slash_info(struct discord *client); +void info_from_uid(char *buf, size_t siz, u64snowflake userid); +void info_from_txt(char *buf, size_t siz, char *txt); +void on_info(struct discord *client, const struct discord_message *event); + +/* cmd_leaderboard.c */ +void create_slash_leaderboard(struct discord *client); +void leaderboard(char *buf, size_t siz, char *txt, u64snowflake userid); +void on_leaderboard(struct discord *client, const struct discord_message *event); + +/* cmd_source.c */ +void create_slash_source(struct discord *client); +char *sort_source(char *kingdom, size_t *fszp); +void on_source(struct discord *client, const struct discord_message *event); diff --git a/src/ocr.c b/src/ocr.c new file mode 100644 index 0000000..4dfcfc4 --- /dev/null +++ b/src/ocr.c @@ -0,0 +1,68 @@ +#include <string.h> +#include <leptonica/allheaders.h> +#include <tesseract/capi.h> +#include <curl/curl.h> +#include "nolan.h" + +static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream); + +static size_t +write_data(void *ptr, size_t size, size_t nmemb, void *stream) +{ + size_t written = fwrite(ptr, size, nmemb, (FILE *)stream); + return written; +} + +void +curl(char *url, char *fname) +{ + CURL *handle; + FILE *fp; + + curl_global_init(CURL_GLOBAL_ALL); + + handle = curl_easy_init(); + + curl_easy_setopt(handle, CURLOPT_URL, url); + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_data); + + if ((fp = fopen(fname, "wb")) == NULL) + die("nolan: Failed to open %s\n", fname); + + curl_easy_setopt(handle, CURLOPT_WRITEDATA, fp); + curl_easy_perform(handle); + + fclose(fp); + curl_easy_cleanup(handle); + curl_global_cleanup(); +} + +char * +ocr(char *fname) +{ + TessBaseAPI *handle; + PIX *img; + char *txt_ocr, *txt_out; + + if ((img = pixRead(fname)) == NULL) + die("nolan: Error reading image\n"); + + handle = TessBaseAPICreate(); + if (TessBaseAPIInit3(handle, NULL, "eng") != 0) + die("nolan: Error initialising tesseract\n"); + + TessBaseAPISetImage2(handle, img); + if (TessBaseAPIRecognize(handle, NULL) != 0) + die("nolan: Error in tesseract recognition\n"); + + txt_ocr = TessBaseAPIGetUTF8Text(handle); + txt_out = strdup(txt_ocr); + + TessDeleteText(txt_ocr); + TessBaseAPIEnd(handle); + TessBaseAPIDelete(handle); + pixDestroy(&img); + + return txt_out; +} + diff --git a/src/raids.c b/src/raids.c new file mode 100644 index 0000000..95b6960 --- /dev/null +++ b/src/raids.c @@ -0,0 +1,31 @@ +#include <stdlib.h> +#include <string.h> +#include "nolan.h" + +void +raids(struct discord *client, const struct discord_message *event) +{ + char *txt, *line, *endline; + + curl(event->attachments->array->url, "./images/raids.jpg"); + txt = ocr("./images/raids.jpg"); + + /* TODO */ + line = txt; + while (line) { + endline = strchr(line, '\n'); + if (endline) + *endline = '\0'; + if (strncmp(line, "+ Raid options", 14) == 0) { + line = endline + 1; + break; + } + line = endline ? (endline + 1) : 0; + } + + struct discord_create_message msg = { + .content = line + }; + discord_create_message(client, event->channel_id, &msg, NULL); + free(txt); +} diff --git a/src/stats.c b/src/stats.c new file mode 100644 index 0000000..284a72a --- /dev/null +++ b/src/stats.c @@ -0,0 +1,429 @@ +#include <stdlib.h> +#include <string.h> +#include "nolan.h" + +#define LEN(X) (sizeof X - 1) + +static void update_players(Player *player); +static long playtime_to_long(char *playtime, char *str); +static void trim_all(char *str); +static void parse_line(Player *player, char *line); +static void for_line(Player *player, char *src); +static int save_player_to_file(Player *player); +static char *update_msg(Player *player, int iplayer); + +Player +create_player(unsigned int line) +{ + FILE *fp; + Player player; + char buf[LINE_SIZE], *p = NULL, *end; + unsigned int i = 0; + + if (line <= 1) + die("nolan: Tried to load the description line as a player\n"); + if ((fp = fopen(STATS_FILE, "r")) == NULL) + die("nolan: Failed to open %s (read)\n", STATS_FILE); + + while (i++ < line && (p = fgets(buf, LINE_SIZE, fp)) != NULL); + fclose(fp); + if (p == NULL) + die("nolan: Line %d is not present in %s\n", line, STATS_FILE); + + player.name = malloc(DISCORD_MAX_USERNAME_LEN); + player.kingdom = malloc(32 + 1); + i = 0; + end = p; + + /* -1 because the last field in the file finish with a '\n' */ + while (i < LENGTH(fields) - 1 && *++end != '\0') { + if (*end != DELIM) + continue; + *end = '\0'; + if (i <= 1) /* name and kingdom */ + cpstr(((char **)&player)[i], p, 32 + 1); + else + ((long *)&player)[i] = atol(p); + p = end + 1; + i++; + } + if (i != LENGTH(fields) - 1) + die("nolan: Player on line %d is missing a field\n", line); + player.userid = strtoul(p, NULL, 10); + + return player; +} + +static void +update_players(Player *player) +{ + unsigned long i = 0, j; + + /* while (i < nplayers && strcmp(players[i].name, player->name) != 0) */ + while (i < nplayers && players[i].userid != player->userid) + i++; + + if (i == nplayers) { /* new player */ + if (nplayers > MAX_PLAYERS) + die("nolan: There is too much players (max:%d)\n", + MAX_PLAYERS); + players[nplayers] = create_player(nplayers + 2); + nplayers++; + } else { + if (player->name) { + cpstr(players[i].name, player->name, + DISCORD_MAX_USERNAME_LEN); + } + if (player->kingdom) + cpstr(players[i].kingdom, player->kingdom, 32 + 1); + /* keep original userid */ + for (j = 2; j < LENGTH(fields) - 1; j++) + ((long *)&players[i])[j] = ((long *)player)[j]; + } +} + +static long +playtime_to_long(char *playtime, char str[]) +{ + char *p; + long days, hours; + + days = atol(playtime); + if ((p = strchr(playtime, str[0])) == 0) + return days; /* less than a day of playtime */ + while (*str && (*p++ == *str++)); + hours = atol(p); + + return days * 24 + hours; +} + +char * +playtime_to_str(long playtime) +{ + long days = playtime / 24; + long hours = playtime % 24; + size_t siz = 32; + char *buf = malloc(siz); + + switch (hours) { + case 0: + if (days <= 1) + snprintf(buf, siz, "%ld day", days); + else + snprintf(buf, siz, "%ld days", days); + break; + case 1: + if (days == 0) + snprintf(buf, siz, "%ld hour", hours); + else if (days == 1) + snprintf(buf, siz, "%ld day, %ld hour", days, hours); + else + snprintf(buf, siz, "%ld days, %ld hour", days, hours); + break; + default: + if (days == 0) + snprintf(buf, siz, "%ld hours", hours); + else if (days == 1) + snprintf(buf, siz, "%ld day, %ld hours", days, hours); + else + snprintf(buf, siz, "%ld days, %ld hours", days, hours); + break; + } + + return buf; +} + +/* trim everything that is not a number or a left parenthesis */ +static void +trim_all(char *str) +{ + const char *r = str; + char *w = str; + + do { + if ((*r >= 48 && *r <= 57) || *r == '(') + *w++ = *r; + } while (*r++); + + *w = '\0'; +} + +static void +parse_line(Player *player, char *line) +{ + char *str; + + if (strncmp(line, "KINGDOM", LEN("KINGDOM")) == 0) { + str = "KINGDOM "; + while (*str && (*line++ == *str++)); + player->kingdom = line; + return; + } + if (strncmp(line, "ROYAUME", LEN("ROYAUME")) == 0) { + str = "ROYAUME "; + while (*str && (*line++ == *str++)); + player->kingdom = line; + return; + } + + if (strncmp(line, "PLAYTIME", LEN("PLAYTIME")) == 0) { + str = "PLAYTIME "; + while (*str && (*line++ == *str++)); + player->playtime = playtime_to_long(line, "days, "); + return; + } + if (strncmp(line, "TEMPS DE JEU", LEN("TEMPS DE JEU")) == 0) { + str = "TEMPS DE JEU "; + while (*str && (*line++ == *str++)); + player->playtime = playtime_to_long(line, "jours, "); + return; + } + + if (strncmp(line, "ASCENSION LEVEL", LEN("ASCENSION LEVEL")) == 0 || + strncmp(line, "NIVEAU D'ELEVATION", LEN("NIVEAU D'ELEVATION")) == 0) { + trim_all(line); + player->ascension = atol(line); + } else if (strncmp(line, "LEVEL", LEN("LEVEL")) == 0 || + strncmp(line, "NIVEAU", LEN("NIVEAU")) == 0) { + trim_all(line); + player->level = atol(line); + } else if (strncmp(line, "GLOBAL RANK", LEN("GLOBAL RANK")) == 0 || + strncmp(line, "RANG GLOBAL", LEN("RANG GLOBAL")) == 0) { + trim_all(line); + player->global = atol(line); + } else if (strncmp(line, "REGIONAL RANK", LEN("REGIONAL RANK")) == 0 || + strncmp(line, "RANG REGIONAL", LEN("RANG REGIONAL")) == 0) { + trim_all(line); + player->regional = atol(line); + } else if (strncmp(line, "COMPETITIVE RANK", LEN("COMPETITIVE RANK")) == 0 || + strncmp(line, "RANG COMPETITIF", LEN("RANG COMPETITIF")) == 0) { + trim_all(line); + player->competitive = atol(line); + } else if (strncmp(line, "MONSTERS SLAIN", LEN("MONSTERS SLAIN")) == 0 || + strncmp(line, "MONSTRES TUES", LEN("MONSTRES TUES")) == 0) { + trim_all(line); + player->monsters = atol(line); + } else if (strncmp(line, "BOSSES SLAIN", LEN("BOSSES SLAIN")) == 0 || + strncmp(line, "BOSS TUES", LEN("BOSS TUES")) == 0) { + trim_all(line); + player->bosses = atol(line); + } else if (strncmp(line, "PLAYERS DEFEATED", LEN("PLAYERS DEFEATED")) == 0 || + strncmp(line, "JOUEURS VAINCUS", LEN("JOUEURS VAINCUS")) == 0) { + trim_all(line); + player->players = atol(line); + } else if (strncmp(line, "QUESTS COMPLETED", LEN("QUESTS COMPLETED")) == 0 || + strncmp(line, "QUETES TERMINEES", LEN("QUETES TERMINEES")) == 0) { + trim_all(line); + player->quests = atol(line); + } else if (strncmp(line, "AREAS EXPLORED", LEN("AREAS EXPLORED")) == 0 || + strncmp(line, "TERRES EXPLOREES", LEN("TERRES EXPLOREES")) == 0) { + trim_all(line); + player->explored = atol(line); + } else if (strncmp(line, "AREAS TAKEN", LEN("AREAS TAKEN")) == 0 || + strncmp(line, "TERRES PRISES", LEN("TERRES PRISES")) == 0) { + trim_all(line); + player->taken = atol(line); + } else if (strncmp(line, "DUNGEONS CLEARED", LEN("DUNGEONS CLEARED")) == 0 || + strncmp(line, "DONJONS TERMINES", LEN("DONJONS TERMINES")) == 0) { + trim_all(line); + player->dungeons = atol(line); + } else if (strncmp(line, "COLISEUM WINS", LEN("COLISEUM WINS")) == 0 || + strncmp(line, "VICTOIRES DANS LE COLISEE", LEN("VICTOIRES DANS LE COLISEE")) == 0) { + trim_all(line); + player->coliseum = atol(line); + } else if (strncmp(line, "ITEMS UPGRADED", LEN("ITEMS UPGRADED")) == 0 || + strncmp(line, "OBJETS AMELIORES", LEN("OBJETS AMELIORES")) == 0) { + trim_all(line); + player->items = atol(line); + } else if (strncmp(line, "FISH CAUGHT", LEN("FISH CAUGHT")) == 0 || + strncmp(line, "POISSONS ATTRAPES", LEN("POISSONS ATTRAPES")) == 0) { + trim_all(line); + player->fish = atol(line); + } else if (strncmp(line, "DISTANCE TRAVELLED", LEN("DISTANCE TRAVELLED")) == 0 || + strncmp(line, "DISTANCE VOYAGEE", LEN("DISTANCE VOYAGEE")) == 0) { + trim_all(line); + player->distance = atol(line); + } else if (strncmp(line, "REPUTATION", LEN("REPUTATION")) == 0) { + trim_all(line); + player->reputation = atol(line); + } else if (strncmp(line, "ENDLESS RECORD", LEN("ENDLESS RECORD")) == 0 || + strncmp(line, "RECORD DU MODE SANS-FIN", LEN("RECORD DU MODE SANS-FIN")) == 0) { + trim_all(line); + player->endless = atol(line); + } else if (strncmp(line, "ENTRIES COMPLETED", LEN("ENTRIES COMPLETED")) == 0 || + strncmp(line, "RECHERCHES TERMINEES", LEN("RECHERCHES TERMINEES")) == 0) { + trim_all(line); + player->codex = atol(line); + } +} + +static void +for_line(Player *player, char *txt) +{ + char *line = txt, *endline; + + while (line) { + endline = strchr(line, '\n'); + if (endline) + *endline = '\0'; + parse_line(player, line); + line = endline ? (endline + 1) : 0; + } +} + +/* Save player to file and return player's index in file if it was found */ +static int +save_player_to_file(Player *player) +{ + FILE *w, *r; + char buf[LINE_SIZE], *p, *endname; + unsigned long iplayer = 0, cpt = 1, i; + + if ((r = fopen(STATS_FILE, "r")) == NULL) + die("nolan: Failed to open %s (read)\n", STATS_FILE); + if ((w = fopen("tmpfile", "w")) == NULL) + die("nolan: Failed to open %s (write)\n", STATS_FILE); + + while ((p = fgets(buf, LINE_SIZE, r)) != NULL) { + endname = strchr(p, DELIM); + if (endname) + *endname = 0; + if (strcmp(player->name, p) == 0) { + iplayer = cpt; + fprintf(w, "%s%c", player->name, DELIM); + fprintf(w, "%s%c", player->kingdom, DELIM); + for (i = 2; i < LENGTH(fields) - 1; i++) + fprintf(w, "%ld%c", ((long *)player)[i], DELIM); + fprintf(w, "%lu\n", player->userid); + } else { + if (endname) + *endname = DELIM; + fprintf(w, "%s", p); + } + cpt++; + } + if (!iplayer) { + fprintf(w, "%s%c", player->name, DELIM); + fprintf(w, "%s%c", player->kingdom, DELIM); + for (i = 2; i < LENGTH(fields) - 1; i++) + fprintf(w, "%ld%c", ((long *)player)[i], DELIM); + fprintf(w, "%lu\n", player->userid); + } + + fclose(r); + fclose(w); + remove(STATS_FILE); + rename("tmpfile", STATS_FILE); + + return iplayer; +} + +char * +update_msg(Player *player, int iplayer) +{ + size_t sz = 1024; + char *buf = malloc(sz + 1), *p, *plto, *pltn, *pltd; + unsigned long i; + long old, new, diff; + + sz -= snprintf(buf, sz, "%s's profile has been updated.\n\n", + player->name); + p = strchr(buf, '\0'); + + if (strcmp(players[iplayer].kingdom, player->kingdom) != 0) { + sz -= snprintf(p, sz, "%s: %s -> %s\n", fields[1], + players[iplayer].kingdom, player->kingdom); + p = strchr(buf, '\0'); + } + + for (i = 2; i < LENGTH(fields) - 1; i++) { + if (sz <= 0) + die("nolan: truncation in updatemsg\n"); + old = ((long *)&players[iplayer])[i]; + new = ((long *)player)[i]; + diff = new - old; + if (diff == 0) + continue; + + if (i == 7) { /* playtime */ + plto = playtime_to_str(old); + pltn = playtime_to_str(new); + pltd = playtime_to_str(diff); + sz -= snprintf(p, sz, "%s: %s -> %s (+ %s)\n", + fields[7], plto, pltn, pltd); + free(plto); + free(pltn); + free(pltd); + } else { + sz -= snprintf(p, sz, "%s: %'ld -> %'ld (%'+ld)\n", + fields[i], old, new, diff); + } + p = strchr(buf, '\0'); + } + + /* + * TODO + * Last update was xxx ago + */ + + return buf; +} + +void +stats(struct discord *client, const struct discord_message *event) +{ + int i, iplayer; + char *txt, *fname = malloc(64); + Player player; + + snprintf(fname, 64, "./images/%s.jpg", event->author->username); + curl(event->attachments->array->url, fname); + txt = ocr(fname); + free(fname); + + if (txt == NULL) { + struct discord_create_message msg = { + .content = "Error: Failed to read image" + }; + discord_create_message(client, event->channel_id, &msg, NULL); + free(txt); + return; + } + + memset(&player, 0, sizeof(player)); + player.name = event->author->username; + player.userid = event->author->id; + for_line(&player, txt); + free(txt); + + if (player.kingdom == NULL) + player.kingdom = "(null)"; + + if (kingdom_verification) { + i = LENGTH(kingdoms); + + while (i > 0 && strcmp(player.kingdom, kingdoms[i++]) != 0); + + if (i == 0) { + struct discord_create_message msg = { + .content = "Sorry you're not part of the kingdom :/" + }; + discord_create_message(client, event->channel_id, &msg, NULL); + return; + } + } + + if ((iplayer = save_player_to_file(&player))) { + txt = update_msg(&player, iplayer - 2); + } else { + txt = malloc(128); + snprintf(txt, 128, "**%s** has been registrated in the database.", + player.name); + } + struct discord_create_message msg = { + .content = txt + }; + discord_create_message(client, event->channel_id, &msg, NULL); + update_players(&player); + free(txt); +} @@ -2,7 +2,6 @@ #include <stdlib.h> #include <stdarg.h> #include <string.h> -#include <sys/stat.h> #include "util.h" @@ -74,10 +73,3 @@ catstr(char *dst, const char *src, size_t siz) die("catstr: string truncation happened during concatenation\n"); return rsiz; } - -int -file_exists (char *filename) -{ - struct stat buf; - return (stat(filename, &buf) == 0); -} @@ -8,4 +8,3 @@ size_t strlcpy(char *dst, const char *src, size_t siz); size_t strlcat(char *dst, const char *src, size_t siz); size_t cpstr(char *dst, const char *src, size_t siz); size_t catstr(char *dst, const char *src, size_t siz); -int file_exists(char *filename); |