diff --git a/c/.gitignore b/c/.gitignore index 12d8518..a111a6f 100644 --- a/c/.gitignore +++ b/c/.gitignore @@ -1,3 +1,6 @@ main deltachat-db -.clangd \ No newline at end of file +.clangd +deltachat_c_echo_bot +*.db +*.db-blobs diff --git a/c/Makefile b/c/Makefile new file mode 100644 index 0000000..b1e8bb1 --- /dev/null +++ b/c/Makefile @@ -0,0 +1,32 @@ +CC = gcc +DELTACHAT_PKG_CONFIG = /usr/local/lib64/pkgconfig +DELTACHAT_INCLUDE = /usr/local/include +DELTACHAT_LIB = /usr/local/lib64 + +.PHONY = all manual pkgconfig + +all: + @echo if you know the directory of deltachat.pc, do: + @echo make DELTACHAT_PKG_CONFIG="" pkgconfig + @echo or if you know the directories of deltachat.h and libdeltachat.*, do: + @echo make DELTACHAT_INCLUDE="" DELTACHAT_LIB="" manual + @echo + @echo if they are in /usr/local/{include,lib64}, you can do either: + @echo make manual + @echo or + @echo make pkgconfig + +pkgconfig: + $(CC) -g -O2 -Wextra -Wconversion -Werror \ + $(shell PKG_CONFIG_PATH="$(DELTACHAT_PKG_CONFIG)" \ + pkg-config --cflags --libs deltachat) \ + -Wl,-rpath,$(shell PKG_CONFIG_PATH=$(DELTACHAT_PKG_CONFIG) pkg-config \ + --libs-only-L deltachat) \ + -lpthread main.c -o deltachat_c_echo_bot + +manual: + $(CC) -g -O2 -Wextra -Wconversion -Werror \ + -I"$(DELTACHAT_INCLUDE)" -L"$(DELTACHAT_LIB)" -ldeltachat \ + -Wl,-rpath,"$(DELTACHAT_LIB)" \ + -lpthread main.c -o deltachat_c_echo_bot + diff --git a/c/README.md b/c/README.md index dfc656b..a00b0fc 100644 --- a/c/README.md +++ b/c/README.md @@ -1,50 +1,131 @@ -# Echo Bot - C +# C Echo Bot -## Requirements +- This describes a process for compiling a C echo bot, on a *POSIX*-like system, assuming you have installed `libdeltachat.{a.so}` from [DeltaChat Core Rust](https://github.com/deltachat/deltachat-core-rust). + - It will typically install the `deltachat.h` and `libdeltachat.{a,so}` into `/usr/local/{include,lib,lib64}`. +- If you are on another platform, your steps might vary. + - Please feel free to submit a PR for platform-specific instructions. -- This readme describes the process for compiling on **linux**, if you are on another platform your steps might vary. If you figgured out how to do it on your platform feel free to sumbit a pr to add your method to this readme. -- You need `libdeltachat`,`git`, `cmake`, `gcc` and `pkg-config` also `rustup` if you compile `libdeltachat` on your own +## API Documentation -### Installing libdeltachat from source + + +## Prerequisites + +- `git` +- a C compiler that allows for passing the linker flag `-Wl,-rpath`, e.g., `gcc` +- GNU `make` +- optionally, `pkg-config` +- CMake +- a Rust tool suite for *DeltaChat Core Rust* +- an installed `libdeltachat.{a.so}` from the *Core*, with: + +*either* + +- the directory containing `deltachat.pc` + - usually in `/usr/local/lib/pkgconfig` or `/usr/local/lib64/pkgconfig` + +*or* + +- the directory containing `deltachat.h` + - usually in `/usr/local/include` +- the directory of `libdeltachat.*` + - usually in `/usr/local/lib` or `/usr/local/lib64` + +## Installing *DeltaChat Core Rust* from source + + +### Assuming root access (requires Rust), defaulting to `/usr/local`: + +```sh +git clone https://github.com/deltachat/deltachat-core-rust +cd deltachat-core-rust +cmake -B build +cmake --build build +sudo cmake --install build +``` + +#### To uninstall + +```sh +cd deltachat-core-rust +sudo xargs -d\\n rm -i < build/install_manifest.txt +``` + +### No root access or to change the install location + +Use `-DCMAKE_INSTALL_PREFIX=""` with `cmake -B build`: ```sh git clone https://github.com/deltachat/deltachat-core-rust cd deltachat-core-rust -cmake -B build . && cmake --build build && sudo cmake --install build +cmake -B build -DCMAKE_INSTALL_PREFIX="" +cmake --build build +cmake --install build +# or `sudo cmake --install build` if it is a write-protected path ``` -> Info: To uninstall, run `sudo xargs -d\\n rm -f There is no way to terminate it currently. I wanted to propose a SIGTERM handler, but realized doing it properly is more difficult than it should be: deltachat/deltachat-core-rust#2280 -> So for the purpose of example it's easier to assume the bot will be running forever. -> -> ~ link2xt on https://github.com/deltachat-bot/echo/pull/13#issuecomment-790594993 +```sh +cd echo/c +./deltachat_c_echo_bot +``` + +### If you have an existing state database, i.e., `dc.db` or one from a backup + +```sh +cd echo/c +mv "" bot.db +mv "" ./bot.db-blobs +./deltachat_c_echo_bot +``` -### Useful Links -- Documentation https://c.delta.chat/ diff --git a/c/compile-and-run.sh b/c/compile-and-run.sh deleted file mode 100755 index 9f7f694..0000000 --- a/c/compile-and-run.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e - -export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig -gcc main.c -o main $(pkg-config --cflags --libs deltachat) -lpthread -LD_LIBRARY_PATH="/usr/local/lib" ./main diff --git a/c/main.c b/c/main.c index 1a16e07..127c04a 100644 --- a/c/main.c +++ b/c/main.c @@ -2,19 +2,139 @@ #include #include #include +#include -void handle_message(dc_context_t *context, int chat_id, int message_id) { - dc_chat_t *chat = dc_get_chat(context, chat_id); +#define NONE 0 +#define INFO 1 +#define INFOBANNER "[info] " +#define WARN 2 +#define WARNBANNER "[warn] " +#define ERR 3 +#define ERRBANNER "[err] " +#define MAXLOGGER 73 // 80 - max banner width +#define logit(a,b) logger ( a , __FILE__ , __LINE__ , b ) +#define strify(a) #a - if (dc_chat_get_type(chat) == DC_CHAT_TYPE_SINGLE) { - dc_msg_t *msg = dc_get_msg(context, message_id); +// global state; only written by main thread, read by both +static long log_level = NONE; +static int exiting_flag = 0; +static dc_context_t *context = 0; +static pthread_t event_thread = 0; + +// separation for thread safety +static char thread_buffer80[80]; +static char main_buffer80[80]; + +void logger (int32_t type, char* file, int lineno, char* msg) { + if (type < log_level) { + return; + } + if (type > NONE) { + fprintf(stderr, "%s:%d:\n", file, lineno); + } + switch (type) { + case NONE: + break; + case INFO: + fprintf(stderr, INFOBANNER); + break; + case WARN: + fprintf(stderr, WARNBANNER); + break; + default: + fprintf(stderr, ERRBANNER); + } + + fprintf(stderr, "%s\n", msg); + fflush(stderr); +} + +// cleanly exit +void stop_context_and_exit (int exit_code) { + // stop the event loop + logit(INFO, "setting event loop exit flag"); + exiting_flag = 1; + + // stop the io, which sends a message to the event loop + if (context) { + logit(INFO, "stopping io"); + dc_stop_io(context); + dc_stop_ongoing_process(context); + } + + // join the event loop + if (event_thread) { + int thread_join_result = pthread_join(event_thread, NULL); + if (thread_join_result) { + logit(WARN, "thread join failed on exit"); + } + } + + // remove any remaining events from the queue + if (context) { + dc_event_emitter_t *emitter = dc_get_event_emitter(context); + logit(INFO, "cleaning up remaining events"); + dc_context_unref(context); + while (dc_get_next_event(emitter)) {} + dc_event_emitter_unref(emitter); + } + + logit(INFO, "exiting"); + exit(exit_code); +} + +// signal handler for SIGINT and SIGTERM +void exit_handler(int signo) { + snprintf(main_buffer80, MAXLOGGER, "caught signal %d", signo); + logit(WARN, main_buffer80); + stop_context_and_exit(0); +} + +// handle chat messages +// called by event_thread +void handle_message (int32_t chat_id, int32_t message_id) { + snprintf(thread_buffer80, MAXLOGGER, + "chat/message id: %d/%d", chat_id, message_id); + logit(INFO, thread_buffer80); + + // dc_get_msg and dc_send_text_msg both are unsigned + if (chat_id < 0) { + snprintf(thread_buffer80, MAXLOGGER, + "chat_id < 0, %d, cannot be sent to dc_get_chat", chat_id); + logit(WARN, thread_buffer80); + return; + } + if (message_id < 0) { + snprintf(thread_buffer80, MAXLOGGER, + "message_id < 0, %d, cannot be sent to dc_get_msg", message_id); + logit(WARN, thread_buffer80); + return; + } + + // process the message + dc_chat_t *chat = dc_get_chat(context, (uint32_t)chat_id); + int32_t type = dc_chat_get_type(chat); + if (type == DC_CHAT_TYPE_SINGLE || + type == DC_CHAT_TYPE_GROUP || + type == DC_CHAT_TYPE_MAILINGLIST) { + + dc_msg_t *msg = dc_get_msg(context, (uint32_t)message_id); if (!msg) { + logit(WARN, "unable to get message handle"); return; } + // echo back + dc_msg_force_plaintext(msg); char *text = dc_msg_get_text(msg); - dc_send_text_msg(context, chat_id, text); + dc_contact_t *contact = dc_get_contact(context, dc_msg_get_from_id(msg)); + char *addr = dc_contact_get_addr(contact); + + dc_send_text_msg(context, (uint32_t)chat_id, text); + + dc_str_unref(addr); + dc_contact_unref(contact); dc_str_unref(text); dc_msg_unref(msg); } @@ -22,126 +142,161 @@ void handle_message(dc_context_t *context, int chat_id, int message_id) { dc_chat_unref(chat); } -void *event_handler(void *context) { +// event loop - separate thread from main +void *event_handler () { + dc_event_emitter_t *emitter = dc_get_event_emitter(context); dc_event_t *event; - while ((event = dc_get_next_event(emitter)) != NULL) { - // use the event as needed, e.g. dc_event_get_id() returns the type. - // once you're done, unref the event to avoid memory leakage: - int event_type = dc_event_get_id(event); - if (event_type == DC_EVENT_ERROR || event_type == DC_EVENT_INFO || - event_type == DC_EVENT_WARNING) { - char *message = dc_event_get_data2_str(event); + // process events + while (event = dc_get_next_event(emitter)) { + + // if we got a sigint or sigterm + if (exiting_flag) + { + logit(INFO, "event loop caught exit flag"); + dc_event_unref(event); + break; + } - switch (event_type) { + // process other events + int event_type = dc_event_get_id(event); + switch (event_type) { case DC_EVENT_ERROR: - printf("[Error] %s\n", message); - break; case DC_EVENT_INFO: - printf("[Info] %s\n", message); - break; case DC_EVENT_WARNING: - printf("[Warn] %s\n", message); - break; + { + char *message = dc_event_get_data2_str(event); + + switch (event_type) { + case DC_EVENT_INFO: + logit(INFO, message); + break; + case DC_EVENT_WARNING: + logit(WARN, message); + break; + default: + logit(ERR, message); + break; + } + dc_str_unref(message); } - dc_str_unref(message); - } else if (event_type == DC_EVENT_CONFIGURE_PROGRESS) { - - int progress = dc_event_get_data1_int(event); - char *comment = dc_event_get_data2_str(event); - printf("[configure-progress] %d %s\n", progress, comment); - - if (progress == 0) { - // Failed to configure - printf( - "[BOT] configuration failed, maybe your credentials are incorect? " - "look for error messages above and restart the bot to try again"); - } else if (progress == 1000) { - printf("[BOT] confuguration sucessfull, starting io"); - dc_start_io(context); + break; + + case DC_EVENT_CONFIGURE_PROGRESS: + { + int progress = dc_event_get_data1_int(event); + char *comment = dc_event_get_data2_str(event); + + snprintf(thread_buffer80, MAXLOGGER, "%d %s", progress, comment); + logit(INFO, thread_buffer80); + + if (progress <= 0) { + logit(ERR, + "configuration failed, check credentials and previous errors"); + stop_context_and_exit(1); + } else if (progress >= 1000) { + logit(INFO, + "successful configuration, starting"); + dc_start_io(context); + } + dc_str_unref(comment); } - dc_str_unref(comment); + break; + + case DC_EVENT_INCOMING_MSG: + { + int chat_id = dc_event_get_data1_int(event); + int message_id = dc_event_get_data2_int(event); - } else if (event_type == DC_EVENT_INCOMING_MSG) { - int chat_id = dc_event_get_data1_int(event); - int message_id = dc_event_get_data2_int(event); + snprintf(thread_buffer80, MAXLOGGER, "%d %d", chat_id, message_id); + logit(INFO, thread_buffer80); + + handle_message(chat_id, message_id); + } + break; - printf("[incoming-msg] %d %d\n", chat_id, message_id); - handle_message(context, chat_id, message_id); - } else { - printf("[?] unhandled event of type: %d\n", event_type); + default: + snprintf(thread_buffer80, MAXLOGGER, + "unknown event type %d", event_type); + logit(WARN, thread_buffer80); } dc_event_unref(event); } dc_event_emitter_unref(emitter); - return NULL; -} - -void stop_context(dc_context_t *context) { - dc_stop_io(context); - dc_stop_ongoing_process(context); + logit(INFO, "exited event loop"); + return 0; } int main() { - // this line is just a hacky workaround to ensure we are using a recent core - // that already auto accepts contact requests, when bot config variable is set. - // basically a core version that includes https://github.com/deltachat/deltachat-core-rust/pull/3567 merged + // make sure we have a modern version of deltachat core dc_jsonrpc_instance_t* unused; - - char *addr = getenv("addr"); - char *mailpw = getenv("mailpw"); - printf("starting bot\n"); - dc_context_t *context = dc_context_new(NULL, "deltachat-db/dc.db", NULL); + // set our log level + char *loglim = getenv("DELTACHAT_C_ECHOBOT_LOGLEVEL"); + if (loglim) { + log_level = strtol(loglim, 0, 10); + snprintf(main_buffer80, MAXLOGGER, "setting log limit to %ld", log_level); + logit(INFO, main_buffer80); + } + + // clean exit signal handlers + if (signal(SIGINT, exit_handler) == SIG_ERR) { + logit(WARN, "unable to set SIGINT handler"); + } + if (signal(SIGTERM, exit_handler) == SIG_ERR) { + logit(WARN, "unable to set SIGTERM handler"); + } - static pthread_t event_thread; - if (pthread_create(&event_thread, NULL, event_handler, context) != 0) { - printf("Event Thread creation failed\n"); - stop_context(context); - return 1; + // event handler loop + context = dc_context_new(NULL, "./bot.db", NULL); + if (pthread_create(&event_thread, NULL, event_handler, NULL)) { + logit(ERR, "thread creation failed"); + stop_context_and_exit(1); + } + + // start main processing + if (dc_is_configured(context)) { + char* addr = dc_get_config (context, "addr"); + snprintf(main_buffer80, MAXLOGGER, "starting %s", addr); + dc_set_config(context, "bot", "1"); + dc_set_config(context, "fetch_existing_msgs", "0"); } + // otherwise set our account up + else { + // setup a new account to make the database + char *addr = getenv("DELTACHAT_C_ECHOBOT_EMAIL"); + char *mailpw = getenv("DELTACHAT_C_ECHOBOT_PASSWORD"); - if (!dc_is_configured(context)) { if (!addr) { - printf("you need to specify the addr enviroment variable to the bots " - "email address\n"); + logit(ERR, "DELTACHAT_C_ECHOBOT_EMAIL environment variable not specified"); + stop_context_and_exit(1); } - if (!mailpw) { - printf("you need to specify the mailpw enviroment variable to the bots " - "email password\n"); - } - if (!addr || !mailpw) { - stop_context(context); - printf("shutting down...\n"); - int thread_join_result = pthread_join(event_thread, NULL); - if (thread_join_result != 0) { - printf("join thread failed with error code %d\n", thread_join_result); - } - return 1; + logit(ERR, "DELTACHAT_C_ECHOBOT_PASSWORD environment variable not specified"); + stop_context_and_exit(1); } - printf("configuring bot\n"); + snprintf(main_buffer80, MAXLOGGER, "first time configuring %s", addr); + logit(INFO, main_buffer80); dc_set_config(context, "addr", addr); dc_set_config(context, "mail_pw", mailpw); dc_set_config(context, "bot", "1"); dc_set_config(context, "fetch_existing_msgs", "0"); dc_configure(context); - } else { - printf("already configured, wait for messages\n"); - char* addr = dc_get_config (context, "addr"); - printf("bot address is %s\n", addr); - dc_start_io(context); + logit(INFO, "done configuring"); } // wait for event thread to complete + dc_start_io(context); int thread_join_result = pthread_join(event_thread, NULL); - if (thread_join_result != 0) { - printf("join thread failed with error code %d\n", thread_join_result); + if (thread_join_result) { + logit(WARN, "thread join failed on exit"); } + event_thread = (pthread_t)NULL; - return 0; + // clean up + stop_context_and_exit(0); }