aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEmil2023-08-04 09:13:47 -0600
committerEmil2023-08-04 09:13:47 -0600
commit935243d8b4ea992c50315f0c8fcb300365a5762d (patch)
treec22d800773997b7b267d5d6cba5931f22ee2be64 /src
downloademil-probotic-master.tar.xz
emil-probotic-master.tar.zst
Initial commitHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/api.c230
-rw-r--r--src/irc.c173
-rw-r--r--src/main.c106
-rw-r--r--src/parse.c278
-rw-r--r--src/sql_stmt.c46
-rw-r--r--src/unity.c25
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"