diff --git a/src/bot/context.c b/src/bot/context.c deleted file mode 100644 index 087a1ce..0000000 --- a/src/bot/context.c +++ /dev/null @@ -1,222 +0,0 @@ -/* bot/context.c - */ - -#include "context.h" - -extern struct BotXmppCtx *g_bctx = NULL; - -struct BotXmppCtx *bot_get_ctx() -{ - return g_bctx; -} - -void bot_set_ctx(struct BotXmppCtx *ctx) -{ - if(g_bctx) { - fprintf(stderr, "Dev error: cannot set XMPP ctx twice in the same program run.\n"); - } else { - g_bctx = ctx; - } -} - -void bot_free_user_data(struct BotUserInfo user, GString *filename, bool freeSegment) -{ - g_string_free(filename, freeSegment); - g_string_free(user.jid, freeSegment); - g_string_free(user.pass, freeSegment); -} - -/* Implement https://xmpp.org/extensions/xep-0084.html */ -/* TODO need to understand the format for data stanza encoding (bytes? base64?) */ -/* int bot_xmpp_set_avatar_user(struct BotXmppCtx * const bctx, const GString *avatarFile) */ -/* { */ -/* gchar *avatar; */ -/* gsize avatarLenght; */ -/* xmpp_stanza_t *iq, *pubsub, *publish, *item, *data; */ - -/* /\* 1. Create IQ 'set' stanza *\/ */ -/* iq = xmpp_stanza_new(bctx->ctx); */ -/* xmpp_stanza_set_name(iq, "iq"); */ -/* xmpp_stanza_set_type(iq, "set"); */ -/* xmpp_stanza_set_id(iq, "publish1"); */ - -/* /\* Create protocol element*\/ */ -/* pubsub = xmpp_stanza_new(bctx->ctx); */ -/* xmpp_stanza_set_name(pubsub, "pubsub"); */ -/* xmpp_stanza_set_ns(pubsub, "http://jabber.org/protocol/pubsub"); */ -/* xmpp_stanza_add_child(iq, pubsub); */ - -/* /\* Create element *\/ */ -/* publish = xmpp_stanza_new(bctx->ctx); */ -/* xmpp_stanza_set_name(publish, "publish"); */ -/* xmpp_stanza_set_attribute(publish, "node", "urn:xmpp:avatar:data"); */ -/* xmpp_stanza_add_child(pubsub, publish); */ - -/* /\* Add where id should be the sha1sum of the image *\/ */ -/* item = xmpp_stanza_new(bctx->ctx); */ -/* xmpp_stanza_set_name(item, "item"); */ -/* xmpp_stanza_set_id(item, "SHA1SUM"); */ -/* xmpp_stanza_add_child(publish, item); */ - -/* /\* Add with base64 data *\/ */ -/* data = xmpp_stanza_new(bctx->ctx); */ -/* xmpp_stanza_set_name(data, "data"); */ -/* xmpp_stanza_set_ns(data, "urn:xmpp:avatar:data"); */ -/* if(g_file_get_contents(avatarFile->str, &avatar, &avatarLenght, NULL)) { */ -/* xmpp_stanza_set_text(data, avatar); */ -/* } else { */ -/* fprintf(stderr, "Unable to read png avatar.\n"); */ -/* } */ -/* xmpp_stanza_set_text(data, "DATA"); */ -/* xmpp_stanza_add_child(item, data); */ - -/* /\* 6. Send stanza and release it + all children *\/ */ -/* xmpp_send(bctx->conn, iq); */ -/* xmpp_stanza_release(iq); */ - -/* return 0; */ -/* } */ - - -/* implement https://xmpp.org/extensions/xep-0153.html */ -int bot_xmpp_set_avatar(struct BotXmppCtx * const bctx, const GString *avatarFile) -{ - gchar *avatar; - gsize avatarLenght; - /* GString *avatarID; */ // TODO - unsigned char hash[SHA_DIGEST_LENGTH]; /* == 20 */ - xmpp_stanza_t *iq, *vcard, *photo, *type, *binval; - - /* 1. Create IQ 'set' stanza */ - iq = xmpp_stanza_new(bctx->ctx); - xmpp_stanza_set_name(iq, "iq"); - xmpp_stanza_set_type(iq, "set"); - xmpp_stanza_set_id(iq, "vcard_set_1"); - - /* Create ctx); - xmpp_stanza_set_name(vcard, "vCard"); - xmpp_stanza_set_ns(vcard, "vcard-temp"); - xmpp_stanza_add_child(iq, vcard); - - /* Create element */ - photo = xmpp_stanza_new(bctx->ctx); - xmpp_stanza_set_name(photo, "PHOTO"); - xmpp_stanza_add_child(vcard, photo); - - /* Add (e.g., image/jpeg) */ - type = xmpp_stanza_new(bctx->ctx); - xmpp_stanza_set_name(type, "TYPE"); - xmpp_stanza_set_text(type, "image/jpeg"); - xmpp_stanza_add_child(photo, type); - - /* Add with base64 data */ - binval = xmpp_stanza_new(bctx->ctx); - xmpp_stanza_set_name(binval, "BINVAL"); - - if(g_file_get_contents(avatarFile->str, &avatar, &avatarLenght, NULL)) { - SHA1((const unsigned char*)avatarFile->str, sizeof(avatarFile->str)-1, hash); - xmpp_stanza_set_text(binval, avatar); - } else { - fprintf(stderr, "Unable to read BASE64-encoded avatar.\n"); - } - xmpp_stanza_add_child(photo, binval); - - /* 6. Send stanza and release it + all children */ - xmpp_send(bctx->conn, iq); - xmpp_stanza_release(iq); - - return 0; -} - -int bot_xmpp_startup(struct BotUserInfo user, GString *avatarFile) -{ - xmpp_log_t* newLog; - xmpp_ctx_t* newCtx; - xmpp_conn_t* newCon; - /* GString *avatarID; */ // TODO - int connectStatus; - struct BotXmppCtx *bctx = malloc(sizeof *bctx); - - /* init library */ - xmpp_initialize(); - - /* create a context */ - newLog = xmpp_get_default_logger(XMPP_LEVEL_DEBUG); /* pass NULL instead to silence output */ - newCtx = xmpp_ctx_new(NULL, newLog); - - if (!(newLog && newCtx)) { - fprintf(stderr, "Unable to startup. Context creation failed."); - return 1; - } - - /* create a connection */ - newCon = xmpp_conn_new(newCtx); - - if (!newCon) { - fprintf(stderr, "Unable to startup. Connection failed."); - return 1; - } - - /* setup authentication information */ - xmpp_conn_set_jid(newCon, user.jid->str); - xmpp_conn_set_pass(newCon, user.pass->str); - - bctx->log = newLog; - bctx->ctx = newCtx; - bctx->conn = newCon; - - /* TODO properly set avatarID in presence stanza */ - bot_xmpp_set_avatar(bctx, avatarFile); - - /* initiate connection */ - connectStatus = xmpp_connect_client(newCon, NULL, 0, conn_handler, newCtx); - if(connectStatus != 0) { - /* connecting went wrong */ - return connectStatus; - } - - bot_set_ctx(bctx); - - /* install signal handler */ - signal(SIGALRM, reminder_alarm_func); - return 0; -} - -void bot_xmpp_shutdown() -{ - struct BotXmppCtx *bctx = bot_get_ctx(); - - /* enter the event loop - - our connect handler will trigger an exit */ - xmpp_run(bctx->ctx); - - /* release our connection and context */ - xmpp_conn_release(bctx->conn); - xmpp_ctx_free(bctx->ctx); - - /* final shutdown of the library */ - xmpp_shutdown(); -} - -/* send a message when SIGALRM is triggered */ -void reminder_alarm_func(int s) -{ - unsigned int minutes; - BotReminder *reminder = bot_get_last_reminder(); - struct BotXmppCtx *bctx = bot_get_ctx(); - GString *msg = g_string_new(NULL); - - if(!reminder) { - fprintf(stderr, "SIGALRM was raised but no reminder was set.\n"); - return; - } - - minutes = reminder->minutes; - g_string_printf(msg, "OHI! Segna le spese! Te lo ricordo di nuovo tra %d minuti.\n", minutes); - send_message(bctx->conn, reminder->from, bctx->ctx, msg); - g_string_free(msg, true); - - /* reinstall alarm */ - alarm(minutes * 60); -} diff --git a/src/bot/context.h b/src/bot/context.h deleted file mode 100644 index 2ad25fd..0000000 --- a/src/bot/context.h +++ /dev/null @@ -1,48 +0,0 @@ -/* bot/context.h - * --- - * Implement XMPP connection startup, shutdown, context and user data - * structures. - * --- - */ - -#pragma once - -#include -#include -#include -#include -#include -#include /* For SHA1 */ -#include -#include -#include "handlers.h" - -struct BotUserInfo { - GString* jid; - GString* pass; -}; - -struct BotXmppCtx { - xmpp_log_t* log; - xmpp_ctx_t* ctx; - xmpp_conn_t* conn; -}; - -void bot_free_user_data(struct BotUserInfo user, GString *filename, bool freeSegment); - -/* sets the given avatar file (base64 encoded) */ -int bot_xmpp_set_avatar(struct BotXmppCtx * const bctx, const GString *avatarFile); - -/* perform xmpp startup, store in bctx if completed successfully */ -int bot_xmpp_startup(struct BotUserInfo user, GString *avatarFile); - -/* teardown and free ctx and connection in bctx */ -void bot_xmpp_shutdown(); - -/* send a reminder when SIGALARM is triggered */ -void reminder_alarm_func(int s); - -/* global context for signal handling */ -extern struct BotXmppCtx *g_bctx; -struct BotXmppCtx *bot_get_ctx(); -void bot_set_ctx(struct BotXmppCtx *bctx); diff --git a/src/bot/handlers.c b/src/bot/handlers.c deleted file mode 100644 index 3c184a7..0000000 --- a/src/bot/handlers.c +++ /dev/null @@ -1,159 +0,0 @@ -/* bot/handlers.c - */ - -#include "handlers.h" - -int version_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata) -{ - xmpp_stanza_t *reply, *query, *name, *version, *text; - const char *ns; - xmpp_ctx_t *ctx = (xmpp_ctx_t*)userdata; - printf("Received version request from %s\n", xmpp_stanza_get_attribute(stanza, "from")); - - reply = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(reply, "iq"); - xmpp_stanza_set_type(reply, "result"); - xmpp_stanza_set_id(reply, xmpp_stanza_get_id(stanza)); - xmpp_stanza_set_attribute(reply, "to", xmpp_stanza_get_attribute(stanza, "from")); - - query = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(query, "query"); - ns = xmpp_stanza_get_ns(xmpp_stanza_get_children(stanza)); - if (ns) { - xmpp_stanza_set_ns(query, ns); - } - - name = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(name, "name"); - xmpp_stanza_add_child(query, name); - - text = xmpp_stanza_new(ctx); - xmpp_stanza_set_text(text, "libstrophe example bot"); - xmpp_stanza_add_child(name, text); - - version = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(version, "version"); - xmpp_stanza_add_child(query, version); - - text = xmpp_stanza_new(ctx); - xmpp_stanza_set_text(text, "1.0"); - xmpp_stanza_add_child(version, text); - - xmpp_stanza_add_child(reply, query); - - xmpp_send(conn, reply); - xmpp_stanza_release(reply); - return 1; -} - - -int message_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata) -{ - unsigned int lastId; - xmpp_stanza_t *reply, *body, *text; - xmpp_ctx_t *ctx = (xmpp_ctx_t*)userdata; - GString *intext; - GString *replytext; - BotReminder *reminder; - - if(!xmpp_stanza_get_child_by_name(stanza, "body")) return 1; - if(!strcmp(xmpp_stanza_get_attribute(stanza, "type"), "error")) return 1; - - intext = g_string_new(xmpp_stanza_get_text(xmpp_stanza_get_child_by_name(stanza, "body"))); - - printf("Incoming message from %s: %s\n", xmpp_stanza_get_attribute(stanza, "from"), intext->str); - - /* parse with yyparse() */ - - reply = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(reply, "message"); - xmpp_stanza_set_type(reply, xmpp_stanza_get_type(stanza)?xmpp_stanza_get_type(stanza):"chat"); - xmpp_stanza_set_attribute(reply, "to", xmpp_stanza_get_attribute(stanza, "from")); - - body = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(body, "body"); - - replytext = g_string_new(NULL); - - /* parse and store the message */ - if(grammar_parse_string(intext) == 0) { - switch(bot_get_last_command()) { - case BOT_CMD_IN: - case BOT_CMD_OUT: - lastId = (bot_get_last_expense())->id; - g_string_printf(replytext, "OK! Ho salvato l'ultima spesa, id: %u\n", lastId); - break; - case BOT_CMD_REMINDER: - // TODO this allocates twice, fix - reminder = malloc(sizeof *reminder); - reminder->minutes = (bot_get_last_reminder())->minutes; - reminder->from = g_string_new(strdup(xmpp_stanza_get_attribute(stanza, "from"))); - bot_set_last_reminder(reminder); - g_string_printf(replytext, "Va bene, ti mando un messaggio tra %d minuti.\n", reminder->minutes); - break; - default: - g_string_printf(replytext, "Non ho riconosciuto il comando: %s", intext->str); - break; - } - } else { - g_string_printf(replytext, "Stavo rosicchiando una ghianda e non ho capito. Che vuol dire \"%s\"?\n%s\n", intext->str, HELPTEXT); - } - - text = xmpp_stanza_new(ctx); - xmpp_stanza_set_text(text, replytext->str); - xmpp_stanza_add_child(body, text); - xmpp_stanza_add_child(reply, body); - - xmpp_send(conn, reply); - xmpp_stanza_release(reply); - g_string_free(replytext, true); - g_string_free(intext, true); - - return 1; -} - -/* define a handler for connection events */ -void conn_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, - const int error, xmpp_stream_error_t * const stream_error, - void * const userdata) -{ - xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata; - - if (status == XMPP_CONN_CONNECT) { - xmpp_stanza_t* pres; - fprintf(stderr, "DEBUG: connected\n"); - xmpp_handler_add(conn,version_handler, "jabber:iq:version", "iq", NULL, ctx); - xmpp_handler_add(conn,message_handler, NULL, "message", NULL, ctx); - - /* Send initial so that we appear online to contacts */ - pres = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(pres, "presence"); - xmpp_send(conn, pres); - xmpp_stanza_release(pres); - } - else { - fprintf(stderr, "DEBUG: disconnected\n"); - xmpp_stop(ctx); - } -} - -void send_message(xmpp_conn_t * const conn, GString * const from, xmpp_ctx_t * const ctx, const GString *msg) -{ - xmpp_stanza_t *reply, *body, *text; - - reply = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(reply, "message"); - xmpp_stanza_set_type(reply, "chat"); - xmpp_stanza_set_attribute(reply, "to", from->str); - - body = xmpp_stanza_new(ctx); - xmpp_stanza_set_name(body, "body"); - - text = xmpp_stanza_new(ctx); - xmpp_stanza_set_text(text, msg->str); - xmpp_stanza_add_child(body, text); - xmpp_stanza_add_child(reply, body); - - xmpp_send(conn, reply); - xmpp_stanza_release(reply); -} diff --git a/src/bot/handlers.h b/src/bot/handlers.h deleted file mode 100644 index 0ab7373..0000000 --- a/src/bot/handlers.h +++ /dev/null @@ -1,32 +0,0 @@ -/* bot/handlers.h - */ - -#pragma once -#include -#include -#include -#include -#include -#include "../../build/expenses/expenses.tab.h" -#include "../grammar/types.h" - -#define HELPTEXT "---\n\ -Comandi:\n\ -Per registrare una spesa:\n<-|out|spesa> \"\"\n\ -Per registrare un ricavo:\n<+|in|ricavo> \"\"\n\ -\n\ -Esempi:\n\ -+10000.99 ele \"Sono ricca!\"\n\ -out 5000 ele \"Oh no le tasse\"" - -/* respond to version queries */ -int version_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata); - -/* react and respond to incoming messages */ -int message_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata); - -/* define a handler for connection events */ -void conn_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, const int error, xmpp_stream_error_t * const stream_error, void * const userdata); - -/* sends a text message */ -void send_message(xmpp_conn_t * const conn, GString * const from, xmpp_ctx_t * const ctx, const GString *msg); diff --git a/src/core/context.c b/src/core/context.c new file mode 100644 index 0000000..323fe57 --- /dev/null +++ b/src/core/context.c @@ -0,0 +1,223 @@ +/* bot/context.c + */ + +#include "context.h" + +extern struct BotXmppCtx *g_bctx = NULL; + +struct BotXmppCtx *bot_get_ctx() +{ + return g_bctx; +} + +void bot_set_ctx(struct BotXmppCtx *ctx) +{ + if(g_bctx) { + fprintf(stderr, "Dev error: cannot set XMPP ctx twice in the same program run.\n"); + } else { + g_bctx = ctx; + } +} + +void bot_free_user_data(struct BotUserInfo user, GString *filename, bool freeSegment) +{ + g_string_free(filename, freeSegment); + g_string_free(user.jid, freeSegment); + g_string_free(user.pass, freeSegment); +} + +/* Implement https://xmpp.org/extensions/xep-0084.html */ +/* TODO need to understand the format for data stanza encoding (bytes? base64?) */ +/* int bot_xmpp_set_avatar_user(struct BotXmppCtx * const bctx, const GString *avatarFile) */ +/* { */ +/* gchar *avatar; */ +/* gsize avatarLenght; */ +/* xmpp_stanza_t *iq, *pubsub, *publish, *item, *data; */ + +/* /\* 1. Create IQ 'set' stanza *\/ */ +/* iq = xmpp_stanza_new(bctx->ctx); */ +/* xmpp_stanza_set_name(iq, "iq"); */ +/* xmpp_stanza_set_type(iq, "set"); */ +/* xmpp_stanza_set_id(iq, "publish1"); */ + +/* /\* Create protocol element*\/ */ +/* pubsub = xmpp_stanza_new(bctx->ctx); */ +/* xmpp_stanza_set_name(pubsub, "pubsub"); */ +/* xmpp_stanza_set_ns(pubsub, "http://jabber.org/protocol/pubsub"); */ +/* xmpp_stanza_add_child(iq, pubsub); */ + +/* /\* Create element *\/ */ +/* publish = xmpp_stanza_new(bctx->ctx); */ +/* xmpp_stanza_set_name(publish, "publish"); */ +/* xmpp_stanza_set_attribute(publish, "node", "urn:xmpp:avatar:data"); */ +/* xmpp_stanza_add_child(pubsub, publish); */ + +/* /\* Add where id should be the sha1sum of the image *\/ */ +/* item = xmpp_stanza_new(bctx->ctx); */ +/* xmpp_stanza_set_name(item, "item"); */ +/* xmpp_stanza_set_id(item, "SHA1SUM"); */ +/* xmpp_stanza_add_child(publish, item); */ + +/* /\* Add with base64 data *\/ */ +/* data = xmpp_stanza_new(bctx->ctx); */ +/* xmpp_stanza_set_name(data, "data"); */ +/* xmpp_stanza_set_ns(data, "urn:xmpp:avatar:data"); */ +/* if(g_file_get_contents(avatarFile->str, &avatar, &avatarLenght, NULL)) { */ +/* xmpp_stanza_set_text(data, avatar); */ +/* } else { */ +/* fprintf(stderr, "Unable to read png avatar.\n"); */ +/* } */ +/* xmpp_stanza_set_text(data, "DATA"); */ +/* xmpp_stanza_add_child(item, data); */ + +/* /\* 6. Send stanza and release it + all children *\/ */ +/* xmpp_send(bctx->conn, iq); */ +/* xmpp_stanza_release(iq); */ + +/* return 0; */ +/* } */ + + +/* implement https://xmpp.org/extensions/xep-0153.html */ +int bot_xmpp_set_avatar(struct BotXmppCtx * const bctx, const GString *avatarFile) +{ + gchar *avatar; + gsize avatarLenght; + /* GString *avatarID; */ // TODO + unsigned char hash[SHA_DIGEST_LENGTH]; /* == 20 */ + xmpp_stanza_t *iq, *vcard, *photo, *type, *binval; + + /* 1. Create IQ 'set' stanza */ + iq = xmpp_stanza_new(bctx->ctx); + xmpp_stanza_set_name(iq, "iq"); + xmpp_stanza_set_type(iq, "set"); + xmpp_stanza_set_id(iq, "vcard_set_1"); + + /* Create ctx); + xmpp_stanza_set_name(vcard, "vCard"); + xmpp_stanza_set_ns(vcard, "vcard-temp"); + xmpp_stanza_add_child(iq, vcard); + + /* Create element */ + photo = xmpp_stanza_new(bctx->ctx); + xmpp_stanza_set_name(photo, "PHOTO"); + xmpp_stanza_add_child(vcard, photo); + + /* Add (e.g., image/jpeg) */ + type = xmpp_stanza_new(bctx->ctx); + xmpp_stanza_set_name(type, "TYPE"); + xmpp_stanza_set_text(type, "image/jpeg"); + xmpp_stanza_add_child(photo, type); + + /* Add with base64 data */ + binval = xmpp_stanza_new(bctx->ctx); + xmpp_stanza_set_name(binval, "BINVAL"); + + if(g_file_get_contents(avatarFile->str, &avatar, &avatarLenght, NULL)) { + SHA1((const unsigned char*)avatarFile->str, sizeof(avatarFile->str)-1, hash); + xmpp_stanza_set_text(binval, avatar); + } else { + fprintf(stderr, "Unable to read BASE64-encoded avatar.\n"); + } + xmpp_stanza_add_child(photo, binval); + + /* 6. Send stanza and release it + all children */ + xmpp_send(bctx->conn, iq); + xmpp_stanza_release(iq); + + return 0; +} + +int bot_xmpp_startup(struct BotUserInfo user, GString *avatarFile) +{ + xmpp_log_t* newLog; + xmpp_ctx_t* newCtx; + xmpp_conn_t* newCon; + /* GString *avatarID; */ // TODO + int connectStatus; + struct BotXmppCtx *bctx = malloc(sizeof *bctx); + + /* init library */ + xmpp_initialize(); + + /* create a context */ + newLog = xmpp_get_default_logger(XMPP_LEVEL_DEBUG); /* pass NULL instead to silence output */ + newCtx = xmpp_ctx_new(NULL, newLog); + + if (!(newLog && newCtx)) { + fprintf(stderr, "Unable to startup. Context creation failed."); + return 1; + } + + /* create a connection */ + newCon = xmpp_conn_new(newCtx); + + if (!newCon) { + fprintf(stderr, "Unable to startup. Connection failed."); + return 1; + } + + /* setup authentication information */ + xmpp_conn_set_jid(newCon, user.jid->str); + xmpp_conn_set_pass(newCon, user.pass->str); + + bctx->log = newLog; + bctx->ctx = newCtx; + bctx->conn = newCon; + + /* TODO properly set avatarID in presence stanza */ + bot_xmpp_set_avatar(bctx, avatarFile); + + /* initiate connection */ + connectStatus = xmpp_connect_client(newCon, NULL, 0, conn_handler, newCtx); + if(connectStatus != 0) { + /* connecting went wrong */ + return connectStatus; + } + + /* store ctx as global */ + bot_set_ctx(bctx); + + /* install signal handler */ + signal(SIGALRM, bot_reminder_alarm_func); + return 0; +} + +void bot_xmpp_shutdown() +{ + struct BotXmppCtx *bctx = bot_get_ctx(); + + /* enter the event loop - + our connect handler will trigger an exit */ + xmpp_run(bctx->ctx); + + /* release our connection and context */ + xmpp_conn_release(bctx->conn); + xmpp_ctx_free(bctx->ctx); + + /* final shutdown of the library */ + xmpp_shutdown(); +} + +/* send a message when SIGALRM is triggered */ +void bot_reminder_alarm_func(int s) +{ + unsigned int minutes; + BotReminder *reminder = bot_get_last_reminder(); + struct BotXmppCtx *bctx = bot_get_ctx(); + GString *msg = g_string_new(NULL); + + if(!reminder) { + fprintf(stderr, "SIGALRM was raised but no reminder was set.\n"); + return; + } + + minutes = reminder->minutes; + g_string_printf(msg, "OHI! Segna le spese! Te lo ricordo di nuovo tra %d minuti.\n", minutes); + send_message(bctx->conn, reminder->from, bctx->ctx, msg); + g_string_free(msg, true); + + /* reinstall alarm */ + alarm(minutes * 60); +} diff --git a/src/core/context.h b/src/core/context.h new file mode 100644 index 0000000..90d6d72 --- /dev/null +++ b/src/core/context.h @@ -0,0 +1,48 @@ +/* bot/context.h + * --- + * Implement XMPP connection startup, shutdown, context and user data + * structures. + * --- + */ + +#pragma once + +#include +#include +#include +#include +#include +#include /* For SHA1 */ +#include +#include +#include "handlers.h" + +struct BotUserInfo { + GString* jid; + GString* pass; +}; + +struct BotXmppCtx { + xmpp_log_t* log; + xmpp_ctx_t* ctx; + xmpp_conn_t* conn; +}; + +void bot_free_user_data(struct BotUserInfo user, GString *filename, bool freeSegment); + +/* sets the given avatar file (base64 encoded) */ +int bot_xmpp_set_avatar(struct BotXmppCtx * const bctx, const GString *avatarFile); + +/* perform xmpp startup, store in bctx if completed successfully */ +int bot_xmpp_startup(struct BotUserInfo user, GString *avatarFile); + +/* teardown and free ctx and connection in bctx */ +void bot_xmpp_shutdown(); + +/* send a reminder when SIGALARM is triggered */ +void bot_reminder_alarm_func(int s); + +/* global context for signal handling */ +extern struct BotXmppCtx *g_bctx; +struct BotXmppCtx *bot_get_ctx(); +void bot_set_ctx(struct BotXmppCtx *bctx); diff --git a/src/core/handlers.c b/src/core/handlers.c new file mode 100644 index 0000000..3c184a7 --- /dev/null +++ b/src/core/handlers.c @@ -0,0 +1,159 @@ +/* bot/handlers.c + */ + +#include "handlers.h" + +int version_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata) +{ + xmpp_stanza_t *reply, *query, *name, *version, *text; + const char *ns; + xmpp_ctx_t *ctx = (xmpp_ctx_t*)userdata; + printf("Received version request from %s\n", xmpp_stanza_get_attribute(stanza, "from")); + + reply = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(reply, "iq"); + xmpp_stanza_set_type(reply, "result"); + xmpp_stanza_set_id(reply, xmpp_stanza_get_id(stanza)); + xmpp_stanza_set_attribute(reply, "to", xmpp_stanza_get_attribute(stanza, "from")); + + query = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(query, "query"); + ns = xmpp_stanza_get_ns(xmpp_stanza_get_children(stanza)); + if (ns) { + xmpp_stanza_set_ns(query, ns); + } + + name = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name, "name"); + xmpp_stanza_add_child(query, name); + + text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(text, "libstrophe example bot"); + xmpp_stanza_add_child(name, text); + + version = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(version, "version"); + xmpp_stanza_add_child(query, version); + + text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(text, "1.0"); + xmpp_stanza_add_child(version, text); + + xmpp_stanza_add_child(reply, query); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); + return 1; +} + + +int message_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata) +{ + unsigned int lastId; + xmpp_stanza_t *reply, *body, *text; + xmpp_ctx_t *ctx = (xmpp_ctx_t*)userdata; + GString *intext; + GString *replytext; + BotReminder *reminder; + + if(!xmpp_stanza_get_child_by_name(stanza, "body")) return 1; + if(!strcmp(xmpp_stanza_get_attribute(stanza, "type"), "error")) return 1; + + intext = g_string_new(xmpp_stanza_get_text(xmpp_stanza_get_child_by_name(stanza, "body"))); + + printf("Incoming message from %s: %s\n", xmpp_stanza_get_attribute(stanza, "from"), intext->str); + + /* parse with yyparse() */ + + reply = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(reply, "message"); + xmpp_stanza_set_type(reply, xmpp_stanza_get_type(stanza)?xmpp_stanza_get_type(stanza):"chat"); + xmpp_stanza_set_attribute(reply, "to", xmpp_stanza_get_attribute(stanza, "from")); + + body = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(body, "body"); + + replytext = g_string_new(NULL); + + /* parse and store the message */ + if(grammar_parse_string(intext) == 0) { + switch(bot_get_last_command()) { + case BOT_CMD_IN: + case BOT_CMD_OUT: + lastId = (bot_get_last_expense())->id; + g_string_printf(replytext, "OK! Ho salvato l'ultima spesa, id: %u\n", lastId); + break; + case BOT_CMD_REMINDER: + // TODO this allocates twice, fix + reminder = malloc(sizeof *reminder); + reminder->minutes = (bot_get_last_reminder())->minutes; + reminder->from = g_string_new(strdup(xmpp_stanza_get_attribute(stanza, "from"))); + bot_set_last_reminder(reminder); + g_string_printf(replytext, "Va bene, ti mando un messaggio tra %d minuti.\n", reminder->minutes); + break; + default: + g_string_printf(replytext, "Non ho riconosciuto il comando: %s", intext->str); + break; + } + } else { + g_string_printf(replytext, "Stavo rosicchiando una ghianda e non ho capito. Che vuol dire \"%s\"?\n%s\n", intext->str, HELPTEXT); + } + + text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(text, replytext->str); + xmpp_stanza_add_child(body, text); + xmpp_stanza_add_child(reply, body); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); + g_string_free(replytext, true); + g_string_free(intext, true); + + return 1; +} + +/* define a handler for connection events */ +void conn_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, + const int error, xmpp_stream_error_t * const stream_error, + void * const userdata) +{ + xmpp_ctx_t *ctx = (xmpp_ctx_t *)userdata; + + if (status == XMPP_CONN_CONNECT) { + xmpp_stanza_t* pres; + fprintf(stderr, "DEBUG: connected\n"); + xmpp_handler_add(conn,version_handler, "jabber:iq:version", "iq", NULL, ctx); + xmpp_handler_add(conn,message_handler, NULL, "message", NULL, ctx); + + /* Send initial so that we appear online to contacts */ + pres = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pres, "presence"); + xmpp_send(conn, pres); + xmpp_stanza_release(pres); + } + else { + fprintf(stderr, "DEBUG: disconnected\n"); + xmpp_stop(ctx); + } +} + +void send_message(xmpp_conn_t * const conn, GString * const from, xmpp_ctx_t * const ctx, const GString *msg) +{ + xmpp_stanza_t *reply, *body, *text; + + reply = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(reply, "message"); + xmpp_stanza_set_type(reply, "chat"); + xmpp_stanza_set_attribute(reply, "to", from->str); + + body = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(body, "body"); + + text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(text, msg->str); + xmpp_stanza_add_child(body, text); + xmpp_stanza_add_child(reply, body); + + xmpp_send(conn, reply); + xmpp_stanza_release(reply); +} diff --git a/src/core/handlers.h b/src/core/handlers.h new file mode 100644 index 0000000..268d937 --- /dev/null +++ b/src/core/handlers.h @@ -0,0 +1,32 @@ +/* bot/handlers.h + */ + +#pragma once +#include +#include +#include +#include +#include +#include "../../build/expenses/grammar.tab.h" +#include "../expenses/types.h" + +#define HELPTEXT "---\n\ +Comandi:\n\ +Per registrare una spesa:\n<-|out|spesa> \"\"\n\ +Per registrare un ricavo:\n<+|in|ricavo> \"\"\n\ +\n\ +Esempi:\n\ ++10000.99 ele \"Sono ricca!\"\n\ +out 5000 ele \"Oh no le tasse\"" + +/* respond to version queries */ +int version_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata); + +/* react and respond to incoming messages */ +int message_handler(xmpp_conn_t * const conn, xmpp_stanza_t * const stanza, void * const userdata); + +/* define a handler for connection events */ +void conn_handler(xmpp_conn_t * const conn, const xmpp_conn_event_t status, const int error, xmpp_stream_error_t * const stream_error, void * const userdata); + +/* sends a text message */ +void send_message(xmpp_conn_t * const conn, GString * const from, xmpp_ctx_t * const ctx, const GString *msg); diff --git a/src/expenses/csv.c b/src/expenses/csv.c new file mode 100644 index 0000000..1069990 --- /dev/null +++ b/src/expenses/csv.c @@ -0,0 +1,90 @@ +#include "csv.h" + +int bot_get_current_entry_id_csv +( + const GString* filename +) +{ + /* TODO this counts by char, probably more efficient ways exist */ + char c; + FILE *fp; + unsigned int count = 0; + + if((fp = fopen(filename->str, "r"))) { + for (c = getc(fp); c != EOF; c = getc(fp)) { + if (c == '\n') { + count++; + } + } + fclose(fp); + } + + return count; +} + +/* returns the number of bytes written, following the convention of fprintf. + * Returns negative in case of error + */ +int write_bot_expense_entry +( + const GString *filename, + BotExpenseEntry *entry, + bool needsHeaders +) +{ + FILE *fp; + int result; + GString *line = serialize_bot_expense_entry_csv(entry); + + if(line == NULL) { + return -1; + } + + if((fp = fopen(filename->str, "a"))) { + if(needsHeaders) { + fprintf(fp, "ID,Time,Value,User,Notes\n"); + } + + /* line is terminated with '\n' */ + result = fprintf(fp, "%s", line->str); + fclose(fp); + + return result; + fprintf(stderr, "Error opening %s\n", filename->str); + } + + fprintf(stderr, "Error opening %s\n", filename->str); + return -1; +} + +/* whole routine to write an expense to a CSV file */ +int write_update_to_csv +( + const GString *filename, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +) +{ + int result; + unsigned int id; + BotExpenseEntry *entry; + BotExpenseEntry *previous = bot_get_last_expense(); + + id = (previous == NULL) ? bot_get_current_entry_id_csv(filename) : previous->id + 1; + + entry = create_bot_expense_entry(previous, id, cmd, value, user, notes); + if(entry == NULL) { + return -1; + } + + /* if no ID, empty file, initialize with headers before writing entry */ + result = write_bot_expense_entry(filename, entry, (id == 0)); + + if(result > 0) { /* write successful */ + bot_set_last_command(cmd); + bot_set_last_expense(entry); + } + return result; +} diff --git a/src/expenses/csv.h b/src/expenses/csv.h new file mode 100644 index 0000000..b01dbe7 --- /dev/null +++ b/src/expenses/csv.h @@ -0,0 +1,31 @@ +/* Routines to write a CSV expenses file + */ +#include +#include +#include +#include +#include "types.h" + +/* count the lines of the file to get the last ID */ +int bot_get_current_entry_id_csv +( + const GString* filename +); + +/* returns the number of bytes written, following the convention of fprintf */ +int write_bot_expense_entry +( + const GString *filename, + BotExpenseEntry *entry, + bool needsHeaders +); + +/* complete routine to write a message to CSV file */ +int write_update_to_csv +( + const GString *filename, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +); diff --git a/src/expenses/grammar.y b/src/expenses/grammar.y new file mode 100644 index 0000000..7670265 --- /dev/null +++ b/src/expenses/grammar.y @@ -0,0 +1,117 @@ +%{ +#include +#include /* For GString */ +#include "../../src/expenses/types.h" +#include "../../src/expenses/csv.h" + +/* suppress warning about missing declarations */ +int yylex(); +int yyparse(); + +void yyerror(const char *str) +{ + fprintf(stderr,"error: %s\n",str); +} + +int yywrap() +{ + return 1; +} + +%} + +%code requires { +#include "../../src/expenses/types.h" +#include "../../src/expenses/csv.h" + +/* Parse a string with Flex+Bison instead of a file / stdout */ +int grammar_parse_string(const GString *buf); + +/* start a SIGALARM routine */ +int start_alarm_reminder(unsigned int minutes); +} + +%define parse.lac full +%define parse.error verbose + +%union { + int res; /* return value of C procedure */ + double val; /* For returning numbers. */ + BotCommand cmd; + BotReportTimespan tsp; + GString *str; +} + +%token TOK_INT +%token TOK_DOUBLE +%token TOK_CMD_IN TOK_CMD_OUT TOK_CMD_DEL TOK_CMD_REPORT TOK_CMD_REMINDER +%token TOK_TS_DAILY TOK_TS_WEEKLY TOK_TS_MONTHLY TOK_TS_YEARLY +%token TOK_USER /* string for user */ +%token TOK_STR /* string for notes */ +%type update; +%type value; +%type ts; +%type msg; + +%% +input: /* empty */ + | input line +; + +line: '\n' + | msg + | error '\n' { yyerrok; } +; + +msg: update value TOK_USER TOK_STR + { + int res = write_update_to_csv(bot_get_filename(), $1, $2, $3, $4); + printf("update [%d]: %d %f %s %s\n", res, $1, $2, $3->str, $4->str); $$ = 0; + } + | TOK_CMD_REMINDER TOK_INT + { + int res = start_alarm_reminder($2); + printf("reminder in %d minutes\n", $2); $$ = 0; + } + | TOK_CMD_DEL TOK_INT { printf("delete: %d\n", $2); $$ = 0; } + | TOK_CMD_REPORT ts { printf("report: %d\n", $2); $$ = 0; } + +value: TOK_INT { $$ = (double)$1; } + | TOK_DOUBLE + ; + +update: TOK_CMD_IN + | TOK_CMD_OUT +; + +ts: TOK_TS_DAILY + | TOK_TS_WEEKLY + | TOK_TS_MONTHLY + | TOK_TS_YEARLY +; +%% + +/* Parse a string with Flex+Bison instead of a file / stdout */ +int grammar_parse_string(const GString *buf) { + int res; + yy_scan_string(buf->str); + res = yyparse(); + yylex_destroy(); + return res; +} + +/* start a SIGALARM routine */ +int start_alarm_reminder(unsigned int minutes) +{ + /* in seconds */ + alarm(minutes * 60); + + BotReminder *reminder = malloc(sizeof *reminder); + reminder->minutes = minutes; + reminder->from = g_string_new(NULL); /* to be set by the message handler of the bot */ + + bot_set_last_command(BOT_CMD_REMINDER); + bot_set_last_reminder(reminder); + + return 0; +} diff --git a/src/expenses/lex.l b/src/expenses/lex.l new file mode 100644 index 0000000..8ade3c3 --- /dev/null +++ b/src/expenses/lex.l @@ -0,0 +1,32 @@ +/* expenses.l: Expenses lexical analysis file + * TODO free memory allocated with g_string_new AFTER the parser is done (see grammar.y) + * TODO support italian-style floating point numbers e.g. 12,34 +*/ + +%{ +#include +#include +#include "grammar.tab.h" +%} + +DIGIT [0-9] + +%% + +{DIGIT}+ yylval.res=atoi(yytext); return TOK_INT; +{DIGIT}+"."{DIGIT}* yylval.val=atof(yytext); return TOK_DOUBLE; +"-"|out|spesa yylval.cmd=BOT_CMD_OUT; return TOK_CMD_OUT; +"+"|in|ricavo yylval.cmd=BOT_CMD_IN; return TOK_CMD_IN; +del|storno yylval.cmd=BOT_CMD_DELETE; return TOK_CMD_DEL; +reminder|ricordami yylval.cmd=BOT_CMD_REMINDER; return TOK_CMD_REMINDER; +report yylval.cmd=BOT_CMD_REPORT; return TOK_CMD_REPORT; +daily|giorno yylval.tsp=BOT_TS_DAILY; return TOK_TS_DAILY; +weekly|sett|settimana yylval.tsp=BOT_TS_WEEKLY; return TOK_TS_WEEKLY; +monthly|mese yylval.tsp=BOT_TS_MONTHLY; return TOK_TS_MONTHLY; +yearly|anno yylval.tsp=BOT_TS_YEARLY; return TOK_TS_YEARLY; +[a-zA-Z0-9_]+ yylval.str=g_string_new(yytext); return TOK_USER; /* user can't have special characters */ +\"[^\n]+\" yylval.str=g_string_new(yytext); return TOK_STR; /* anything excepts '\n'; needs to be at the bottom of flex rules here */ +[ \t]+ /* ignore whitespace */; +\n /* ignore end of line */; + +%% diff --git a/src/expenses/types.c b/src/expenses/types.c new file mode 100644 index 0000000..b4a29cc --- /dev/null +++ b/src/expenses/types.c @@ -0,0 +1,161 @@ +/* function definition to parse a string instead of a file + */ +#include "types.h" + +extern BotCommand g_LastCommand = BOT_CMD_NONE; +extern BotExpenseEntry *g_LastExpenseEntry = NULL; +extern BotReminder *g_LastReminder = NULL; +extern GString *g_filename = NULL; + +BotCommand bot_get_last_command() +{ + return g_LastCommand; +} +void bot_set_last_command(BotCommand cmd) +{ + g_LastCommand = cmd; +} + +BotExpenseEntry *bot_get_last_expense() +{ + return g_LastExpenseEntry; +} +void bot_set_last_expense(BotExpenseEntry *entry) +{ + if(g_LastExpenseEntry) { + free_bot_expense_entry(g_LastExpenseEntry); + } + g_LastExpenseEntry = entry; +} + +BotReminder *bot_get_last_reminder() +{ + return g_LastReminder; +} +void bot_set_last_reminder(BotReminder *reminder) +{ + if(g_LastReminder) { + free_bot_reminder(g_LastReminder); + } + g_LastReminder = reminder; +} + +GString *bot_get_filename() +{ + return g_filename; +} + +void bot_set_filename(GString *fn) +{ + if(g_filename) { + g_string_free(g_filename, true); + } + g_filename = fn; +} + +/* Create a BotExpenseEntry with data fed from the parser. + * Returns NULL if error getting local time. + */ +BotExpenseEntry *create_bot_expense_entry +( + const BotExpenseEntry *previous, + const unsigned int id, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +) +{ + double signedValue = value; + BotExpenseEntry *entry = malloc(sizeof *entry); // TODO this should be freed once it's written to registry + struct tm *lt = malloc(sizeof *lt); + time_t t = time(NULL); + + /* get the local time */ + localtime_r(&t, lt); + if(lt == NULL) { + fprintf(stderr, "Unable to get localtime.\n"); + return NULL; + } + + if(cmd == BOT_CMD_OUT) { + signedValue = -value; + } + + entry->id = id; + entry->time = *lt; + entry->value = signedValue; + entry->user = user; + entry->notes = notes; + entry->_serialized = false; + + return entry; +} + +/* FP should not be null. This assumes that it has been initialized with an header line as well + * Returns NULL in case 'entry' is uninitialized. + */ +GString *serialize_bot_expense_entry_csv +( + BotExpenseEntry *entry +) +{ + /* will be automatically expanded by g_string_printf */ + GString *tstr = g_string_new(NULL); + GString *result = g_string_new(NULL); + + if(entry == NULL) { + fprintf(stderr, "BotExpenseEntry 'entry' is NULL. Initialize it before calling 'serialize_bot_expense_entry_csv'\n"); + return NULL; + } + + /* Time in ISO 8061 */ + g_string_printf(tstr, + "%d-%02d-%02d %02d:%02d:%02d", + entry->time.tm_year + 1900, + entry->time.tm_mon + 1, + entry->time.tm_mday, + entry->time.tm_hour, + entry->time.tm_min, + entry->time.tm_sec); + + g_string_printf(result, + "%u,%s,%.2f,%s,%s\n", + entry->id, + tstr->str, + entry->value, + entry->user->str, + entry->notes->str + ); + + entry->_serialized = true; + + return result; +} + + +/* To be called after it has been serialized */ +void free_bot_expense_entry +( + BotExpenseEntry *entry +) +{ + if(!entry->_serialized) { + /* TODO could fail here instead */ + fprintf(stderr, "WARNING: freeing an entry that wasn't serialized!\n"); + } + + /* Also free underlying string, we don't need it anymore */ + g_string_free(entry->user, TRUE); + g_string_free(entry->notes, TRUE); + free(entry); +} + +void free_bot_reminder +( + BotReminder *reminder +) +{ + g_string_free(reminder->from, true); + free(reminder); +} diff --git a/src/expenses/types.h b/src/expenses/types.h new file mode 100644 index 0000000..0f66238 --- /dev/null +++ b/src/expenses/types.h @@ -0,0 +1,95 @@ +/* additional types for expenses grammar + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +typedef enum BotCommand { + BOT_CMD_NONE, + BOT_CMD_IN, + BOT_CMD_OUT, + BOT_CMD_DELETE, + BOT_CMD_REPORT, + BOT_CMD_REMINDER +} BotCommand; + +typedef enum BotReportTimespan { + BOT_TS_DAILY, + BOT_TS_WEEKLY, + BOT_TS_MONTHLY, + BOT_TS_YEARLY +} BotReportTimespan; + +typedef struct BotExpenseEntry { + unsigned int id; + struct tm time; /* number of seconds since the epoch, needs `localtime()` + timezone to localize: https://stackoverflow.com/questions/1442116/how-can-i-get-the-date-and-time-values-in-a-c-program */ + double value; /* positive or negative according to in or out */ + bool _serialized; /* utility to check if entry has been serialized before freeing */ + GString *user; + GString *notes; +} BotExpenseEntry; + +typedef struct BotReminder { + GString *from; + unsigned int minutes; +} BotReminder; + +/* Parser definitions */ +int yylex(); +int yyparse(); +int yylex_destroy(); +typedef struct yy_buffer_state * YY_BUFFER_STATE; +YY_BUFFER_STATE yy_scan_string(const char * str); + +/* Create a BotExpenseEntry with data fed from the parser. */ +BotExpenseEntry *create_bot_expense_entry +( + const BotExpenseEntry *previous, + const unsigned int id, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +); + +/* FP should not be null */ +GString *serialize_bot_expense_entry_csv +( + BotExpenseEntry *data +); + +/* To be called after it has been serialized */ +void free_bot_expense_entry +( + BotExpenseEntry *entry +); + +void free_bot_reminder +( + BotReminder *reminder + ); + +/* use this to check the last received command when responding */ +extern BotCommand g_LastCommand; +BotCommand bot_get_last_command(); +void bot_set_last_command(BotCommand cmd); + +/* store the last expense in memory */ +extern BotExpenseEntry *g_LastExpenseEntry; +BotExpenseEntry *bot_get_last_expense(); +void bot_set_last_expense(BotExpenseEntry *entry); + +/* store the last reminder in memory */ +extern BotReminder *g_LastReminder; +BotReminder *bot_get_last_reminder(); +void bot_set_last_reminder(BotReminder *reminder); + +/* store the filename in memory */ +extern GString *g_filename; +GString *bot_get_filename(); +void bot_set_filename(GString *fn); diff --git a/src/grammar/expenses.l b/src/grammar/expenses.l deleted file mode 100644 index 5ee74f0..0000000 --- a/src/grammar/expenses.l +++ /dev/null @@ -1,32 +0,0 @@ -/* expenses.l: Expenses lexical analysis file - * TODO free memory allocated with g_string_new AFTER the parser is done (see grammar.y) - * TODO support italian-style floating point numbers e.g. 12,34 -*/ - -%{ -#include -#include -#include "expenses.tab.h" -%} - -DIGIT [0-9] - -%% - -{DIGIT}+ yylval.res=atoi(yytext); return TOK_INT; -{DIGIT}+"."{DIGIT}* yylval.val=atof(yytext); return TOK_DOUBLE; -"-"|out|spesa yylval.cmd=BOT_CMD_OUT; return TOK_CMD_OUT; -"+"|in|ricavo yylval.cmd=BOT_CMD_IN; return TOK_CMD_IN; -del|storno yylval.cmd=BOT_CMD_DELETE; return TOK_CMD_DEL; -reminder|ricordami yylval.cmd=BOT_CMD_REMINDER; return TOK_CMD_REMINDER; -report yylval.cmd=BOT_CMD_REPORT; return TOK_CMD_REPORT; -daily|giorno yylval.tsp=BOT_TS_DAILY; return TOK_TS_DAILY; -weekly|sett|settimana yylval.tsp=BOT_TS_WEEKLY; return TOK_TS_WEEKLY; -monthly|mese yylval.tsp=BOT_TS_MONTHLY; return TOK_TS_MONTHLY; -yearly|anno yylval.tsp=BOT_TS_YEARLY; return TOK_TS_YEARLY; -[a-zA-Z0-9_]+ yylval.str=g_string_new(yytext); return TOK_USER; /* user can't have special characters */ -\"[^\n]+\" yylval.str=g_string_new(yytext); return TOK_STR; /* anything excepts '\n'; needs to be at the bottom of flex rules here */ -[ \t]+ /* ignore whitespace */; -\n /* ignore end of line */; - -%% diff --git a/src/grammar/expenses.y b/src/grammar/expenses.y deleted file mode 100644 index 2c0123d..0000000 --- a/src/grammar/expenses.y +++ /dev/null @@ -1,96 +0,0 @@ -%{ -#include -#include /* For GString */ -#include "../../src/grammar/types.h" - -/* suppress warning about missing declarations */ -int yylex(); -int yyparse(); - -void yyerror(const char *str) -{ - fprintf(stderr,"error: %s\n",str); -} - -int yywrap() -{ - return 1; -} - -%} - -%code requires { -#include "../../src/grammar/types.h" - -/* Parse a string with Flex+Bison instead of a file / stdout */ -int grammar_parse_string(const GString *buf); -} - -%define parse.lac full -%define parse.error verbose - -%union { - int res; /* return value of C procedure */ - double val; /* For returning numbers. */ - BotCommand cmd; - BotReportTimespan tsp; - GString *str; -} - -%token TOK_INT -%token TOK_DOUBLE -%token TOK_CMD_IN TOK_CMD_OUT TOK_CMD_DEL TOK_CMD_REPORT TOK_CMD_REMINDER -%token TOK_TS_DAILY TOK_TS_WEEKLY TOK_TS_MONTHLY TOK_TS_YEARLY -%token TOK_USER /* string for user */ -%token TOK_STR /* string for notes */ -%type update; -%type value; -%type ts; -%type msg; - -%% -input: /* empty */ - | input line -; - -line: '\n' - | msg - | error '\n' { yyerrok; } -; - -msg: update value TOK_USER TOK_STR - { - int res = write_update_to_csv(bot_get_filename(), $1, $2, $3, $4); - printf("update [%d]: %d %f %s %s\n", res, $1, $2, $3->str, $4->str); $$ = 0; - } - | TOK_CMD_REMINDER TOK_INT - { - int res = start_alarm_reminder($2); - printf("reminder in %d minutes\n", $2); $$ = 0; - } - | TOK_CMD_DEL TOK_INT { printf("delete: %d\n", $2); $$ = 0; } - | TOK_CMD_REPORT ts { printf("report: %d\n", $2); $$ = 0; } - -value: TOK_INT { $$ = (double)$1; } - | TOK_DOUBLE - ; - -update: TOK_CMD_IN - | TOK_CMD_OUT -; - -ts: TOK_TS_DAILY - | TOK_TS_WEEKLY - | TOK_TS_MONTHLY - | TOK_TS_YEARLY -; -%% - -/* Parse a string with Flex+Bison instead of a file / stdout */ -int grammar_parse_string(const GString *buf) { - int res; - yy_scan_string(buf->str); - res = yyparse(); - yylex_destroy(); - return res; -} diff --git a/src/grammar/types.c b/src/grammar/types.c deleted file mode 100644 index 3c24b08..0000000 --- a/src/grammar/types.c +++ /dev/null @@ -1,265 +0,0 @@ -/* function definition to parse a string instead of a file - */ -#include "types.h" - -extern BotCommand g_LastCommand = BOT_CMD_NONE; -extern BotExpenseEntry *g_LastExpenseEntry = NULL; -extern BotReminder *g_LastReminder = NULL; -extern GString *g_filename = NULL; - -BotCommand bot_get_last_command() -{ - return g_LastCommand; -} -void bot_set_last_command(BotCommand cmd) -{ - g_LastCommand = cmd; -} - -BotExpenseEntry *bot_get_last_expense() -{ - return g_LastExpenseEntry; -} -void bot_set_last_expense(BotExpenseEntry *entry) -{ - if(g_LastExpenseEntry) { - free_bot_expense_entry(g_LastExpenseEntry); - } - g_LastExpenseEntry = entry; -} - -BotReminder *bot_get_last_reminder() -{ - return g_LastReminder; -} -void bot_set_last_reminder(BotReminder *reminder) -{ - if(g_LastReminder) { - free_bot_reminder(g_LastReminder); - } - g_LastReminder = reminder; -} - -GString *bot_get_filename() -{ - return g_filename; -} - -void bot_set_filename(GString *fn) -{ - if(g_filename) { - g_string_free(g_filename, true); - } - g_filename = fn; -} - -int bot_get_current_entry_id -( - const GString* filename -) -{ - /* TODO this counts by char, probably more efficient ways exist */ - char c; - FILE *fp; - unsigned int count = 0; - - if((fp = fopen(filename->str, "r"))) { - for (c = getc(fp); c != EOF; c = getc(fp)) { - if (c == '\n') { - count++; - } - } - fclose(fp); - } - - return count; -} - -/* Create a BotExpenseEntry with data fed from the parser. - * Returns NULL if error getting local time. - */ -BotExpenseEntry *create_bot_expense_entry -( - const BotExpenseEntry *previous, - const unsigned int id, - const BotCommand cmd, - const double value, - GString* user, - GString* notes -) -{ - double signedValue = value; - BotExpenseEntry *entry = malloc(sizeof *entry); // TODO this should be freed once it's written to registry - struct tm *lt = malloc(sizeof *lt); - time_t t = time(NULL); - - /* get the local time */ - localtime_r(&t, lt); - if(lt == NULL) { - fprintf(stderr, "Unable to get localtime.\n"); - return NULL; - } - - if(cmd == BOT_CMD_OUT) { - signedValue = -value; - } - - entry->id = id; - entry->time = *lt; - entry->value = signedValue; - entry->user = user; - entry->notes = notes; - entry->_serialized = false; - - return entry; -} - -/* FP should not be null. This assumes that it has been initialized with an header line as well - * Returns NULL in case 'entry' is uninitialized. - */ -GString *serialize_bot_expense_entry_csv -( - BotExpenseEntry *entry -) -{ - /* will be automatically expanded by g_string_printf */ - GString *tstr = g_string_new(NULL); - GString *result = g_string_new(NULL); - - if(entry == NULL) { - fprintf(stderr, "BotExpenseEntry 'entry' is NULL. Initialize it before calling 'serialize_bot_expense_entry_csv'\n"); - return NULL; - } - - /* Time in ISO 8061 */ - g_string_printf(tstr, - "%d-%02d-%02d %02d:%02d:%02d", - entry->time.tm_year + 1900, - entry->time.tm_mon + 1, - entry->time.tm_mday, - entry->time.tm_hour, - entry->time.tm_min, - entry->time.tm_sec); - - g_string_printf(result, - "%u,%s,%.2f,%s,%s\n", - entry->id, - tstr->str, - entry->value, - entry->user->str, - entry->notes->str - ); - - entry->_serialized = true; - - return result; -} - -/* returns the number of bytes written, following the convention of fprintf. - * Returns negative in case of error - */ -int write_bot_expense_entry -( - const GString *filename, - BotExpenseEntry *entry, - bool needsHeaders -) -{ - FILE *fp; - int result; - GString *line = serialize_bot_expense_entry_csv(entry); - - if(line == NULL) { - return -1; - } - - if((fp = fopen(filename->str, "a"))) { - if(needsHeaders) { - fprintf(fp, "ID,Time,Value,User,Notes\n"); - } - - /* line is terminated with '\n' */ - result = fprintf(fp, "%s", line->str); - fclose(fp); - - return result; - fprintf(stderr, "Error opening %s\n", filename->str); - } - - fprintf(stderr, "Error opening %s\n", filename->str); - return -1; -} - -/* whole routine to write an expense to a CSV file */ -int write_update_to_csv -( - const GString *filename, - const BotCommand cmd, - const double value, - GString* user, - GString* notes -) -{ - int result; - unsigned int id; - BotExpenseEntry *entry; - BotExpenseEntry *previous = bot_get_last_expense(); - - id = (previous == NULL) ? bot_get_current_entry_id(filename) : previous->id + 1; - - entry = create_bot_expense_entry(previous, id, cmd, value, user, notes); - if(entry == NULL) { - return -1; - } - - /* if no ID, empty file, initialize with headers before writing entry */ - result = write_bot_expense_entry(filename, entry, (id == 0)); - - if(result > 0) { /* write successful */ - bot_set_last_command(cmd); - bot_set_last_expense(entry); - } - return result; -} - -/* start a SIGALARM routine */ -int start_alarm_reminder(unsigned int minutes) -{ - /* in seconds */ - alarm(minutes * 60); - - BotReminder *reminder = malloc(sizeof *reminder); - reminder->minutes = minutes; - reminder->from = g_string_new(NULL); /* to be set by the message handler of the bot */ - - bot_set_last_command(BOT_CMD_REMINDER); - bot_set_last_reminder(reminder); - - return 0; -} - -/* To be called after it has been serialized */ -void free_bot_expense_entry -( - BotExpenseEntry *entry -) -{ - if(!entry->_serialized) { - /* TODO could fail here instead */ - fprintf(stderr, "WARNING: freeing an entry that wasn't serialized!\n"); - } - - /* Also free underlying string, we don't need it anymore */ - g_string_free(entry->user, TRUE); - g_string_free(entry->notes, TRUE); - free(entry); -} - -void free_bot_reminder -( - BotReminder *reminder -) -{ - g_string_free(reminder->from, true); - free(reminder); -} diff --git a/src/grammar/types.h b/src/grammar/types.h deleted file mode 100644 index fd43932..0000000 --- a/src/grammar/types.h +++ /dev/null @@ -1,125 +0,0 @@ -/* additional types for expenses grammar - */ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -typedef enum BotCommand { - BOT_CMD_NONE, - BOT_CMD_IN, - BOT_CMD_OUT, - BOT_CMD_DELETE, - BOT_CMD_REPORT, - BOT_CMD_REMINDER -} BotCommand; - -typedef enum BotReportTimespan { - BOT_TS_DAILY, - BOT_TS_WEEKLY, - BOT_TS_MONTHLY, - BOT_TS_YEARLY -} BotReportTimespan; - -typedef struct BotExpenseEntry { - unsigned int id; - struct tm time; /* number of seconds since the epoch, needs `localtime()` + timezone to localize: https://stackoverflow.com/questions/1442116/how-can-i-get-the-date-and-time-values-in-a-c-program */ - double value; /* positive or negative according to in or out */ - bool _serialized; /* utility to check if entry has been serialized before freeing */ - GString *user; - GString *notes; -} BotExpenseEntry; - -typedef struct BotReminder { - GString *from; - unsigned int minutes; -} BotReminder; - -/* Parser definitions */ -int yylex(); -int yyparse(); -int yylex_destroy(); -typedef struct yy_buffer_state * YY_BUFFER_STATE; -YY_BUFFER_STATE yy_scan_string(const char * str); - -/* Parse a string with Flex+Bison instead of a file / stdout */ -int grammar_parse_string(const GString *buf); - -/* count the lines of the file to get the last ID */ -int bot_get_current_entry_id -( - const GString* filename -); - -/* complete routine to write a message to CSV file */ -int write_update_to_csv -( - const GString *filename, - const BotCommand cmd, - const double value, - GString* user, - GString* notes -); - -/* Create a BotExpenseEntry with data fed from the parser. */ -BotExpenseEntry *create_bot_expense_entry -( - const BotExpenseEntry *previous, - const unsigned int id, - const BotCommand cmd, - const double value, - GString* user, - GString* notes -); - -/* FP should not be null */ -GString *serialize_bot_expense_entry_csv -( - BotExpenseEntry *data -); - -/* returns the number of bytes written, following the convention of fprintf */ -int write_bot_expense_entry -( - const GString *filename, - BotExpenseEntry *entry, - bool needsHeaders -); - -/* set a SIGALARM to be raised in 'minutes'; also sets the last reminder */ -int start_alarm_reminder(unsigned int minutes); - -/* To be called after it has been serialized */ -void free_bot_expense_entry -( - BotExpenseEntry *entry -); - -void free_bot_reminder -( - BotReminder *reminder - ); - -/* use this to check the last received command when responding */ -extern BotCommand g_LastCommand; -BotCommand bot_get_last_command(); -void bot_set_last_command(BotCommand cmd); - -/* store the last expense in memory */ -extern BotExpenseEntry *g_LastExpenseEntry; -BotExpenseEntry *bot_get_last_expense(); -void bot_set_last_expense(BotExpenseEntry *entry); - -/* store the last reminder in memory */ -extern BotReminder *g_LastReminder; -BotReminder *bot_get_last_reminder(); -void bot_set_last_reminder(BotReminder *reminder); - -/* store the filename in memory */ -extern GString *g_filename; -GString *bot_get_filename(); -void bot_set_filename(GString *fn); diff --git a/src/main.c b/src/main.c index 4da4ae7..584c4dc 100644 --- a/src/main.c +++ b/src/main.c @@ -3,8 +3,8 @@ #include #include -#include "bot/context.h" -#include "grammar/types.h" +#include "core/context.h" +#include "expenses/types.h" int main(int argc, char **argv) {