From dabeb3a9fe2295e9b95a46e72de6e78e78f1befb Mon Sep 17 00:00:00 2001 From: tomsmeding Date: Mon, 22 May 2017 20:59:49 +0200 Subject: server: WIP firebase --- .gitignore | 3 + command.c | 12 ++++ db.c | 62 ++++++++++++++++++++- db.h | 9 +++ firebase-io/.gitignore | 1 + firebase-io/firebase-io.js | 134 ++++++++++++++++++++++++++++++++++++++++++++ firebase-io/package.json | 23 ++++++++ firebase-migration.sh | 14 +++++ firebase.c | 136 +++++++++++++++++++++++++++++++++++++++++++++ firebase.h | 10 ++++ main.c | 3 + schema.sql | 6 ++ 12 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 firebase-io/.gitignore create mode 100755 firebase-io/firebase-io.js create mode 100644 firebase-io/package.json create mode 100755 firebase-migration.sh create mode 100644 firebase.c create mode 100644 firebase.h diff --git a/.gitignore b/.gitignore index 3623175..ee7ea32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ *.o +*.dSYM *.sql.h tomsg_server db.db +*.dylib +firebaseServiceAccountKey.json diff --git a/command.c b/command.c index 0ae7fa3..c5891d5 100644 --- a/command.c +++ b/command.c @@ -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])) diff --git a/db.c b/db.c index 983080f..12230c3 100644 --- a/db.c +++ b/db.c @@ -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;ib.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 (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" < +#include +#include +#include +#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