diff options
| author | Emil | 2023-08-04 09:13:47 -0600 |
|---|---|---|
| committer | Emil | 2023-08-04 09:13:47 -0600 |
| commit | 935243d8b4ea992c50315f0c8fcb300365a5762d (patch) | |
| tree | c22d800773997b7b267d5d6cba5931f22ee2be64 /src | |
| download | emil-probotic-935243d8b4ea992c50315f0c8fcb300365a5762d.tar.xz emil-probotic-935243d8b4ea992c50315f0c8fcb300365a5762d.tar.zst | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/api.c | 230 | ||||
| -rw-r--r-- | src/irc.c | 173 | ||||
| -rw-r--r-- | src/main.c | 106 | ||||
| -rw-r--r-- | src/parse.c | 278 | ||||
| -rw-r--r-- | src/sql_stmt.c | 46 | ||||
| -rw-r--r-- | src/unity.c | 25 |
6 files changed, 858 insertions, 0 deletions
diff --git a/src/api.c b/src/api.c new file mode 100644 index 0000000..09799a5 --- /dev/null +++ b/src/api.c @@ -0,0 +1,230 @@ +#define DBFILE "probotic_data.sqlite" + +#define stmt_prepare(stmt) \ + sqlite3_prepare_v2(connection, stmt ## _template, -1, &stmt, NULL) + +VARDECL char const * db = DBFILE; + +VARDECL sqlite3 * connection = NULL; + +DECL void DBERR(const int l){ + if(l != SQLITE_OK && l != SQLITE_ROW && l != SQLITE_DONE) + { + fprintf(stderr, + "sqlite (%d): %s\n", + sqlite3_errcode(connection), sqlite3_errmsg(connection)); + exit(DB_ERROR); + } +} + +DECL int +api_init(void) +{ + DBERR(sqlite3_open_v2(db, &connection, SQLITE_OPEN_READWRITE, NULL)); + // dont you fucking dare to remove this spacing + DBERR(stmt_prepare(remind_stmt)); + DBERR(stmt_prepare(set_repo_stmt)); + DBERR(stmt_prepare(get_nth_id_stmt)); + DBERR(stmt_prepare(new_assignment_stmt)); + DBERR(stmt_prepare(purge_assignments_stmt)); + DBERR(stmt_prepare(is_no_assignment_stmt)); + return 0; +} + +DECL void +api_rope(void) +{ + DBERR(sqlite3_finalize(remind_stmt)); + DBERR(sqlite3_finalize(set_repo_stmt)); + DBERR(sqlite3_finalize(get_nth_id_stmt)); + DBERR(sqlite3_finalize(new_assignment_stmt)); + DBERR(sqlite3_finalize(purge_assignments_stmt)); + DBERR(sqlite3_finalize(is_no_assignment_stmt)); + sqlite3_close(connection); +} + +DECL void +rope(void) +{ + if (session) + { irc_destroy_session(session); } + api_rope(); +} + +DECL char * +remind(char * who) +{ + char * r; + char * title; + char * desc; + char * repo; + DBERR(sqlite3_reset(remind_stmt)); + DBERR(sqlite3_bind_text(remind_stmt, 1, who, -1, SQLITE_STATIC)); + const int i = sqlite3_step(remind_stmt); + DBERR(i); + if (i == SQLITE_ROW) + { + title = (char *) sqlite3_column_text(remind_stmt, 0); + title = strdup(title); + desc = (char *) sqlite3_column_text(remind_stmt, 1); + if (desc) { desc = strdup(desc); } else { desc = ""; } + repo = (char *) sqlite3_column_text(remind_stmt, 3); + if (repo) { repo = strdup(repo); } else { repo = "<no link available>"; } + asprintf(&r, + IRC_RED "%s: " IRC_YELLOW "%s" IRC_GREEN + " (@" IRC_BLUE "%s" IRC_GREEN ")" IRC_STOP, + title, desc, repo); + } + else + { + r = strdup(IRC_RED "No current assignment." IRC_STOP); + } + return r; +} + +DECL void +set_repo(char const * const who, + char const * const link) +{ + DBERR(sqlite3_reset(set_repo_stmt)); + DBERR(sqlite3_bind_text(set_repo_stmt, 1, link, -1, SQLITE_STATIC)); + DBERR(sqlite3_bind_text(set_repo_stmt, 2, who, -1, SQLITE_STATIC)); + DBERR(sqlite3_step(set_repo_stmt)); +} + +DECL int +rtos(void * data, + int argc, + char** argv, + char** colname +){ + (void) colname; + + char ** r = (char**)data; + + size_t data_len = 0; + for(int i = 0; i < argc; i++) + { + if(argv[i]) + { + data_len += strlen(argv[i]); + } + else + { + /* strlen("NULL") == 4 */ + data_len += 4; + } + /* strlen("|") * 2 == 2 */ + data_len += 2; + } + ++data_len; + + *r = (char *)calloc(data_len, sizeof(char)); + + for(int i = 0; i < argc; i++){ + strcat(*r, "|"); + if(argv[i]){ + strcat(*r, argv[i]); + } + else + { + strcat(*r, "NULL"); + } + } + strcat(*r, "|\n"); + + return 0; +} + +DECL char * +dump() +{ + char* errmsg; + char* r = NULL; + + DBERR(sqlite3_exec(connection, dump_stmt, rtos, &r, &errmsg)); + + return r; +} + +DECL char * +raw(char const * const sql) +{ + char* errmsg; + char *r = NULL; + + sqlite3_exec(connection, sql, rtos, &r, &errmsg); + + if (errmsg){ + free(r); + r = errmsg; + } else { strcat(r, "\00"); } + return r; +} + + +DECL int +get_project_count_callback(void* data, int argc, char** argv, char** colname) +{ + (void)argc; + (void)colname; + int* count = (int*)data; + *count = atoi(argv[0]); + return 0; +} + +DECL int +get_project_count() +{ + int r = 0; + + char const * sql = "SELECT COUNT(*) FROM project;"; + DBERR(sqlite3_exec(connection, sql, get_project_count_callback, &r, NULL)); + + return r; +} + +DECL int +get_nth_id(const int i) +{ + int r; + DBERR(sqlite3_reset(get_nth_id_stmt)); + DBERR(sqlite3_bind_int(get_nth_id_stmt, 1, i)); + DBERR(sqlite3_step(get_nth_id_stmt)); + r = sqlite3_column_int(get_nth_id_stmt, 0); + return r; +} + +DECL void +new_assignment(char const * const who, const int project) +{ + DBERR(sqlite3_reset(new_assignment_stmt)); + DBERR(sqlite3_bind_text(new_assignment_stmt, 1, who, -1, SQLITE_STATIC)); + DBERR(sqlite3_bind_int(new_assignment_stmt, 2, project)); + DBERR(sqlite3_step(new_assignment_stmt)); +} + +DECL void +random_assign(char const * const who) +{ + int i = rand() % get_project_count(); + i = get_nth_id(i); + new_assignment(who, i); +} + +DECL void +purge_assignments(char const * const who) +{ + DBERR(sqlite3_reset(purge_assignments_stmt)); + DBERR(sqlite3_bind_text(purge_assignments_stmt, 1, who, -1, SQLITE_STATIC)); + DBERR(sqlite3_step(purge_assignments_stmt)); +} + +DECL int +is_no_assignment(char const * const who){ + DBERR(sqlite3_reset(is_no_assignment_stmt)); + DBERR(sqlite3_bind_text(is_no_assignment_stmt, 1, who, -1, SQLITE_STATIC)); + const int e = sqlite3_step(is_no_assignment_stmt); + DBERR(e); + return (e == SQLITE_DONE); +} diff --git a/src/irc.c b/src/irc.c new file mode 100644 index 0000000..a9b8ffb --- /dev/null +++ b/src/irc.c @@ -0,0 +1,173 @@ +/* irc.c - IRC interface + + Probotic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3 only as + published by the Free Software Foundation. + + Probotic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License version 3 for more details. + + You should have received a copy of the GNU General Public License + version 3 along with Probotic. + +*/ + +VARDECL char const * help_msg = +IRC_GREEN "!help " IRC_STOP " : This message\n" +IRC_GREEN "!remind " IRC_STOP " : Dump current assignment\n" +IRC_GREEN "!reroll " IRC_STOP " : Rerolls assignment\n" +IRC_GREEN "!set_repo <link>" IRC_STOP " : Sets project repository link\n" +IRC_GREEN "!raw <sql> " IRC_STOP " : Execute raw SQL\n" +IRC_GREEN "!dump " IRC_STOP " : List all possible projects\n" +IRC_GREEN "!request " IRC_STOP " : Request personal project\n" +IRC_GREEN "!remind " IRC_STOP " : Prints your assignment\n"; + +VARDECL char const * fmsg = +"%s\x2C\x20\x79\x6F\x75\x20\x61\x72\x65\x20\x66\x61\x67\x67" + "\x6F\x74\x20\x66\x6F\x72\x20\x74\x68\x61\x74\x20\x6F\x70" + "\x69\x6E\x69\x6F\x6E\x2E"; + +#define PREFIX_COMMAND_CHAR '!' +#define PREFIX_CHANNEL_COMMAND_CHAR '%' + +VARDECL irc_session_t * session; +VARDECL irc_callbacks_t callbacks; + +VARDECL char * current_username = NULL; +/* Do you have any idea how many things this breaks? */ +int stupid_shit = -1; + +#define IRCMSG(msg) irc_cmd_msg(session, creds.channel, msg) + +DECL char * +get_username(const char * origin) +{ + const char USERNAME_TERMINATOR = '!'; + int i = 0; + char * r; + while (origin[i] != USERNAME_TERMINATOR) + { i++; } + r = (char *) malloc(i + 1); + strncpy(r, origin, i); + r[i] = '\00'; + return r; +} + +DECL void +ircmsg(const char* fmt, + ...) +{ + if(!strcmp(fmt, "") || fmt == NULL){ return; } + va_list args; + char * fmtdmsg; + char * swp; + + va_start(args, fmt); + if(vasprintf(&fmtdmsg, fmt, args) == -1) + { exit(1); } + + puts(fmtdmsg); + const char* delim = "\n"; + char* data = strtok(fmtdmsg, delim); + do{ + swp = irc_color_convert_to_mirc(data); + IRCMSG(swp); + free(swp); + }while((data = strtok(NULL, delim), data)); + + free(fmtdmsg); + va_end(args); +} + +DECL void +event_connect(irc_session_t * session, + const char * event, + const char * origin, + const char ** params, + unsigned int count) +{ + (void) event; + (void) origin; + (void) params; + (void) count; + /* msg ChanServ IDENTIFY? */ + irc_cmd_join(session, creds.channel, 0); + if(is_no_assignment(creds.channel)){ + ircmsg(IRC_RED "No assignment for this channel. Finding a new..." IRC_STOP); + random_assign(creds.channel); + } + ircmsg(remind(creds.channel)); +} + +DECL void +event_channel(irc_session_t * session, + char const * event, + char const * origin, + char const ** params, + unsigned int count) +{ + /* char const * channel = params[0]; */ + char const * message = params[1]; + (void) session; + (void) event; + (void) origin; + /* (void) channel; */ + (void) message; + (void) count; + /* parses the command */ + switch(*message){ + case PREFIX_CHANNEL_COMMAND_CHAR: + current_username = strdup(creds.channel); + break; + case PREFIX_COMMAND_CHAR: + current_username = get_username(origin); + break; + } + if(!current_username || *(message+1) == '\00'){ return; } + --stupid_shit; + if(stupid_shit == 0) + { ircmsg(fmsg, current_username); } + parse_command(message+1); + free(current_username); + current_username = NULL; +} + +DECL int +init(void) +{ + srand(time(NULL)); + if(api_init()) + { ERR(DB_ERROR, "Error initializing database."); } + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.event_connect = event_connect; + callbacks.event_channel = event_channel; + session = irc_create_session(&callbacks); + if (!session) + { + ERRMSG("Error creating IRC session"); + goto fail; + } + assert(creds.username != NULL); + assert(creds.server != NULL); + irc_connect(session, + creds.server, creds.port, creds.password, + creds.username, creds.username, creds.username); + creds_free_password(); + atexit(rope); + return 0; +fail: + creds_free_password(); + return 1; +} + +DECL int +loop(void) +{ + /* We should figure out how the failure happens so we can tell the user that. */ + if (irc_run(session) != 0) + { ERR(1, "Error running IRC session\nPossible issue: bad URL," + " no network connection, bad port, refused connection."); } + return 0; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..fe0ff96 --- /dev/null +++ b/src/main.c @@ -0,0 +1,106 @@ +/* main.c + + Probotic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3 only as + published by the Free Software Foundation. + + Probotic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License version 3 for more details. + + You should have received a copy of the GNU General Public License + version 3 along with Probotic. + +*/ + +#define VERSION_STRING "0.999" + +void +help(void) +{ + ERRMSG(PROGN ": usage\n" + "-server SERVER - Sets server\n" + "-port PORT - Sets port\n" + "-username USERNAME - Sets username\n" + "-password PASSW0RD - Sets password\n" + "-auth FILE - Use auth file"); +} + +void +version(void) +{ + ERRMSG(PROGN ": " VERSION_STRING); +} + +int +main (int argc, + char ** argv) +{ + char const * authfile = NULL; + if (argc > 1) + { + char * arg; + char * buf; + while (++argv, --argc) + { + arg = *argv; + if (*arg == '-') + { + ++arg; + if (strcmp(arg, "version") == 0) + { return 0; } + else if (strcmp(arg, "help") == 0) + { goto help; } + if (argc < 2) + { goto nop; } + if (strcmp(arg, "db") == 0) + { db = argv[1]; } + else if (strcmp(arg, "server") == 0) + { free(creds.server); creds.server = strdup(argv[1]); } + else if (strcmp(arg, "port") == 0) + { creds.port = atoi(argv[1]); } + else if (strcmp(arg, "channel") == 0) + { free(creds.channel); creds.channel = strdup(argv[1]); } + else if (strcmp(arg, "username") == 0) + { free(creds.username); creds.username = strdup(argv[1]); } + else if (strcmp(arg, "password") == 0) + { free(creds.password); creds.password = strdup(argv[1]); } + else if (strcmp(arg, "auth") == 0) + { + authfile = argv[1]; + buf = slurp(authfile); + if (!buf) + { + fprintf(stderr, "file: %s\n", authfile); + PERROR(1); + } + if (parse_pair(buf, strlen(buf))) + { + free(buf); + creds_free_rest(); + ERR(CREDS_ERROR, "Cannot parse creds"); + } + free(buf); + atexit(creds_free_rest); + } + ++argv; --argc; + } + else + { nop: ERRFMT(1, "Oprand without option '%s'", arg); } + } + } +#ifndef NDEBUG + fprintf(stderr, "-- server:'%s:%d' channel:'%s' username:'%s' pass:%s --\n", + creds.server, creds.port, creds.channel, creds.username, + creds.password); +#endif /* NDEBUG */ + + if (init()) + { return 1; } + + return loop(); +help: + help(); + return 1; +} diff --git a/src/parse.c b/src/parse.c new file mode 100644 index 0000000..1e110b6 --- /dev/null +++ b/src/parse.c @@ -0,0 +1,278 @@ +/* parse.c + + Probotic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3 only as + published by the Free Software Foundation. + + Probotic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License version 3 for more details. + + You should have received a copy of the GNU General Public License + version 3 along with Probotic. + +*/ + +#define PARAMS_COUNT 6 + +enum cred_names_map +{ + DATABASE, + USERNAME, + PASSWORD, + CHANNEL, + SERVER, + PORT +}; + +VARDECL char const * cred_names[] = +{ + "database", + "username", + "password", + "channel", + "server", + "port" +}; + +VARDECL size_t const cred_names_len[] = +{ + 8, + 8, + 8, + 7, + 6, + 4, + 6 +}; + +VARDECL creds_t creds = {0}; + +DECL char ** +str_split(char const * s, char c) +{ + char ** ret = NULL; + size_t i = 0; + + size_t current_token_i = 0; + + size_t token_start_i = 0; + size_t tokens_q = 0; + + /* count tokens */ + for (i = 1; s[i]; ++i) + { + /* end of a token*/ + if (s[i] == c && s[i - 1] != c) + { ++tokens_q; } + } + ++tokens_q; + + ret = (char **)calloc(tokens_q + 1, sizeof(char *)); + if (!ret) + { return ret; } + + for (i = 1; s[i]; ++i) + { + + if ((s[i + 1] == c || !s[i + 1]) && s[i] != c) + { + /* end of a token*/ + ret[current_token_i] = strndup(s + token_start_i, i - token_start_i + 1); + if (!ret[current_token_i]) + { + split_clean(ret); + return NULL; + } + ++current_token_i; + } + else if (s[i] != c && s[i - 1] == c) + { + /* start of a token */ + token_start_i = i; + } + } + + /* Signal that the split array is ended (for iteration purposes) */ + ret[current_token_i + 1] = NULL; + + return ret; +} + +DECL void +split_clean(char ** split) +{ + while (*split) + { + free(*split); + } + free(split); +} + +DECL char * +slurp(char const * fn) +{ + size_t len; + char * b; + FILE * fp = fopen(fn, "r"); + if (fp) + { + fseek(fp, 0, SEEK_END); + len = ftell(fp); + rewind(fp); + b = malloc(len+2); + if (b) + { + fread(b, 1, len, fp); + b[len+1] = '\0'; + } + fclose(fp); + + return b; + } + else + { return NULL; } +} + +DECL void +parse_command(char const * cmd) +{ + size_t i = 0; + char* msgswp = NULL; + /* size_t len = strlen(cmd); */ + /* TODO does not handle commands with leading space, + use custom implemented to-spec isspace implementation */ + while (cmd[i] != '\0' && + cmd[i] != ' ') + { ++i; } + if (cmd[i] == '\0') + { + /* no arguments */ + if (strcmp(cmd, "kill") == 0) + { exit(1); } + if (strcmp(cmd, "remind") == 0) + { + msgswp = remind(current_username); + ircmsg("%s: %s", current_username, msgswp); + } + /* XXX: maybe no? */ + /*else if (strcmp(cmd, "next") == 0) | TODO: implement */ + /* { ircmsg("%s: No future assignments", current_username); } */ + else if (strcmp(cmd, "help") == 0) + { ircmsg(help_msg); } + else if (strcmp(cmd, "magic") == 0) + { stupid_shit = 8 + (rand() % 100); } + else if (strcmp(cmd, "dump") == 0) + { + ircmsg("%s: All projects:", current_username); + msgswp = dump(); + ircmsg(msgswp); + } + else if (strcmp(cmd, "reroll") == 0) + { + ircmsg("%s: Rerolling...", current_username); + purge_assignments(current_username); + random_assign(current_username); + ircmsg(remind(current_username)); + } + } + else + { + /* some arguments */ + char const * const arg = cmd + i + 1; + if (strncmp(cmd, "raw", i) == 0) + { + ircmsg("%s: Executing SQL `%s'.", current_username, arg); + msgswp = raw(arg); + ircmsg(msgswp); + } + else if (strncmp(cmd, "set_repo", i) == 0) + { + ircmsg("%s: Setting project repository...", current_username); + set_repo(creds.channel, arg); + msgswp = remind(creds.channel); + ircmsg("%s: %s", current_username, msgswp); + } + /* XXX: what is this suppose to do? */ + else if (strncmp(cmd, "submit", i) == 0) /* TODO: implement */ + { + ircmsg("%s: Submitting project link '%s' to <random janny>", + current_username, arg); + } + } + free(msgswp); +} + +DECL int +parse_pair(char const * buf, size_t len) +{ + size_t i, f, x; + /* fprintf(stderr, "ENT len:%ld buf:%sEOF\n", len, buf); */ + for (i = 0; buf[i] && + i < len; ++i) + { + if (buf[i] == '=') + { + ++i; + for (f = 0, x = 0; f < PARAMS_COUNT; ++f) + { + /* fprintf(stderr, "x%ld, i%ld, %s\n", x, i, buf); */ + /* X macro for handling this data may be better */ + if (strncmp(buf, cred_names[f], cred_names_len[f]) == 0) + { + /* fprintf(stderr, "f%ld:len%ld:%s\n", f, cred_names_len[f], */ + /* cred_names[f]); fflush(stderr); */ + buf += i; + while (buf[x] != '\0') + { + if (buf[x] == '\n') + { + len -= i; i = 0; break; + } + ++x; + } + switch (f) + { + case DATABASE: db = strndup(buf,x); break; + case USERNAME: creds.username = strndup(buf,x); break; + case PASSWORD: creds.password = strndup(buf,x); break; + case CHANNEL: creds.channel = strndup(buf,x); break; + case SERVER: creds.server = strndup(buf,x); break; + case PORT: creds.port = atoi(buf); break; + } + if (x + 2 < len) + { buf += x + 1; } + else + { return 1; } + goto next; + } + } + } + next:; + } + return 0; +} + +DECL int +is_admin(char const * user) +{ + /* No Gods or Kings, Only size_t */ + return 1; +} + +void +creds_free_password(void) +{ + FULL_FREE(creds.password); +} + +void +creds_free_rest(void) +{ + FULL_FREE(creds.username); + /* FULL_FREE(creds.password); */ + FULL_FREE(creds.channel); + FULL_FREE(creds.server); + FREE(creds.admins); +} diff --git a/src/sql_stmt.c b/src/sql_stmt.c new file mode 100644 index 0000000..fa4a293 --- /dev/null +++ b/src/sql_stmt.c @@ -0,0 +1,46 @@ +VARDECL sqlite3_stmt * remind_stmt; +VARDECL char const remind_stmt_template[] = + "SELECT " + "title," + "body," + "difficulty," + "repo_link," + "trigger_date," + "started DATE," + "span" + " FROM assignment INNER JOIN project on assignment.project = project.rowid " + "WHERE who = ?;"; + +VARDECL sqlite3_stmt * set_repo_stmt; +VARDECL char const set_repo_stmt_template[] = + "UPDATE assignment " + "SET " + "repo_link = ? " + "WHERE who = ?;"; + +VARDECL char const dump_stmt[] = + "SELECT * FROM project;"; + +VARDECL sqlite3_stmt * get_nth_id_stmt; +VARDECL char const get_nth_id_stmt_template[] = + "SELECT rowid " + "FROM project " + "LIMIT 1 " + "OFFSET ?;"; + +VARDECL sqlite3_stmt * new_assignment_stmt; +VARDECL char const new_assignment_stmt_template[] = + "INSERT INTO assignment " + "(who, project)" + " VALUES " + "(?, ?);"; + +VARDECL sqlite3_stmt * purge_assignments_stmt; +VARDECL char const purge_assignments_stmt_template[] = + "DELETE FROM assignment " + "WHERE who = ?;"; + +VARDECL sqlite3_stmt* is_no_assignment_stmt; +VARDECL const char is_no_assignment_stmt_template[] = + "SELECT * FROM assignment " + "WHERE who = ?;" ; diff --git a/src/unity.c b/src/unity.c new file mode 100644 index 0000000..048d8fe --- /dev/null +++ b/src/unity.c @@ -0,0 +1,25 @@ +#define DECL static +#define VARDECL static + +#include <assert.h> +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <time.h> +#include <stdlib.h> + +#include <sqlite3.h> + +#include "api.h" +#include "error.h" +#include "free.h" +#include "help.h" +#include "irc.h" +#include "irccolors.h" +#include "parse.h" + +#include "sql_stmt.c" +#include "irc.c" +#include "parse.c" +#include "api.c" +#include "main.c" |
