diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | command.c | 12 | ||||
-rw-r--r-- | db.c | 62 | ||||
-rw-r--r-- | db.h | 9 | ||||
-rw-r--r-- | firebase-io/.gitignore | 1 | ||||
-rwxr-xr-x | firebase-io/firebase-io.js | 134 | ||||
-rw-r--r-- | firebase-io/package.json | 23 | ||||
-rwxr-xr-x | firebase-migration.sh | 14 | ||||
-rw-r--r-- | firebase.c | 136 | ||||
-rw-r--r-- | firebase.h | 10 | ||||
-rw-r--r-- | main.c | 3 | ||||
-rw-r--r-- | schema.sql | 6 |
12 files changed, 412 insertions, 1 deletions
@@ -1,4 +1,7 @@ *.o +*.dSYM *.sql.h tomsg_server db.db +*.dylib +firebaseServiceAccountKey.json @@ -9,6 +9,7 @@ #include "db.h" #include "event.h" #include "net.h" +#include "firebase.h" #include "user_data.h" #include "util.h" @@ -213,6 +214,7 @@ static bool cmd_send(struct conn_data *data,const char *tag,const char **args){ roomname,username,timestamp,message); event_emit_message(timestamp,message,username,roomname); + firebase_send_message(roomname,roomid,username,message); free(username); struct db_user_list members=db_list_members(roomid); @@ -295,6 +297,15 @@ static bool cmd_is_online(struct conn_data *data,const char *tag,const char **ar return net_send_number(data->fd,tag,nfds); } +static bool cmd_firebase_token(struct conn_data *data,const char *tag,const char **args){ + if(data->userid==-1){ + net_send_error(data->fd,tag,"Not logged in"); + return false; + } + db_add_token(data->userid,args[0]); + return net_send_ok(data->fd,tag); +} + struct cmd_info{ const char *cmdname; @@ -315,6 +326,7 @@ static const struct cmd_info commands[]={ {"history",2,false,cmd_history}, {"ping",0,false,cmd_ping}, {"is_online",1,false,cmd_is_online}, + {"firebase_token",1,false,cmd_firebase_token}, }; #define NCOMMANDS (sizeof(commands)/sizeof(commands[0])) @@ -10,7 +10,7 @@ #define SQLITE(func,...) do{if(sqlite3_##func(__VA_ARGS__)!=SQLITE_OK){die_sqlite("sqlite3_" #func);}}while(0) -sqlite3 *database; +sqlite3 *database=NULL; __attribute__((noreturn)) static void die_sqlite(const char *func){ @@ -19,6 +19,8 @@ static void die_sqlite(const char *func){ void db_init(void){ + SQLITE(config,SQLITE_CONFIG_SERIALIZED); + SQLITE(initialize); SQLITE(open_v2,"db.db",&database,SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE,NULL); char *str=malloc(schema_sql_len+1,char); memcpy(str,schema_sql,schema_sql_len); @@ -29,6 +31,8 @@ void db_init(void){ void db_close(void){ sqlite3_close(database); + SQLITE(shutdown); + database=NULL; } @@ -229,6 +233,54 @@ i64 db_find_user(const char *name){ return userid; } +struct db_strings_list db_user_tokens(i64 userid){ + sqlite3_stmt *stmt; + SQLITE(prepare_v2,database,"select token from Firebase where user = ?",-1,&stmt,NULL); + SQLITE(bind_int64,stmt,1,userid); + + struct db_strings_list sl; + i64 cap=4; + sl.count=0; + sl.list=malloc(cap,char*); + + int ret; + while((ret=sqlite3_step(stmt))==SQLITE_ROW){ + if(sl.count==cap){ + cap*=2; + sl.list=realloc(sl.list,cap,char*); + } + sl.list[sl.count]=strdup((const char*)sqlite3_column_text(stmt,0)); + sl.count++; + } + + if(ret!=SQLITE_DONE)die_sqlite("sqlite3_step"); + SQLITE(finalize,stmt); + + return sl; +} + +bool db_add_token(i64 userid,const char *token){ + assert(userid!=-1); + sqlite3_stmt *stmt; + SQLITE(prepare_v2,database,"insert into Firebase (user, token) values (?, ?)",-1,&stmt,NULL); + SQLITE(bind_int64,stmt,1,userid); + SQLITE(bind_text,stmt,2,token,-1,SQLITE_STATIC); + bool success=sqlite3_step(stmt)==SQLITE_DONE; + SQLITE(finalize,stmt); + return success; +} + +bool db_delete_token(i64 userid,const char *token){ + assert(userid!=-1); + sqlite3_stmt *stmt; + SQLITE(prepare_v2,database,"delete from Firebase where user = ? and token = ?",-1,&stmt,NULL); + SQLITE(bind_int64,stmt,1,userid); + SQLITE(bind_text,stmt,2,token,-1,SQLITE_STATIC); + bool success=sqlite3_step(stmt)==SQLITE_DONE; + SQLITE(finalize,stmt); + return success; +} + void db_create_message(i64 roomid,i64 userid,i64 timestamp,const char *message){ sqlite3_stmt *stmt; @@ -307,3 +359,11 @@ void db_nullify_message_list(struct db_message_list ml){ if(ml.list)free(ml.list); ml.list=NULL; } + +void db_nullify_strings_list(struct db_strings_list sl){ + for(i64 i=0;i<sl.count;i++){ + free(sl.list[i]); + } + if(sl.list)free(sl.list); + sl.list=NULL; +} @@ -28,6 +28,11 @@ struct db_user_list{ struct db_name_id *list; }; +struct db_strings_list{ + i64 count; + char **list; +}; + void db_init(void); void db_close(void); @@ -48,6 +53,9 @@ char* db_get_username(i64 userid); char* db_get_pass(i64 userid); bool db_delete_user(i64 userid); i64 db_find_user(const char *name); // -1 if not found +struct db_strings_list db_user_tokens(i64 userid); +bool db_add_token(i64 userid,const char *token); +bool db_delete_token(i64 userid,const char *token); void db_create_message(i64 roomid,i64 userid,i64 timestamp,const char *message); struct db_message_list db_get_messages(i64 roomid,i64 count); // gets latest `count` messages @@ -56,3 +64,4 @@ void db_nullify_name_id(struct db_name_id ni); void db_nullify_room_list(struct db_room_list rl); void db_nullify_user_list(struct db_user_list ul); void db_nullify_message_list(struct db_message_list ml); +void db_nullify_strings_list(struct db_strings_list sl); diff --git a/firebase-io/.gitignore b/firebase-io/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/firebase-io/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/firebase-io/firebase-io.js b/firebase-io/firebase-io.js new file mode 100755 index 0000000..8a94076 --- /dev/null +++ b/firebase-io/firebase-io.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node +const firebase=require("firebase-admin"); +const util=require("util"); + +firebase.initializeApp({ + credential:firebase.credential.cert(require("./firebaseServiceAccountKey.json")), + databaseURL:"https://tomsg-83196.firebaseio.com", +}); + +const fieldConverters=new Map([ + ["token",(b)=>b.toString()], + ["user",(b)=>b.toString()], + ["room",(b)=>b.toString()], + ["message",(b)=>b.toString()], + ["timestamp",(b)=>b.toString()], // Nanosecond timestamps are too close to 52-bit range + ["online",(b)=>parseInt(b.toString(),10)], +]); + +function typeConvertFields(fields){ + for(const key of fields.keys()){ + const conv=fieldConverters.get(key); + if(conv!=undefined)fields.set(key,conv(fields.get(key))); + } +} + +function readFields(buffer){ + const fields=new Map(); + if(buffer.length==0)return fields; + let cursor=0; + while(true){ + let idx=buffer.indexOf(32,cursor); + if(idx==-1){ + console.error("ERR: Incomplete field in input line"); + return null; + } + const key=String(buffer.slice(cursor,idx)); + cursor=idx+1; + + idx=buffer.indexOf(32,cursor); + if(idx==-1){ + console.error("ERR: Incomplete field in input line"); + return null; + } + const arglen=parseInt(String(buffer.slice(cursor,idx)),10); + if(arglen<0||arglen==null||isNaN(arglen)){ + console.error("ERR: Invalid length in input line"); + return null; + } + cursor=idx+1; + + if(cursor+arglen>buffer.length){ + console.error("ERR: Length larger than remaining line in input line"); + return null; + } + const arg=buffer.slice(cursor,cursor+arglen); + cursor+=arglen; + + fields.set(key,arg); + + if(cursor==buffer.length)break; + if(buffer[cursor]!=32){ + console.error("ERR: No space after argument in input line"); + return null; + } + cursor++; + } + return fields; +} + +function processMessage(type,fields){ + switch(type){ + case "message": + const user=fields.get("user"); + const token=fields.get("token"); + const payload={ + notification: { + title: user+" ("+fields.get("room")+")", + body: fields.get("message"), + } + }; + firebase.messaging().sendToDevice(token,payload) + .then((response)=>{ + const result=response.results[0]; + const realToken=result.canonicalRegistrationToken; + if(result.error){ + console.error("JS: Send error:",result.error); + } else if(realToken&&realToken!=token){ + console.log("delete_token "+user+" "+token); + console.log("add_token "+user+" "+realToken); + } + }) + .catch((err)=>{ + console.error("JS: Early send error:",err); + }); + break; + + default: + console.error("JS: Unknown type '"+type+"'"); + } +} + +function handleInputLine(buffer){ + let idx=buffer.indexOf(32); + if(idx==-1){ + console.error("ERR: No space in input line"); + return; + } + const type=String(buffer.slice(0,idx)); + const fields=readFields(buffer.slice(idx+1)); + if(fields==null)return; + typeConvertFields(fields); + processMessage(type,fields); +} + + +{ + let buffer=null; + process.stdin.on("data",(data)=>{ + const prevlen=buffer==null?0:buffer.length; + if(buffer)buffer=Buffer.concat([buffer,data]); + else buffer=data; + let cursor=0; + let lfidx=buffer.indexOf(10,prevlen); + while(lfidx!=-1){ + handleInputLine(buffer.slice(cursor,lfidx)); + cursor=lfidx+1; + lfidx=buffer.indexOf(10,cursor); + } + if(cursor>=buffer.length)buffer=null; + else buffer=Buffer.from(buffer.slice(cursor)); + }); +} + +console.error("Firebase js plugin loaded!"); diff --git a/firebase-io/package.json b/firebase-io/package.json new file mode 100644 index 0000000..df6b578 --- /dev/null +++ b/firebase-io/package.json @@ -0,0 +1,23 @@ +{ + "name": "tomsg-firebase", + "version": "0.1.0", + "description": "Javascript part of firebase plugin to tomsg-server", + "main": "firebase-io.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.tomsmeding.com/tomsg" + }, + "keywords": [ + "tomsg", + "chat", + "firebase" + ], + "author": "Tom Smeding <tom.smeding@gmail.com> (https://tomsmeding.com)", + "license": "MIT", + "dependencies": { + "firebase-admin": "^4.2.1" + } +} diff --git a/firebase-migration.sh b/firebase-migration.sh new file mode 100755 index 0000000..ad2f121 --- /dev/null +++ b/firebase-migration.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +dbname="$1" +if test $# -ne 1 -o -n "$1"; then + echo >&2 "Pass database file as command-line argument" + exit 1 +fi + +sqlite3 "$dbname" <<EOF +create table Firebase ( + user integer, + token text +); +create index firebase_user_index on Firebase(user); +EOF diff --git a/firebase.c b/firebase.c new file mode 100644 index 0000000..780b973 --- /dev/null +++ b/firebase.c @@ -0,0 +1,136 @@ +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <assert.h> +#include "db.h" +#include "firebase.h" + + +#define JS_PLUGIN_PATH "firebase-io/firebase-io.js" + +FILE *js_write=NULL; + + +static void write_field(FILE *f,const char *key,const char *value){ + fprintf(f," %s %zu %s",key,strlen(value),value); +} + +static void token_send(const char *token,const char *room,const char *user,const char *msg){ + fprintf(js_write,"message"); + write_field(js_write,"token",token); + write_field(js_write,"room",room); + write_field(js_write,"user",user); + write_field(js_write,"message",msg); + fprintf(js_write,"\n"); + fflush(js_write); +} + +static void script_output_listener(FILE *js_read){ + // Do not use js_write here, it's NULL + char *line=NULL; + size_t linecap=0; + ssize_t linelen; + while((linelen=getline(&line,&linecap,js_read))>0){ + if(line[linelen-1]!='\n'){ + fprintf(stderr,"firebase thread: unexpected EOF from js script\n"); + return; + } + line[linelen-1]='\0'; + linelen--; + + char *spacep=strchr(line,' '); + if(spacep==NULL)goto invalid_line; + *spacep='\0'; + char *cmd=line; + + if(strcmp(cmd,"add_token")==0||strcmp(cmd,"delete_token")==0){ + char *username=spacep+1; + spacep=strchr(username,' '); + if(spacep==NULL)goto invalid_line; + *spacep='\0'; + char *token=spacep+1; + + i64 userid=db_find_user(username); + if(userid==-1){ + fprintf(stderr,"firebase thread: unknown username '%s'\n",username); + continue; + } + + if(strcmp(cmd,"add_token")==0){ + db_add_token(userid,token); + } else { + db_delete_token(userid,token); + } + } + + continue; + + invalid_line: + fprintf(stderr,"firebase thread: Invalid line form js script: <%s>\n",line); + } +} + + +void firebase_init(void){ + int wrpipe[2],rdpipe[2]; + if(pipe(wrpipe)<0||pipe(rdpipe)<0){ + perror("pipe"); + exit(1); + } + + pid_t pid=fork(); // The JS script + if(pid<0){ + perror("fork"); + exit(1); + } + if(pid==0){ + close(wrpipe[1]); + close(rdpipe[0]); + dup2(wrpipe[0],0); + dup2(rdpipe[1],1); + execl(JS_PLUGIN_PATH,JS_PLUGIN_PATH); + perror("execl"); + exit(1); + } + close(wrpipe[0]); + close(rdpipe[1]); + + pid=fork(); // The script stdout listener + if(pid<0){ + perror("fork"); + exit(1); + } + if(pid==0){ + close(wrpipe[1]); + FILE *js_read=fdopen(rdpipe[0],"r"); + script_output_listener(js_read); + fclose(js_read); + exit(0); + } + + js_write=fdopen(wrpipe[1],"w"); + printf("Started js firebase plugin\n"); +} + +void firebase_stop(void){ + // Script will stop itself on EOF on stdin + fclose(js_write); +} + +void firebase_send_message(const char *room,i64 roomid,const char *user,const char *msg){ + if(roomid==-1)roomid=db_find_room(room); + if(roomid==-1){ + debug("firebase_send_message: Cannot find roomid for room '%s'",room); + return; + } + + struct db_user_list members=db_list_members(roomid); + for(i64 i=0;i<members.count;i++){ + struct db_strings_list tokens=db_user_tokens(members.list[i].id); + for(i64 j=0;j<tokens.count;j++){ + token_send(tokens.list[j],room,user,msg); + } + db_nullify_strings_list(tokens); + } + db_nullify_user_list(members); +} diff --git a/firebase.h b/firebase.h new file mode 100644 index 0000000..8a091d4 --- /dev/null +++ b/firebase.h @@ -0,0 +1,10 @@ +#pragma once + +#include "global.h" + + +void firebase_init(void); +void firebase_stop(void); + +// roomid and room should refer to the same thing; pass roomid==-1 if unknown +void firebase_send_message(const char *room,i64 roomid,const char *user,const char *msg); @@ -12,6 +12,7 @@ #include "conn_data.h" #include "db.h" #include "event.h" +#include "firebase.h" #include "net.h" #include "plugin.h" #include "runloop.h" @@ -157,11 +158,13 @@ int main(int argc,char **argv){ if(argc<=1)printf("Loaded no plugins\n"); db_init(); + firebase_init(); int sock=create_server_socket(); printf("Listening on port %d\n",PORT); runloop_set_timeout(60*1000000,timeout_callback); runloop_add_fd(sock,server_socket_callback,false); runloop_run(); printf("Shutting down because runloop stopped\n"); + firebase_stop(); db_close(); } @@ -31,3 +31,9 @@ create table Messages ( foreign key(user) references Users(id) on delete set null ); create index messages_time_index on Messages(time); + +create table Firebase ( + user integer, + token text +); +create index firebase_user_index on Firebase(user); |