aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--command.c12
-rw-r--r--db.c62
-rw-r--r--db.h9
-rw-r--r--firebase-io/.gitignore1
-rwxr-xr-xfirebase-io/firebase-io.js134
-rw-r--r--firebase-io/package.json23
-rwxr-xr-xfirebase-migration.sh14
-rw-r--r--firebase.c136
-rw-r--r--firebase.h10
-rw-r--r--main.c3
-rw-r--r--schema.sql6
12 files changed, 412 insertions, 1 deletions
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;i<sl.count;i++){
+ free(sl.list[i]);
+ }
+ if(sl.list)free(sl.list);
+ sl.list=NULL;
+}
diff --git a/db.h b/db.h
index 2377ec2..c8b73f1 100644
--- a/db.h
+++ b/db.h
@@ -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);
diff --git a/main.c b/main.c
index 43a24d2..891e19f 100644
--- a/main.c
+++ b/main.c
@@ -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();
}
diff --git a/schema.sql b/schema.sql
index 009308e..6291102 100644
--- a/schema.sql
+++ b/schema.sql
@@ -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);