aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRatakor <ratakor@disroot.org>2023-05-28 09:19:31 +0200
committerRatakor <ratakor@disroot.org>2023-05-28 09:19:31 +0200
commitafd1bd5a1c6acefeabc9b0179c6b1bd31f33d2b0 (patch)
treee13f0b7424e46337c430a984d039beaeba5d1fb5
parent69e1dbc29758d8ab3bda6c9e2f40ddd962f20515 (diff)
! Move everything to separate files + add / cmdsv0.0.5
-rw-r--r--README.md9
-rw-r--r--config.def.h10
-rw-r--r--src/cmd_help.c54
-rw-r--r--src/cmd_info.c189
-rw-r--r--src/cmd_leaderboard.c243
-rw-r--r--src/cmd_source.c89
-rw-r--r--src/init.c196
-rw-r--r--src/main.c69
-rw-r--r--src/nolan.c1108
-rw-r--r--src/nolan.h83
-rw-r--r--src/ocr.c68
-rw-r--r--src/raids.c31
-rw-r--r--src/stats.c429
-rw-r--r--src/util.c8
-rw-r--r--src/util.h1
15 files changed, 1461 insertions, 1126 deletions
diff --git a/README.md b/README.md
index 47be9d7..1500875 100644
--- a/README.md
+++ b/README.md
@@ -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,
+ &params, 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);
+}
diff --git a/src/util.c b/src/util.c
index 601f36c..d034463 100644
--- a/src/util.c
+++ b/src/util.c
@@ -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);
-}
diff --git a/src/util.h b/src/util.h
index 21f764d..a3b9373 100644
--- a/src/util.h
+++ b/src/util.h
@@ -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);