From 713a266f1597cc106d2cde94600329081c87c382 Mon Sep 17 00:00:00 2001 From: Will Brown Date: Wed, 20 Nov 2024 20:15:45 -0500 Subject: [PATCH] Add basic functionality (barely) --- Makefile | 11 +++ README.md | 15 ++++ config.json | 24 ++++++ main.c | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 294 insertions(+) create mode 100644 Makefile create mode 100644 config.json create mode 100644 main.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f16dd14 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +CFLAGS = -Wall -Wextra -O2 +LDFLAGS = -lpthread -lcurl -lmrss + +SRC = $(wildcard *.c) +OBJ = $(SRC:.c=.o) + +zblock: $(OBJ) /usr/local/lib/libdiscord.a + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + +.PHONY: clean +clean: rm -f $(OBJ) linuxbot diff --git a/README.md b/README.md index 5daa331..1446a58 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # zblock A simple, lightweight Discord RSS bot + +## Requirements +- curl +- concord +- libmrss + +## TODO +[ ] Add build instructions +[ ] Get all new feeds, not just the first one +[ ] Set permissions for add and remove command +[ ] Import feeds from disk on startup +[ ] Remove all feeds if bot is removed from a guild +[ ] Implement list command +[ ] Implement remove command + diff --git a/config.json b/config.json new file mode 100644 index 0000000..5ea7951 --- /dev/null +++ b/config.json @@ -0,0 +1,24 @@ +{ + "logging": { + "level": "info", + "filename": "bot.log", + "quiet": true, + "overwrite": true, + "use_color": true, + "http": { + "enable": false, + "filename": "http.log" + }, + "disable_modules": ["WEBSOCKETS", "USER_AGENT"] + }, + "discord": { + "token": "YOUR-BOT-TOKEN", + "default_prefix": { + "enable": false, + "prefix": "YOUR-COMMANDS-PREFIX" + } + }, + "zblock": { + "database_path": "feeds" + } +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..c71a739 --- /dev/null +++ b/main.c @@ -0,0 +1,244 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +// Function pointer type for commands +typedef void (*command_func)(struct discord *, const struct discord_interaction *); + +struct bot_command { + struct discord_create_global_application_command cmd; + const command_func func; +}; + +#define P99_PROTECT(...) __VA_ARGS__ + +// absolutely ridiculous preprocessor hack. +#define _CREATE_OPTIONS(options) &(struct discord_application_command_options) { .size = sizeof((struct discord_application_command_option[]) options) / sizeof(struct discord_application_command_option), .array = (struct discord_application_command_option[]) options } +#define CREATE_OPTIONS(...) _CREATE_OPTIONS(P99_PROTECT(__VA_ARGS__)) + +#define BOT_COMMAND_NOT_IMPLEMENTED() do { \ + struct discord_interaction_response res = { \ + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, \ + .data = &(struct discord_interaction_callback_data) { \ + .content = "This command has not been implemented yet." \ + } \ + }; \ + discord_create_interaction_response(client, event->id, event->token, &res, NULL); \ +} while (0) + +struct feed_info { + char *title; + char *url; + char *pubDate; + u64snowflake guild_id; + u64snowflake channel_id; + unsigned timer_id; + unsigned feed_id; +}; + +void feed_info_free(struct feed_info *feed) { + free(feed->title); + free(feed->url); + free(feed->pubDate); + free(feed); +} + +static const char *database_path = "feeds"; + +// default interval for the feed retrieval timer +#define TIMER_INTERVAL 600 + +// this just barely works at the moment +static void timer_retrieve_feeds(struct discord *client, struct discord_timer *timer) { + struct feed_info *feed = timer->data; + + struct mrss_t *mrss_feed; + if (mrss_parse_url(feed->url, &mrss_feed)) return; // do nothing on error + + // get publication date and check if it is the same (change this later this is lazy and won't get all new feeds) + if (strcmp(mrss_feed->item->pubDate, feed->pubDate)) { + // Send new entry in the feed + char msg[DISCORD_MAX_MESSAGE_LEN]; + snprintf(msg, sizeof(msg), "## %s\n### %s\n%s", mrss_feed->title, mrss_feed->item->title, mrss_feed->item->link); + struct discord_create_message res = { .content = msg }; + discord_create_message(client, feed->channel_id, &res, NULL); + } + + mrss_free(mrss_feed); +} + +static void bot_command_add(struct discord *client, const struct discord_interaction *event) { + char msg[DISCORD_MAX_MESSAGE_LEN]; + struct feed_info *feed = calloc(1, sizeof(struct feed_info)); + if (!feed) { + snprintf(msg, sizeof(msg), "Error adding feed: %s", strerror(errno)); + goto send_msg; + } + + feed->url = strdup(event->data->options->array[0].value); + if (!feed->url) { + snprintf(msg, sizeof(msg), "Error adding feed: %s", strerror(errno)); + feed_info_free(feed); + goto send_msg; + } + + mrss_t *mrss_feed = NULL; + if(mrss_parse_url(feed->url, &mrss_feed)) { + // error here figure this out + feed_info_free(feed); + goto send_msg; + } + + feed->title = mrss_feed->title; + feed->pubDate = mrss_feed->item->pubDate; + feed->feed_id = rand(); + + + char file_path[PATH_MAX]; + // check if we ran out of path + int path_len = snprintf(file_path, sizeof(file_path), "%s/%lu/%lu/%x", database_path, event->guild_id, event->channel_id, feed->feed_id); + + FILE *fp = fopen(file_path, "w"); + if (!fp) { + snprintf(msg, sizeof(msg), "Error adding feed: %s", strerror(errno)); + mrss_free(mrss_feed); + feed_info_free(feed); + goto send_msg; + } + fprintf(fp, "title=%s\nurl=%s\npubDate=%s\n", feed->title, feed->url, feed->pubDate); + fclose(fp); + + // spawn the timer for this feed + feed->timer_id = discord_timer_interval(client, timer_retrieve_feeds, NULL, feed, 0, TIMER_INTERVAL, -1); + + mrss_free(mrss_feed); + + // send the confirmation message + snprintf(msg, sizeof(msg), "The following feed has been successfully added to this channel:\n`%s`", feed->url); + + send_msg: + struct discord_interaction_response res = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .content = msg + } + }; + + discord_create_interaction_response(client, event->id, event->token, &res, NULL); +} + +static void bot_command_list(struct discord *client, const struct discord_interaction *event) { + BOT_COMMAND_NOT_IMPLEMENTED(); +} + +static void bot_command_help(struct discord *client, const struct discord_interaction *event) { + char msg[DISCORD_MAX_MESSAGE_LEN]; + + // intro message + snprintf( + msg, sizeof(msg), + "Hello %s, welcome to zblock, a lightweight RSS bot for Discord!\n" + "You can find the source code for this bot at https://github.com/WCBROW01/zblock\n" + "Please submit any bugs or issues there, or feel free to make a pull request!", + event->user ? event->user->username : event->member->user->username + ); + + struct discord_interaction_response res = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .content = msg + } + }; + + discord_create_interaction_response(client, event->id, event->token, &res, NULL); +} + +static struct bot_command commands[] = { + { + .cmd = { + .name = "add", + .description = "Add an RSS feed", + .default_permission = true, + .options = CREATE_OPTIONS({ + { + .type = DISCORD_APPLICATION_OPTION_STRING, + .name = "url", + .description = "The URL of your feed", + .required = true + } + }) + }, + .func = &bot_command_add + }, + { + .cmd = { + .name = "list", + .description = "List the RSS feeds in the current channel", + .default_permission = true + }, + .func = &bot_command_list + }, + { + .cmd = { + .name = "help", + .description = "Get help on how to use the bot", + .dm_permission = true + }, + .func = &bot_command_help + } +}; + +static void on_ready(struct discord *client, const struct discord_ready *event) { + log_info("Logged in as %s!", event->user->username); + + // create commands + for (struct bot_command *i = commands; i < commands + sizeof(commands) / sizeof(*commands); ++i) { + discord_create_global_application_command(client, event->application->id, &i->cmd, NULL); + } + + // create feed retrieval timers + + + log_info("Ready!"); +} + +static void on_interaction(struct discord *client, const struct discord_interaction *event) { + if (event->type != DISCORD_INTERACTION_APPLICATION_COMMAND) + return; // not a slash command + + // invoke the command + for (struct bot_command *i = commands; i < commands + sizeof(commands) / sizeof(*commands); ++i) { + if (!strcmp(event->data->name, i->cmd.name)) { + i->func(client, event); + return; + } + } + + // not a real command + struct discord_interaction_response res = { + .type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE, + .data = &(struct discord_interaction_callback_data) { + .content = "Invalid command, contact the maintainer of this bot." + } + }; + discord_create_interaction_response(client, event->id, event->token, &res, NULL); +} + +int main(void) { + srand(time(NULL)); + struct discord *client = discord_config_init("config.json"); + discord_set_on_ready(client, &on_ready); + discord_set_on_interaction_create(client, &on_interaction); + discord_run(client); + discord_cleanup(client); + ccord_global_cleanup(); +}