diff options
-rw-r--r-- | command.c | 82 | ||||
-rw-r--r-- | command.h | 4 | ||||
-rw-r--r-- | db.c | 63 | ||||
-rw-r--r-- | db.h | 5 | ||||
-rw-r--r-- | event.c | 3 | ||||
-rw-r--r-- | event.h | 3 | ||||
-rwxr-xr-x | migrate_1_to_2.sh | 39 | ||||
-rw-r--r-- | protocol.md | 43 | ||||
-rw-r--r-- | schema.sql | 6 | ||||
-rw-r--r-- | ssh/client.c | 37 | ||||
-rw-r--r-- | ssh/tomsg_clientlib.c | 41 | ||||
-rw-r--r-- | ssh/tomsg_clientlib.h | 13 | ||||
-rw-r--r-- | webclient/client.html | 123 | ||||
-rw-r--r-- | weechat/net.c | 40 | ||||
-rw-r--r-- | weechat/net.h | 6 | ||||
-rw-r--r-- | weechat/tomsg.c | 298 |
16 files changed, 718 insertions, 88 deletions
@@ -232,8 +232,18 @@ static struct cmd_retval cmd_send(struct conn_data *data,const char *tag,const c return RET_OK; } userdata_mark_active(data->userid,data->fd,true); + const char *roomname=args[0]; - const char *message=args[1]; + + i64 replyid; + if(!parse_i64(args[1],&replyid)){ + debug("Connection fd=%d sent an invalid number for 'send': '%s'", + data->fd,args[1]); + return RET_CLOSE(true); + } + + const char *message=args[2]; + if(strlen(message)>MAX_MESSAGE_LEN){ net_send_error(data->fd,tag,"Message too long"); return RET_OK; @@ -247,17 +257,26 @@ static struct cmd_retval cmd_send(struct conn_data *data,const char *tag,const c net_send_error(data->fd,tag,"Not in that room"); return RET_OK; } + if(replyid>=0){ + struct db_message msg=db_get_message(replyid); + if(msg.msgid==-1){ + net_send_error(data->fd,tag,"Replied-to message not found"); + return RET_OK; + } + db_nullify_message(msg); + } i64 timestamp=make_timestamp(); - i64 msgid=db_create_message(roomid,data->userid,make_timestamp(),message); + i64 msgid=db_create_message(roomid,data->userid,make_timestamp(),replyid,message); bool closed=net_send_number(data->fd,tag,msgid); char *pushbuf=NULL; char *username=db_get_username(data->userid); - i64 pushbuflen=asprintf(&pushbuf,"_push message %s %s %" PRIi64 " %" PRIi64 " %s\n", - roomname,username,timestamp,msgid,message); + i64 pushbuflen=asprintf(&pushbuf, + "_push message %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", + roomname,username,timestamp,msgid,replyid,message); - event_emit_message(timestamp,message,username,roomname); + event_emit_message(timestamp,message,username,roomname,replyid); firebase_send_message(roomname,roomid,username,message); free(username); @@ -317,9 +336,9 @@ static struct cmd_retval history_cmd_helper( for(i64 i=ml.count-1;i>=0;i--){ char *username=db_get_username(ml.list[i].userid); - len=asprintf(&buf,"%s history_message %" PRIi64 " %s %s %" PRIi64 " %" PRIi64 " %s\n", + len=asprintf(&buf,"%s history_message %" PRIi64 " %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", tag,ml.count-1-i,roomname,username,ml.list[i].timestamp, - ml.list[i].msgid,ml.list[i].message); + ml.list[i].msgid,ml.list[i].replyid,ml.list[i].message); free(username); closed=net_send_raw_text(data->fd,buf,len); free(buf); @@ -346,6 +365,48 @@ static struct cmd_retval cmd_history_before(struct conn_data *data,const char *t return history_cmd_helper(data,tag,args,"history_before",beforeid); } +static struct cmd_retval cmd_get_message(struct conn_data *data, const char *tag, const char **args) { + i64 msgid; + if (!parse_i64(args[0], &msgid)) { + debug("Connection fd=%d sent an invalid number for 'get_message': '%s'", + data->fd, args[0]); + return RET_CLOSE(true); + } + + if (data->userid == -1) { + net_send_error(data->fd, tag, "Not logged in"); + return RET_OK; + } + + struct db_message msg = db_get_message(msgid); + if (msg.msgid == -1) { + net_send_error(data->fd, tag, "Message not found"); + return RET_OK; + } + + if (!db_is_member(msg.roomid, data->userid)) { + db_nullify_message(msg); + // Don't disclose that the message actually exists elsewhere + net_send_error(data->fd, tag, "Message not found"); + return RET_OK; + } + + char *roomname = db_get_roomname(msg.roomid); + char *username = db_get_username(msg.userid); + + char *buf = NULL; + i64 len = asprintf(&buf, "%s message %s %s %" PRIi64 " %" PRIi64 " %" PRIi64 " %s\n", + tag, roomname, username, msg.timestamp, msg.msgid, msg.replyid, msg.message); + free(roomname); + free(username); + + bool closed = net_send_raw_text(data->fd, buf, len); + free(buf); + + db_nullify_message(msg); + return RET_CLOSE(closed); +} + static struct cmd_retval cmd_ping(struct conn_data *data,const char *tag,const char **args){ (void)args; return RET_CLOSE(net_send_pong(data->fd,tag)); @@ -404,8 +465,8 @@ struct cmd_info{ // Use CommandHash.hs to re-generate this perfect hash function for a different // list of commands. -#define COMMAND_HASH_MODULUS 31 -#define COMMAND_HASH(cmd0, len) ((cmd0 + 6 * len) % COMMAND_HASH_MODULUS) +#define COMMAND_HASH_MODULUS 34 +#define COMMAND_HASH(cmd0, len) ((4 * cmd0 + 7 * len) % COMMAND_HASH_MODULUS) #define COMMAND_ENTRY(cmd0, cmd, nargs, longlast, handler) \ [COMMAND_HASH(cmd0, strlen(cmd))] = {cmd, nargs, longlast, handler} @@ -421,9 +482,10 @@ static const struct cmd_info commands[COMMAND_HASH_MODULUS] = { COMMAND_ENTRY('l', "list_members", 1, false, cmd_list_members), COMMAND_ENTRY('c', "create_room", 0, false, cmd_create_room), COMMAND_ENTRY('i', "invite", 2, false, cmd_invite), - COMMAND_ENTRY('s', "send", 2, true, cmd_send), + COMMAND_ENTRY('s', "send", 3, true, cmd_send), COMMAND_ENTRY('h', "history", 2, false, cmd_history), COMMAND_ENTRY('h', "history_before", 3, false, cmd_history_before), + COMMAND_ENTRY('g', "get_message", 1, false, cmd_get_message), COMMAND_ENTRY('p', "ping", 0, false, cmd_ping), COMMAND_ENTRY('i', "is_online", 1, false, cmd_is_online), COMMAND_ENTRY('f', "firebase_token", 1, false, cmd_firebase_token), @@ -4,8 +4,8 @@ #include "conn_data.h" -#define PROTOCOL_VERSION 1 -#define MIN_SUPPORTED_PROTOCOL_VERSION 1 +#define PROTOCOL_VERSION 2 +#define MIN_SUPPORTED_PROTOCOL_VERSION 2 // Returns true if socket should be closed. @@ -10,7 +10,8 @@ #define SQLITE(func,...) do{if(sqlite3_##func(__VA_ARGS__)!=SQLITE_OK){die_sqlite("sqlite3_" #func);}}while(0) -#define DATABASE_VERSION 1 +#define DATABASE_VERSION 2 + #define PASSHASH_OPSLIMIT 3 #define PASSHASH_MEMLIMIT crypto_pwhash_MEMLIMIT_INTERACTIVE @@ -381,16 +382,18 @@ bool db_delete_token(i64 userid,const char *token){ } -i64 db_create_message(i64 roomid,i64 userid,i64 timestamp,const char *message){ +i64 db_create_message(i64 roomid,i64 userid,i64 timestamp,i64 replyid,const char *message){ sqlite3_stmt *stmt; SQLITE(prepare_v2,database, - "insert into Messages (room, user, time, message) " - "values (?, ?, ?, ?)" + "insert into Messages (room, user, time, reply, message) " + "values (?, ?, ?, ?, ?)" ,-1,&stmt,NULL); SQLITE(bind_int64,stmt,1,roomid); SQLITE(bind_int64,stmt,2,userid); SQLITE(bind_int64,stmt,3,timestamp); - SQLITE(bind_blob,stmt,4,message,strlen(message),SQLITE_STATIC); + if(replyid>=0)SQLITE(bind_int64,stmt,4,replyid); + else SQLITE(bind_null,stmt,4); + SQLITE(bind_blob,stmt,5,message,strlen(message),SQLITE_STATIC); if(sqlite3_step(stmt)!=SQLITE_DONE)die_sqlite("sqlite3_step"); SQLITE(finalize,stmt); @@ -407,7 +410,7 @@ struct db_message_list db_get_messages_before(i64 roomid,i64 count,i64 beforeid) sqlite3_stmt *stmt; if(beforeid<0){ SQLITE(prepare_v2,database, - "select id, user, time, message " + "select id, user, time, reply, message " "from Messages " "where room = ? " "order by time desc " @@ -417,7 +420,7 @@ struct db_message_list db_get_messages_before(i64 roomid,i64 count,i64 beforeid) SQLITE(bind_int64,stmt,2,count); } else { SQLITE(prepare_v2,database, - "select id, user, time, message " + "select id, user, time, reply, message " "from Messages " "where room = ? and time < (select time from Messages where id = ?) " "order by time desc " @@ -442,7 +445,12 @@ struct db_message_list db_get_messages_before(i64 roomid,i64 count,i64 beforeid) ml.list[ml.count].roomid=roomid; ml.list[ml.count].userid=sqlite3_column_int64(stmt,1); ml.list[ml.count].timestamp=sqlite3_column_int64(stmt,2); - ml.list[ml.count].message=strdup((const char*)sqlite3_column_text(stmt,3)); + if(sqlite3_column_type(stmt,3)==SQLITE_INTEGER){ + ml.list[ml.count].replyid=sqlite3_column_int64(stmt,3); + } else { + ml.list[ml.count].replyid=-1; // NULL, not a reply + } + ml.list[ml.count].message=strdup((const char*)sqlite3_column_text(stmt,4)); ml.count++; } @@ -452,6 +460,39 @@ struct db_message_list db_get_messages_before(i64 roomid,i64 count,i64 beforeid) return ml; } +struct db_message db_get_message(i64 msgid) { + sqlite3_stmt *stmt; + SQLITE(prepare_v2, database, + "select room, user, time, reply, message " + "from Messages where id = ?", + -1, &stmt, NULL); + SQLITE(bind_int64, stmt, 1, msgid); + + struct db_message msg; + if (sqlite3_step(stmt) == SQLITE_ROW) { + msg.msgid = msgid; + msg.roomid = sqlite3_column_int64(stmt,0); + msg.userid = sqlite3_column_int64(stmt,1); + msg.timestamp = sqlite3_column_int64(stmt,2); + if (sqlite3_column_type(stmt, 3) == SQLITE_INTEGER) { + msg.replyid = sqlite3_column_int64(stmt, 3); + } else { + msg.replyid = -1; // NULL, not a reply + } + msg.message = strdup((const char*)sqlite3_column_text(stmt, 4)); + } else { + msg.msgid = -1; + msg.roomid = -1; + msg.userid = -1; + msg.timestamp = -1; + msg.replyid = -1; + msg.message = NULL; + } + + SQLITE(finalize, stmt); + return msg; +} + void db_nullify_name_id(struct db_name_id ni){ if(ni.name)free(ni.name); @@ -473,9 +514,13 @@ void db_nullify_user_list(struct db_user_list ul){ ul.list=NULL; } +void db_nullify_message(struct db_message msg){ + if(msg.message)free(msg.message); +} + void db_nullify_message_list(struct db_message_list ml){ for(i64 i=0;i<ml.count;i++){ - free(ml.list[i].message); + db_nullify_message(ml.list[i]); } if(ml.list)free(ml.list); ml.list=NULL; @@ -16,6 +16,7 @@ struct db_room_list{ struct db_message{ i64 msgid; i64 roomid,userid,timestamp; + i64 replyid; // message this is a reply to, or -1 if normal message char *message; }; @@ -59,13 +60,15 @@ 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); -i64 db_create_message(i64 roomid,i64 userid,i64 timestamp,const char *message); // returns msgid +i64 db_create_message(i64 roomid,i64 userid,i64 timestamp,i64 replyid,const char *message); // returns msgid struct db_message_list db_get_messages(i64 roomid,i64 count); // gets latest `count` messages in rev. chron. order // if beforeid<0, same as db_get_messages struct db_message_list db_get_messages_before(i64 roomid,i64 count,i64 beforeid); +struct db_message db_get_message(i64 msgid); // returns {msgid=-1} if nonexistent 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(struct db_message ml); void db_nullify_message_list(struct db_message_list ml); void db_nullify_strings_list(struct db_strings_list sl); @@ -67,13 +67,14 @@ void event_emit(const struct event_item *event){ } } -void event_emit_message(i64 timestamp,const char *message,const char *user,const char *room){ +void event_emit_message(i64 timestamp,const char *message,const char *user,const char *room,i64 replyid){ struct event_item *event=event_item_alloc(); event->type=EVENT_MESSAGE; event->timestamp=timestamp; event->message=strdup(message); event->user=strdup(user); event->room=strdup(room); + event->replyid=replyid; event_emit(event); event_item_free(event); } @@ -13,6 +13,7 @@ struct event_item{ enum event_type type; i64 timestamp; // always set char *message,*user,*room; + i64 replyid; i64 num; }; @@ -26,6 +27,6 @@ struct event_item* event_item_alloc(void); void event_item_free(struct event_item *event); void event_emit(const struct event_item *event); -void event_emit_message(i64 timestamp,const char *message,const char *user,const char *room); +void event_emit_message(i64 timestamp,const char *message,const char *user,const char *room,i64 replyid); void event_emit_online(i64 timestamp,const char *user,i64 numonline); void event_emit_join(i64 timestamp,const char *user,const char *room); diff --git a/migrate_1_to_2.sh b/migrate_1_to_2.sh new file mode 100755 index 0000000..02269d1 --- /dev/null +++ b/migrate_1_to_2.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail +INDB=db.db +OUTDB=db_migrated.db + +# This script does not use ALTER TABLE ADD COLUMN because I'm scared by the +# note in the sqlite documentation stating: +# The ALTER TABLE command works by modifying the SQL text of the schema +# stored in the sqlite_master table. No changes are made to table content. +# Because of this, the execution time of the ALTER TABLE command is +# independent of the amount of data in the table. The ALTER TABLE command +# runs as quickly on a table with 10 million rows as it does on a table +# with 1 row. +# I might be mistaken, but that sounds like a performance issue in later usage. +# Therefore, to be sure, we just copy the entire database here. We don't just +# copy and rename the table inside one database to not unnecessarily double the +# disk footprint of the database. + +inversion="$(sqlite3 "$INDB" 'select version from Meta')" + +if [[ $inversion != "1" ]]; then + echo >&2 "$0 migrates from version 1 to version 2, but the database is at version $inversion." + exit 1 +fi + +if [[ -f "$OUTDB" ]]; then + echo >&2 "Output database $OUTDB already exists; remove before running this script" + exit 1 +fi + +sqlite3 "$OUTDB" <schema.sql + +sqlite3 "$INDB" '.dump' | + sed '/^INSERT INTO/!d; s/^INSERT INTO [Mm]essages/&(id,room,user,time,message)/' | + sqlite3 "$OUTDB" + +sqlite3 "$OUTDB" 'update Meta set version = 2' + +echo "Migrated '$INDB' to '$OUTDB'." diff --git a/protocol.md b/protocol.md index ac8d86a..ff6651b 100644 --- a/protocol.md +++ b/protocol.md @@ -1,8 +1,12 @@ -# tomsg protocol (version 1) +# tomsg protocol (version 2) -The underlying transport of the protocol is a plain TCP socket. The individual -messages are all line-based; this means that a single message, both -client->server and server->client, always ends with a newline (ASCII 10) +The underlying transport of the protocol is a plain TCP socket. However, +because private information is communicated, the recommended transport is an +encrypted and authenticated wrapper around a TCP socket; examples are an SSH +connection, or something built on a TLS connection. + +The individual messages are all line-based; this means that a single message, +both client->server and server->client, always ends with a newline (ASCII 10) character. Preliminary definitions: @@ -56,18 +60,26 @@ A _response_ from the server will be in one of the following forms: - Response to the client's `history` command; is followed by exactly `<count>` messages of type `history_message` with the same tag. The messages are returned in chronological order (oldest first). -- `<tag> history_message <index:i64> <roomname:word> <user:word> <timestamp:i64> <msgid:i64> <message:string...>` +- `<tag> history_message <index:i64> <roomname:word> <user:word> <timestamp:i64> <msgid:i64> <replymsgid:i64> <message:string...>` - Part of the response to the client's `history` command. Index 0 is the oldest message; index (`<count>` - 1) (from the `history` response) is the newest message in the fragment requested. Timestamps are microseconds since - the UNIX epoch. The message id for each message is globally unique for the - entire server. + the UNIX epoch. The reply message id is -1 if the message is a normal + message, or nonnegative if it is a reply to the referenced message in the + same room. +- `<tag> message <roomname:word> <user:word> <timestamp:i64> <msgid:i64> <replymsgid:i64> <message:string...>` + - Response to the client's `get_message` command. This contains the + information for a single message, as in `_push message` and the + `history_message` response. A command is identified by its name, which can be found in the list below. Its arguments are zero or more words, except if for the command in question the last argument is of type `string...` (in which case it may contain spaces and ranges until the end of the line). +Note: all messages on the server have an ID that is globally unique on a single +tomsg server. + - `<tag> version <version:word>` - Indicates that the client wishes to speak the protocol with the specified version. For the version of the protocol described in this document, see the @@ -122,14 +134,20 @@ ranges until the end of the line). Also marks the current session as active. - Returns `ok` or `error`. -- `<tag> send <roomname:word> <message:string...>` +- `<tag> send <roomname:word> <replymsgid:i64> <message:string...>` - Sends a message to the given room. All room members receive a `_push message` push message on all their sessions (except the session the `send` message was sent from). + If `<replymsgid>` is -1, this sends a normal message. Otherwise, + `<replymsgid>` must be the id of a message in the given room, in which case + the sent message is a reply to the indicated earlier message. If + `<replymsgid>` is not a message in this room, `error` is returned. + Also marks the current session as active. The returned `number` response contains the message id of the message sent. + This number will be non-negative. - Returns `number` or `error`. - `<tag> history <roomname:word> <number:i64>` - Requests the last `<number>` messages in the given room, if the client is a @@ -142,6 +160,9 @@ ranges until the end of the line). - Same as `history`, except the returned messages are the last `<number>` strictly before the timestamp of the message with id `<msgid>`. - Returns `history`, followed by zero or more `history_message`. +- `<tag> get_message <msgid:i64>` + - Retrieves the message with the given id. + - Returns `message` or `error`. - `<tag> ping` - Asks for a `pong` response. - Returns `pong`. @@ -179,9 +200,11 @@ are listed below. (excluding user X) of all rooms user X is a member of receive a `_push online` push message stating that `<user>` (user X) is now online with `<numonline>` sessions. -- `_push message <roomname:word> <user:word> <timestamp:i64> <msgid:i64> <message:string...>` +- `_push message <roomname:word> <user:word> <timestamp:i64> <msgid:i64> <replymsgid:i64> <message:string...>` - Sent to all sessions of all members of a room in which a message is - posted, except the session that sent the message. + posted, except the session that sent the message. `<replymsgid>` is -1 if + the message is a normal message, and references the replied-to message + otherwise. - `_push invite <roomname:word> <user:word>` - Sent to all sessions of the invited user after an `invite` command. The `<user>` parameter indicates who invited you to the given room. @@ -1,3 +1,5 @@ +-- Database schema version 2 + pragma foreign_keys = on; create table Meta ( @@ -31,9 +33,11 @@ create table Messages ( room integer not null, user integer null, time integer not null, + reply integer null, message blob, foreign key(room) references Rooms(id) on delete cascade, - foreign key(user) references Users(id) on delete set null + foreign key(user) references Users(id) on delete set null, + foreign key(reply) references Messages(id) on delete set null ); create index messages_time_index on Messages(room, time desc); diff --git a/ssh/client.c b/ssh/client.c index 52f26c6..a262d6f 100644 --- a/ssh/client.c +++ b/ssh/client.c @@ -199,7 +199,7 @@ static bool handle_line( if (state->focus_room != NULL) { char *message = NULL; sv_copy(line, &message); - enum tomsg_retval ret = tomsg_send(client, state->focus_room, message, NULL); + enum tomsg_retval ret = tomsg_send(client, state->focus_room, message, -1, NULL); free(message); if (ret != TOMSG_OK) return true; return false; @@ -253,7 +253,15 @@ static bool handle_line( } else if (sv_equals(command, "s") || sv_equals(command, "send")) { if (parse_args(line, args, num_args = 2, true)) { autocomplete_roomname(state, &args[0]); - ret = tomsg_send(client, args[0], args[1], NULL); + ret = tomsg_send(client, args[0], args[1], -1, NULL); + } + + } else if (sv_equals(command, "r") || sv_equals(command, "reply")) { + int64_t replyid; + if (parse_args(line, args, num_args = 3, true) && + parse_i64(args[1], &replyid)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_send(client, args[0], args[2], replyid, NULL); } } else if (sv_equals(command, "ping")) { @@ -277,6 +285,14 @@ static bool handle_line( ret = tomsg_history(client, args[0], count, msgid); } + } else if (sv_equals(command, "get") || + sv_equals(command, "get_message")) { + int64_t msgid; + if (parse_args(line, args, num_args = 1, false) && + parse_i64(args[0], &msgid)) { + ret = tomsg_get_message(client, msgid); + } + } else if (sv_equals(command, "on") || sv_equals(command, "is_online")) { if (parse_args(line, args, num_args = 1, false)) { @@ -314,9 +330,11 @@ static bool handle_line( " create_room\n" " invite <room> <user>\n" " s/send <room> <message...>\n" + " r/reply <room> <msgid> <message...>\n" " ping\n" " hist/history <room> <count>\n" " histb/history_before <room> <count> <before_msgid>\n" + " get/get_message <msgid>\n" " on/is_online <user>\n" " act/user_active <y/n>\n" " help\n" @@ -352,6 +370,7 @@ static const char* event_type_descr(enum tomsg_event_type type) { case TOMSG_EV_INVITE: return "invite"; case TOMSG_EV_SEND: return "send"; case TOMSG_EV_HISTORY: return "history"; + case TOMSG_EV_GET_MESSAGE: return "get_message"; case TOMSG_EV_PING: return "ping"; case TOMSG_EV_IS_ONLINE: return "is_online"; case TOMSG_EV_USER_ACTIVE: return "user_active"; @@ -365,8 +384,12 @@ static const char* event_type_descr(enum tomsg_event_type type) { } static void print_history_message(const struct history_message msg) { - printf("%" PRIi64 " \x1B[90m%" PRIi64 "\x1B[0m <%s> %s\n", - msg.msgid, msg.timestamp, msg.username, msg.message); + printf("%" PRIi64 " \x1B[90m%" PRIi64 "\x1B[0m <%s> ", + msg.msgid, msg.timestamp, msg.username); + if (msg.replyid != -1) { + printf("\x1B[32m[%" PRIi64 "<-]\x1B[0m ", msg.replyid); + } + printf("%s\n", msg.message); } static void handle_event(struct state *state, const struct tomsg_event event) { @@ -438,6 +461,12 @@ static void handle_event(struct state *state, const struct tomsg_event event) { } break; + case TOMSG_EV_GET_MESSAGE: + printf(" %" PRIi64 " is in room %s:\n ", + event.get_message.message.msgid, event.get_message.room_name); + print_history_message(event.get_message.message); + break; + case TOMSG_EV_PING: printf(" Pong\n"); break; diff --git a/ssh/tomsg_clientlib.c b/ssh/tomsg_clientlib.c index c14d5c5..e6d3684 100644 --- a/ssh/tomsg_clientlib.c +++ b/ssh/tomsg_clientlib.c @@ -178,7 +178,7 @@ const char* tomsg_strerror(enum tomsg_retval code) { static enum tomsg_retval version_negotiation(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; - const enum sshnc_retval retssh = sshnc_send(client->conn, "ver version 1\n", 14); + const enum sshnc_retval retssh = sshnc_send(client->conn, "ver version 2\n", 14); if (retssh == SSHNC_EOF) return TOMSG_ERR_CLOSED; if (retssh != SSHNC_OK) return TOMSG_ERR_TRANSPORT; @@ -635,6 +635,11 @@ void tomsg_event_nullify(struct tomsg_event event) { free(event.history.messages); break; + case TOMSG_EV_GET_MESSAGE: + free(event.get_message.room_name); + history_message_nullify(event.get_message.message); + break; + case TOMSG_EV_PUSH_MESSAGE: free(event.push_message.room_name); history_message_nullify(event.push_message.message); @@ -691,6 +696,7 @@ static enum tomsg_retval handle_line( PARSE_WORD(push_message.message.username); PARSE_I64(push_message.message.timestamp); PARSE_I64(push_message.message.msgid); + PARSE_I64(push_message.message.replyid); PARSE_RESTSTRING(push_message.message.message); return TOMSG_OK; } else if (sv_equals(command, "online")) { @@ -809,6 +815,11 @@ static enum tomsg_retval handle_line( inflight.event.history.messages = calloc(count, sizeof(struct history_message)); if (!inflight.event.history.messages) CLEANUP_RETURN(TOMSG_ERR_MEMORY); + // If there are no history_message's coming, complete early + if (count == 0) { + SUCCESS_RETURN(); + } + // Re-add the tag for the history_message events enum tomsg_retval ret = add_inflight(client, tag, inflight.event); if (ret != TOMSG_OK) return ret; @@ -822,11 +833,12 @@ static enum tomsg_retval handle_line( // (and everywhere): the messages buffer has been allocated with // calloc(), so tomsg_event_nullify() will cleanup up precisely // everything that we haven't filled in yet. - struct history_message *msg = &inflight.event.history.messages[index];; + struct history_message *msg = &inflight.event.history.messages[index]; if (!sv_equals(sv_tokenise_word(&line), inflight.event.history.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_copy(sv_tokenise_word(&line), &msg->username)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &msg->timestamp)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_parse_i64(sv_tokenise_word(&line), &msg->msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_parse_i64(sv_tokenise_word(&line), &msg->replyid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_copy(line, &msg->message)) CLEANUP_RETURN(TOMSG_ERR_PARSE); // If there are more history_message events coming, re-add the tag for them @@ -841,6 +853,16 @@ static enum tomsg_retval handle_line( CLEANUP_RETURN(TOMSG_ERR_PARSE); } + case TOMSG_EV_GET_MESSAGE: + if (!sv_equals(command, "message")) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_copy(sv_tokenise_word(&line), &inflight.event.get_message.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_copy(sv_tokenise_word(&line), &inflight.event.get_message.message.username)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.get_message.message.timestamp)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.get_message.message.msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.get_message.message.replyid)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + if (!sv_copy(line, &inflight.event.get_message.message.message)) CLEANUP_RETURN(TOMSG_ERR_PARSE); + SUCCESS_RETURN(); + case TOMSG_EV_PING: if (!sv_equals(command, "pong")) CLEANUP_RETURN(TOMSG_ERR_PARSE); if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE); @@ -1018,13 +1040,15 @@ enum tomsg_retval tomsg_invite( enum tomsg_retval tomsg_send( struct tomsg_client *client, const char *room_name, const char *message, + int64_t replyid, int64_t *tagp //output ) { if (!client->conn) return TOMSG_ERR_CLOSED; if (hasspacelf(room_name)) return TOMSG_ERR_SPACE; if (haslf(message)) return TOMSG_ERR_SPACE; const int64_t tag = client->next_tag++; - SEND_FMT(client, tag, "send %s %s", room_name, message); + if (replyid < 0) replyid = -1; + SEND_FMT(client, tag, "send %s %" PRIi64 " %s", room_name, replyid, message); const struct tomsg_event event = (struct tomsg_event){ .type = TOMSG_EV_SEND, .send.tag = tag, @@ -1055,6 +1079,17 @@ enum tomsg_retval tomsg_history( return add_inflight(client, tag, event); } +enum tomsg_retval tomsg_get_message(struct tomsg_client *client, int64_t msgid) { + if (!client->conn) return TOMSG_ERR_CLOSED; + if (msgid < -1) msgid = -1; + const int64_t tag = client->next_tag++; + const struct tomsg_event event = (struct tomsg_event){ + .type = TOMSG_EV_GET_MESSAGE, + }; + SEND_FMT(client, tag, "get_message %" PRIi64, msgid); + return add_inflight(client, tag, event); +} + enum tomsg_retval tomsg_ping(struct tomsg_client *client) { if (!client->conn) return TOMSG_ERR_CLOSED; const int64_t tag = client->next_tag++; diff --git a/ssh/tomsg_clientlib.h b/ssh/tomsg_clientlib.h index ab1bde5..8a4aae4 100644 --- a/ssh/tomsg_clientlib.h +++ b/ssh/tomsg_clientlib.h @@ -126,6 +126,7 @@ enum tomsg_event_type { TOMSG_EV_INVITE, // join TOMSG_EV_SEND, // send TOMSG_EV_HISTORY, // history + TOMSG_EV_GET_MESSAGE, // get_message TOMSG_EV_PING, // - TOMSG_EV_IS_ONLINE, // is_online TOMSG_EV_USER_ACTIVE, // user_active @@ -139,6 +140,7 @@ struct history_message { char *username; int64_t timestamp; int64_t msgid; + int64_t replyid; // -1 if normal message, the replied-to message id otherwise char *message; }; @@ -180,6 +182,10 @@ struct tomsg_event { struct history_message *messages; } history; struct { + char *room_name; + struct history_message message; + } get_message; + struct { char *username; int64_t online_count; } is_online; @@ -245,8 +251,12 @@ enum tomsg_retval tomsg_invite( // If 'tag' is not NULL, will write the message request tag to the referenced // location. This tag will also be given in the TOMSG_EV_SEND response, so that // the response can be linked to the original message. +// If 'replyid' is -1, will send a normal message. If not, it must be the id of +// an earlier message in the same room, and this will send a reply to the +// indicated message. enum tomsg_retval tomsg_send( struct tomsg_client *client, const char *room_name, const char *message, + int64_t replyid, int64_t *tag // output ); @@ -257,6 +267,9 @@ enum tomsg_retval tomsg_history( struct tomsg_client *client, const char *room_name, int64_t count, int64_t before_msgid); +// Send a get_message command to the server. +enum tomsg_retval tomsg_get_message(struct tomsg_client *client, int64_t msgid); + // Send a ping command to the server. enum tomsg_retval tomsg_ping(struct tomsg_client *client); diff --git a/webclient/client.html b/webclient/client.html index 28a91fd..f88ee07 100644 --- a/webclient/client.html +++ b/webclient/client.html @@ -4,12 +4,16 @@ <meta charset="utf-8"> <title>tomsg webclient</title> <script> -var PROTOCOL_VERSION=1; +var PROTOCOL_VERSION=2; + +// Note: message id's are considered strings in this code. This is to not have to deal with full 64-bit integers. var sock=null,negotiated_version=false,username=null; +var messagecache=new Map(); // msgid => [user,message] var roomlist=[":console"]; var currentroom=":console"; -var roomlogs=new Map([[":console",[]]]); +var currentreply=null; // [msgid, table row] or null +var roomlogs=new Map([[":console",[]]]); // see drawRoomEntry for entry types var commandlist=[ {cmd:"register",usage:"/register <username> <password>"}, @@ -93,11 +97,17 @@ function net_historyCollectionCallback(id,list,count,cb, item,err){ } } -function reconnect(){ +function resetState() { if(sock)sock.close(); - - net_callbacks={}; negotiated_version=false; + username=null; + messagecache.clear(); + net_callbacks={}; + cancelReply(); +} + +function reconnect(){ + resetState(); var url; if(location.hostname!="")url="wss://"+location.hostname; @@ -112,8 +122,10 @@ function reconnect(){ if(id=="_push"){ if(type=="message"){ var r=spl.word[2],u=spl.word[3],t=new Date(+spl.word[4]/1000); - // Ignore msgid at word[5] - addRoomEntry(r,"message",[u,t,spl.rest[6]]); + var msgid=spl.word[5],replyid=spl.word[6]; + var text=spl.rest[7]; + messagecache.set(msgid,[u,text]); + addRoomEntry(r,"message",[u,t,msgid,replyid,text]); } else if(type=="invite"){ var r=spl.word[2],inviter=spl.word[3]; roomlist.push(r); @@ -137,13 +149,14 @@ function reconnect(){ var obj; if(type=="ok")fn(true); else if(type=="error")fn(null,spl.rest[2]); - else if(type=="number")fn(+spl.rest[2]); + else if(type=="number")fn(spl.rest[2]); // Note: no int parse! else if(type=="name")fn(spl.rest[2]); else if(type=="list")fn(spl.word.slice(3)); else if(type=="history"){ var count=+spl.word[2]; net_callbacks[id]=net_historyCollectionCallback.bind(this,id,[],count,fn); - } else if(type=="history_message")fn([spl.word[4],+spl.word[5]/1000,spl.rest[7]]); + } else if(type=="history_message")fn([spl.word[4],+spl.word[5]/1000,spl.word[6],spl.word[7],spl.rest[8]]); + else if(type=="message")fn([spl.word[3],+spl.word[4]/1000,spl.word[5],spl.word[6],spl.rest[7]]); else alert("Unknown server response message type '"+type+"'!"); } else { alert("No callback for server message id '"+id+"'!"); @@ -206,6 +219,18 @@ function fetchRoomHistory(roomid){ }); } +function getMessage(msgid,callback){ + if(messagecache.has(msgid)){ + callback(null,messagecache.get(msgid)); + return; + } + + net_send("get_message "+msgid,function(item,err){ + if(err)callback(err,null); + else callback(null,item); + }); +} + function updateStatus(){ var str=null; if(!sock||sock.readyState>=2){ @@ -284,21 +309,44 @@ function drawRoomEntry(type,args){ tr.appendChild(td2); tbody.appendChild(tr); switch(type){ - case "message": + case "message": // [username, timestamp_ms, msgid, replyid, text] tr.classList.add("message"); node0.nodeValue=formatTime(new Date(args[1])); node1.nodeValue="<"+args[0]+">"; - node2.nodeValue=args[2]; + node2.nodeValue=args[4]; + if(args[3]!="-1"){ + var replyNode=document.createElement("span"); + replyNode.classList.add("reply"); + replyNode.classList.add("pending"); + getMessage(args[3],function(err,item){ + replyNode.classList.remove("pending"); + var text; + if(err){ + replyNode.classList.add("failed"); + text="Failed to fetch original message!"; + } else { + text="> <"+item[0]+"> "+item[4]; + } + replyNode.appendChild(document.createTextNode(text)); + }); + td2.insertBefore(replyNode,node2); + td2.insertBefore(document.createElement("br"),node2); + } + + tr.addEventListener("click",function(){ + if(currentreply&¤treply[0]==args[2])cancelReply(); + else startReply(args[2],tr); + }); break; - case "error": + case "error": // [timestamp_ms, text] tr.classList.add("error"); node0.nodeValue=formatTime(new Date(args[0])); node1.nodeValue="--"; node2.nodeValue=args[1]; break; - case "notice": + case "notice": // [timestamp_ms, text] tr.classList.add("notice"); node0.nodeValue=formatTime(new Date(args[0])); node1.nodeValue="--"; @@ -323,6 +371,21 @@ function addRoomEntry(roomid,type,args){ } } +function cancelReply(){ + if(!currentreply)return; + currentreply[1].classList.remove("replying"); + document.getElementById("roominput_td").classList.remove("replying"); + currentreply=null; +} + +function startReply(msgid,tr){ + cancelReply(); + currentreply=[msgid,tr]; + tr.classList.add("replying"); + document.getElementById("roominput_td").classList.add("replying"); + document.getElementById("roominput").focus(); +} + function showUsage(roomid,cmd){ for(var i=0;i<commandlist.length;i++){ if(commandlist[i].cmd==cmd){ @@ -423,10 +486,15 @@ function sendMessage(roomid,text){ addRoomEntry(roomid,"error",[now(),"Cannot send a message here"]); return; } + var sentAs=username,msg=text.replace(/\n/g,""); - net_send("send "+roomid+" "+msg,function(msgid,err){ + var replyid=currentreply?currentreply[0]:"-1"; + cancelReply(); + + net_send("send "+roomid+" "+replyid+" "+msg,function(msgid,err){ if(msgid!=null){ - addRoomEntry(roomid,"message",[sentAs,new Date().getTime(),msg]); + msgid=msgid.toString(); + addRoomEntry(roomid,"message",[sentAs,new Date().getTime(),msgid,replyid,msg]); return; } addRoomEntry(roomid,"error",[now(),"Unable to send message: "+err]); @@ -478,6 +546,10 @@ table{ height:100%; } +.invisible{ + display:none; +} + /* SIDEBAR */ #sidebar{ width:150px; @@ -557,6 +629,13 @@ table{ } #roomlog > tr.message{ color:inherit; + cursor:pointer; +} +#roomlog > tr.message:hover{ + background-color:#202030; +} +#roomlog > tr.replying{ + background-color:#090 !important; } #roomlog > tr.error{ color:#f44; @@ -564,6 +643,17 @@ table{ #roomlog > tr.notice{ color:#a8f; } +#roomlog > tr > td > span.reply{ + color:#8f8; + font-style:italic; +} +#roomlog > tr > td > span.reply.pending:after{ + content:"(...)"; +} +#roomlog > tr > td > span.reply.failed{ + color:#f22; + font-style:normal; +} #roombar_tr{ height:30px; @@ -574,6 +664,9 @@ table{ vertical-align:top; padding:0px 1px 0px 10px; } +#roominput_td.replying{ + background-color:#090; +} #roominput{ position:absolute; background-color:rgba(0,0,0,0); diff --git a/weechat/net.c b/weechat/net.c index 897655a..ce8da6a 100644 --- a/weechat/net.c +++ b/weechat/net.c @@ -220,9 +220,9 @@ void net_handle_recv(int fd,const char *msg){ } const char *roomp=p+1; p=strchr(roomp,' '); - const char *q,*r,*s; + const char *q,*r,*s,*t; if(p==NULL||(q=strchr(p+1,' '))==NULL||(r=strchr(q+1,' '))==NULL - ||(s=strchr(r+1,' '))==NULL){ + ||(s=strchr(r+1,' '))==NULL||(t=strchr(s+1,' '))==NULL){ debugf("net_handle_recv: not enough arguments to 'message' <%s>\n",msg); return; } @@ -233,10 +233,12 @@ void net_handle_recv(int fd,const char *msg){ i64 stamplen=r-stampp; const char *msgidp=r+1; i64 msgidlen=s-msgidp; - const char *textp=s+1; + const char *replyidp=s+1; + i64 replyidlen=t-replyidp; + const char *textp=t+1; i64 textlen=msglen-(textp-msg); - (void)msgidp; (void)msgidlen; + (void)msgidp; (void)msgidlen; (void)replyidlen; struct net_response res; res.type=NET_MESSAGE; @@ -246,6 +248,16 @@ void net_handle_recv(int fd,const char *msg){ debugf("net_handle_recv: timestamp not a number in 'message' <%s>\n",msg); return; } + res.msgid=strtoll(msgidp,(char**)&endp,10); + if(endp-msgidp!=msgidlen){ + debugf("net_handle_recv: msgid not a number in 'message' <%s>\n",msg); + return; + } + res.replyid=strtoll(replyidp,(char**)&endp,10); + if(endp-replyidp!=replyidlen){ + debugf("net_handle_recv: replyid not a number in 'message' <%s>\n",msg); + return; + } res.room=malloc(roomlen+1); res.username=malloc(usernamelen+1); res.message=malloc(textlen+1); @@ -276,9 +288,9 @@ void net_handle_recv(int fd,const char *msg){ } const char *roomp=p+1; p=strchr(roomp,' '); - const char *q,*r,*s; + const char *q,*r,*s,*t; if(p==NULL||(q=strchr(p+1,' '))==NULL||(r=strchr(q+1,' '))==NULL - ||(s=strchr(r+1,' '))==NULL){ + ||(s=strchr(r+1,' '))==NULL||(t=strchr(s+1,' '))==NULL){ debugf("net_handle_recv: not enough arguments to 'history_message' <%s>\n",msg); return; } @@ -289,11 +301,11 @@ void net_handle_recv(int fd,const char *msg){ i64 stamplen=r-stampp; const char *msgidp=r+1; i64 msgidlen=s-msgidp; - const char *textp=s+1; + const char *replyidp=s+1; + i64 replyidlen=t-replyidp; + const char *textp=t+1; i64 textlen=msglen-(textp-msg); - (void)msgidp; (void)msgidlen; - struct net_response res; res.type=NET_HISTORY; const char *endp; @@ -302,6 +314,16 @@ void net_handle_recv(int fd,const char *msg){ debugf("net_handle_recv: timestamp not a number in 'history_message' <%s>\n",msg); return; } + res.msgid=strtoll(msgidp,(char**)&endp,10); + if(endp-msgidp!=msgidlen){ + debugf("net_handle_recv: msgid not a number in 'history_message' <%s>\n",msg); + return; + } + res.replyid=strtoll(replyidp,(char**)&endp,10); + if(endp-replyidp!=replyidlen){ + debugf("net_handle_recv: replyid not a number in 'history_message' <%s>\n",msg); + return; + } res.room=malloc(roomlen+1); res.username=malloc(usernamelen+1); res.message=malloc(textlen+1); diff --git a/weechat/net.h b/weechat/net.h index 63636e7..4a61bca 100644 --- a/weechat/net.h +++ b/weechat/net.h @@ -17,8 +17,8 @@ enum net_response_type{ NET_NAME, // name NET_LIST, // nitems, items NET_PONG, // - - NET_MESSAGE, // room, username, timestamp, message - NET_HISTORY, // room, username, timestamp, message + NET_MESSAGE, // room, username, timestamp, msgid, replyid, message + NET_HISTORY, // room, username, timestamp, msgid, replyid, message NET_JOIN, // room, username NET_INVITE, // room, username NET_ONLINE, // online.username, online.num @@ -38,6 +38,8 @@ struct net_response{ char *room; char *username; i64 timestamp; + i64 msgid; + i64 replyid; char *message; }; struct { diff --git a/weechat/tomsg.c b/weechat/tomsg.c index 04213ae..df6e067 100644 --- a/weechat/tomsg.c +++ b/weechat/tomsg.c @@ -21,6 +21,8 @@ WEECHAT_PLUGIN_PRIORITY(1000) static const char *errpfx,*netpfx; +#define PROTOCOL_VERSION 2 + #define NICK_COLOR "default" #define NICK_AWAY_COLOR "weechat.color.nicklist_away" @@ -48,12 +50,100 @@ struct conndata{ i64 linebuf_sz,linebuf_len; char *linebuf; + + // struct t_hashtable *msgtable; // msgid -> msgdata }; +// struct msgdata{ +// char *username; +// char *message; +// }; + static struct t_weechat_plugin *weechat_plugin; -static struct t_hashtable *conntable; +static struct t_hashtable *conntable; // fd -> conndata +static struct t_hashtable *roomtable; // roomname -> roomdata + + +static void display_message( + struct roomdata *room, + int64_t timestamp, + const char *username, + const char *message, + int64_t msgid, + bool is_reply +) { + char tags[128]; + int pos = snprintf(tags, sizeof tags, "tomsgid_%" PRIi64, msgid); + strcpy(tags + pos, room->nmembers <= 2 ? ",notify_private" : ",notify_message"); + if (is_reply) { + strcat(tags, ",tomsg_reply"); + } + + weechat_printf_date_tags( + room->buffer, timestamp / 1000000LL, + tags, + "%s\t%s", username, message + ); +} + +static void edit_reply_message( + struct roomdata *room, + int64_t msgid, + const char *new_message +) { + struct t_hdata *buffer_h = weechat_hdata_get("buffer"); + struct t_hdata *lines_h = weechat_hdata_get("lines"); + struct t_hdata *line_h = weechat_hdata_get("line"); + struct t_hdata *line_data_h = weechat_hdata_get("line_data"); + + struct t_gui_lines *lines_ptr = weechat_hdata_pointer(buffer_h, room->buffer, "lines"); + if (!lines_ptr) { + debugf("ERROR: Cannot get lines_ptr!"); + return; + } + + struct t_gui_line *line_ptr = weechat_hdata_pointer(lines_h, lines_ptr, "last_line"); + struct t_gui_line_data *line_data_ptr; + + while (line_ptr) { + line_data_ptr = weechat_hdata_pointer(line_h, line_ptr, "data"); + + const int tags_count = weechat_hdata_integer(line_data_h, line_data_ptr, "tags_count"); + bool have_reply_tag = false; + bool have_msgid_tag = false; + for (int i = 0; i < tags_count; i++) { + char key[32]; + snprintf(key, sizeof key, "%d|tags_array", i); + const char *tag = weechat_hdata_string(line_data_h, line_data_ptr, key); + if (strcmp(tag, "tomsg_reply") == 0) { + have_reply_tag = true; + } else if (memcmp(tag, "tomsgid_", 8) == 0) { + int64_t this_id = strtoll(tag + 8, NULL, 10); + if (this_id == msgid) have_msgid_tag = true; + } + } + + if (have_reply_tag && have_msgid_tag) break; + + line_ptr = weechat_hdata_pointer(line_h, line_ptr, "prev_line"); + } + + if (line_ptr) { + debugf("edit_reply_message: found line\n"); + struct t_hashtable *hashtable = + weechat_hashtable_new( + 8, WEECHAT_HASHTABLE_STRING, WEECHAT_HASHTABLE_STRING, NULL, NULL); + if (hashtable) { + weechat_hashtable_set(hashtable, "message", new_message); + weechat_hdata_update(line_data_h, line_data_ptr, hashtable); + weechat_hashtable_free(hashtable); + } + } else { + debugf("edit_reply_message: not found!\n"); + } +} static void room_update_attributes(struct roomdata *room){ @@ -76,7 +166,7 @@ static void close_room(struct roomdata *room){ } static void message_net_callback(int fd,struct net_response res,void *payload){ - (void)payload; + // struct msgdata *msgdata=(struct msgdata*)payload; debugf("message_net_callback(fd=%d,res={.type=%d})\n",fd,res.type); struct conndata *conn=weechat_hashtable_get(conntable,&fd); assert(conn); @@ -104,7 +194,12 @@ static int room_input_cb(const void *room_vp,void *_d,struct t_gui_buffer *buffe tosend=input+skipfirst; } - net_sendf(conn->fd,message_net_callback,NULL,"send %s %s",room->name,tosend); + // struct msgdata *msgdata=malloc(sizeof(msgdata)); + // msgdata->username=strdup(conn->username); + // msgdata->message=strdup(tosend); + void *msgdata=NULL; + + net_sendf(conn->fd,message_net_callback,msgdata,"send %s -1 %s",room->name,tosend); weechat_printf(room->buffer,"%s\t%s",conn->username,tosend); if(free_tosend){ @@ -124,6 +219,9 @@ static void create_room_buffer(struct roomdata *room){ if(room->buffer!=NULL){ weechat_buffer_close(room->buffer); } + + weechat_hashtable_set(roomtable,room->name,room); + room->buffer= weechat_buffer_new(room->name, room_input_cb,room,NULL, room_close_cb,room,NULL); weechat_buffer_set(room->buffer,"nicklist","1"); @@ -194,6 +292,28 @@ static void members_net_callback(int fd,struct net_response res,void *payload){ room_update_attributes(room); } +struct room_and_msgid { + struct roomdata *room; + int64_t msgid; +}; + +static void reply_get_message_net_callback(int fd, struct net_response res, void *payload) { + (void)fd; + struct room_and_msgid *data = (struct room_and_msgid*)payload; + + debugf("Got reply from get_message for msgid=%" PRIi64 "\n", res.msgid); + + const char *green = weechat_color("green"); + + size_t prefixlen = strlen(green) + 3 + strlen(res.username) + 2; + char *buffer = malloc(prefixlen + strlen(res.message) + 1); + sprintf(buffer, "%s> <%s> %s", weechat_color("green"), res.username, res.message); + edit_reply_message(data->room, data->msgid, buffer); + free(buffer); + + free(data); +} + static void push_net_callback(int fd,struct net_response res,void *payload){ (void)payload; debugf("push_net_callback(fd=%d,res={.type=%d})\n",fd,res.type); @@ -215,15 +335,27 @@ static void push_net_callback(int fd,struct net_response res,void *payload){ create_room_buffer(room); } - if(res.type==NET_MESSAGE){ - bool private=room->nmembers<=2; - weechat_printf_date_tags( - room->buffer,res.timestamp/1000000LL,private?"notify_private":"notify_message", - "%s\t%s",res.username,res.message); - } else if(res.type==NET_HISTORY){ - weechat_printf_date_tags( - room->buffer,res.timestamp/1000000LL,NULL, - "%s\t%s",res.username,res.message); + if(res.type==NET_MESSAGE||res.type==NET_HISTORY){ + if(res.replyid!=-1){ + debugf("Found reply msgid=%" PRIi64 " replyid=%" PRIi64 "\n",res.msgid,res.replyid); + char str[128]; + snprintf(str,sizeof str,"%s> ...",weechat_color("green")); + display_message(room,res.timestamp,res.username,str,res.msgid,true); + display_message(room,res.timestamp,"",res.message,res.msgid,false); + + struct room_and_msgid *payload=malloc(sizeof(struct room_and_msgid)); + assert(payload); + payload->room=room; + payload->msgid=res.msgid; + net_sendf(fd,reply_get_message_net_callback,payload,"get_message %" PRIi64,res.replyid); + } else { + display_message(room,res.timestamp,res.username,res.message,res.msgid,false); + } + + // struct msgdata *msgdata=malloc(sizeof(msgdata)); + // msgdata->username=strdup(res.username); + // msgdata->message=strdup(res.message); + // weechat_hashtable_set(conn->msgtable,(void*)res.msgid,msgdata); } else if(res.type==NET_JOIN){ weechat_printf(room->buffer,"%sUser %s joined this room",netpfx,res.username); if(room->buffer_nickgroup){ @@ -327,7 +459,17 @@ static void pong_net_callback(int fd,struct net_response res,void *payload){ static void conn_destroy(struct conndata *conn){ debugf("conn_destroy(conn=%p (fd=%d))\n",conn,conn->fd); weechat_unhook(conn->fd_hook); + if(conntable)weechat_hashtable_remove(conntable,&conn->fd); + if(roomtable){ + for(int i=0;i<conn->nrooms;i++){ + weechat_hashtable_remove(roomtable,conn->rooms[i]->name); + } + } + + // TODO: free all items in the hashtable + // weechat_hashtable_free(conn->msgtable); + for(int i=0;i<conn->nrooms;i++){ close_room(conn->rooms[i]); } @@ -462,8 +604,10 @@ static char* password_hide_modifier(const void *_p,void *_d,const char *modifier static int conn_close_cb(const void *conn_vp,void *_d,struct t_gui_buffer *buffer){ (void)_d; (void)buffer; struct conndata *conn=(struct conndata*)conn_vp; - debugf("conn_close_cb(conn=%p,buffer=%p) fd=%d\n",conn,buffer,conn->fd); - conn_destroy(conn); + if(conn){ + debugf("conn_close_cb(conn=%p,buffer=%p) fd=%d\n",conn,buffer,conn->fd); + conn_destroy(conn); + } return WEECHAT_RC_OK; } @@ -477,7 +621,7 @@ static void version_net_callback(int fd,struct net_response res,void *payload){ weechat_printf(conn->buffer,"Version negotiation complete."); } else { conn_destroy(conn); - weechat_printf(NULL,"tomsg: Server has incompatible protocol version (we want 1)!"); + weechat_printf(NULL,"tomsg: Server has incompatible protocol version (we want %d)!",PROTOCOL_VERSION); } } @@ -501,12 +645,15 @@ static int connect_cb(const void *_p,void *hostname,int status,int _g,int fd,con conn->linebuf_len=0; conn->linebuf=malloc(conn->linebuf_sz); assert(conn->linebuf); + // conn->msgtable=weechat_hashtable_new( + // 4096, WEECHAT_HASHTABLE_INTEGER, WEECHAT_HASHTABLE_POINTER, NULL, NULL); + // assert(conn->msgtable); weechat_printf(conn->buffer,"Connected!"); weechat_hashtable_set(conntable,&fd,conn); - net_sendf(fd,version_net_callback,NULL,"version 1"); + net_sendf(fd,version_net_callback,NULL,"version %d", PROTOCOL_VERSION); return WEECHAT_RC_OK; } @@ -517,8 +664,10 @@ static int connect_cb(const void *_p,void *hostname,int status,int _g,int fd,con } } -static int cmd_tomsg_cb(const void *_p,void *_d,struct t_gui_buffer *buffer,int argc,char **argv,char **_a){ - (void)_p; (void)_d; (void)_a; +static int cmd_tomsg_cb( + const void *_p,void *_d,struct t_gui_buffer *buffer, + int argc,char **argv,char **argv_eol){ + (void)_p; (void)_d; if(argc<2){ weechat_printf(buffer,"%stomsg: Invalid number of arguments to /tomsg",errpfx); return WEECHAT_RC_ERROR; @@ -554,6 +703,50 @@ static int cmd_tomsg_cb(const void *_p,void *_d,struct t_gui_buffer *buffer,int NULL,NULL,0,NULL, NULL, connect_cb,NULL,hostname_copy); + } else if(strcmp(argv[1],"reply")==0){ + if(argc<4){ + weechat_printf(buffer,"%stomsg: Invalid number of arguments to /tomsg reply",errpfx); + return WEECHAT_RC_ERROR; + } + + char *endp; + int64_t msgid=strtol(argv[2],&endp,10); + if(*argv[2]=='\0'||*endp!='\0'){ + weechat_printf(NULL,"%stomsg: Invalid msgid argument to /tomsg reply",errpfx); + return WEECHAT_RC_ERROR; + } + + const char *buffername=weechat_buffer_get_string(buffer,"full_name"); + debugf("tomsg reply: buffername=<%s>\n",buffername); + const char *prefix="tomsg."; + size_t prefixlen=strlen(prefix); + if(strlen(buffername)>prefixlen&& + memcmp(buffername,prefix,prefixlen)==0){ + struct roomdata *room= + weechat_hashtable_get(roomtable,buffername+prefixlen); + if(!room){ + weechat_printf(NULL,"%stomsg: Cannot locate internal room data!",errpfx); + return WEECHAT_RC_ERROR; + } + + const char *message_body=argv_eol[3]; + + // struct msgdata *msgdata=malloc(sizeof(struct msgdata)); + // msgdata->username=strdup(room->conn->username); + // msgdata->message=strdup(message_body); + void *msgdata=NULL; + + int fd=room->conn->fd; + net_sendf(fd,message_net_callback,msgdata,"send %s %" PRIi64 " %s",room->name,msgid,message_body); + + // TODO: fix this + weechat_printf(room->buffer,"%s\t%s> ...", + room->conn->username,weechat_color("green")); + weechat_printf(room->buffer,"\t%s",message_body); + } else { + weechat_printf(NULL,"%stomsg: /tomsg reply on a non-tomsg buffer",errpfx); + return WEECHAT_RC_ERROR; + } } else { weechat_printf(buffer,"%stomsg: Unknown command \"%s\" to /tomsg",errpfx,argv[1]); return WEECHAT_RC_ERROR; @@ -562,6 +755,62 @@ static int cmd_tomsg_cb(const void *_p,void *_d,struct t_gui_buffer *buffer,int return WEECHAT_RC_OK; } +static void bind_key_easy(const char *context, const char *key, const char *value) { + struct t_hashtable *ht = weechat_hashtable_new( + 1, WEECHAT_HASHTABLE_STRING, WEECHAT_HASHTABLE_STRING, NULL, NULL); + weechat_hashtable_set(ht, key, value); + weechat_key_bind(context, ht); + weechat_hashtable_free(ht); +} + +static int cursor_reply_signal_cb( + const void *pointer_, void *data_, + const char *signal, struct t_hashtable *hashtable +) { + (void)pointer_; (void)data_; (void)signal; + + const char *tags = weechat_hashtable_get(hashtable, "_chat_line_tags"); + weechat_command(NULL, "/cursor stop"); + + int64_t msgid = -1; + while (*tags) { + const char *ptr = strchr(tags, ','); + if (ptr - tags > (ptrdiff_t)strlen("tomsgid_")) { + char *endp; + msgid = strtoll(tags + 8, &endp, 10); + if ((ptr == NULL && *endp == '\0') || endp == ptr) { + break; + } + msgid = -1; + } + } + + if (msgid == -1) return WEECHAT_RC_OK; + + const char *buffer_name = weechat_hashtable_get(hashtable, "_buffer_full_name"); + struct t_gui_buffer *bufptr = weechat_buffer_search("==", buffer_name); + + const char *current_input = weechat_buffer_get_string(bufptr, "input"); + int input_pos = weechat_buffer_get_integer(bufptr, "input_pos"); + + size_t new_input_capacity = 64 + strlen(current_input) + 1; + char *new_input = malloc(new_input_capacity); + assert(new_input); + int total_length = snprintf( + new_input, new_input_capacity, "/tomsg reply %" PRIi64 " %s", + msgid, current_input); + int prefix_length = total_length - strlen(current_input); + + weechat_buffer_set(bufptr, "input", new_input); + free(new_input); + + char posbuf[64]; + snprintf(posbuf, sizeof posbuf, "%d", prefix_length + input_pos); + weechat_buffer_set(bufptr, "input_pos", posbuf); + + return WEECHAT_RC_OK; +} + int weechat_plugin_init(struct t_weechat_plugin *plugin,int argc,char **argv){ (void)argc; (void)argv; weechat_plugin = plugin; @@ -574,18 +823,25 @@ int weechat_plugin_init(struct t_weechat_plugin *plugin,int argc,char **argv){ weechat_hook_command( "tomsg", "Execute commands related to tomsg.", - "connect <hostname> [port]", - " connect: Connect to a tomsg server", + "connect <hostname> [port] | " + "reply <msgid> <your reply...>", + " connect: Connect to a tomsg server\n" + " reply: Reply to a message in a tomsg buffer. Tip: use middleclick-R.", NULL, cmd_tomsg_cb,NULL,NULL); weechat_hook_modifier("input_text_display_with_cursor",password_hide_modifier,NULL,NULL); + weechat_hook_hsignal("tomsg_cursor_reply", cursor_reply_signal_cb, NULL, NULL); + bind_key_easy("cursor","@chat(tomsg.*):r","hsignal:tomsg_cursor_reply"); + net_set_push_callback(push_net_callback); net_set_history_callback(history_push_net_callback); conntable=weechat_hashtable_new( 16,WEECHAT_HASHTABLE_INTEGER,WEECHAT_HASHTABLE_POINTER,NULL,NULL); + roomtable=weechat_hashtable_new( + 16,WEECHAT_HASHTABLE_STRING,WEECHAT_HASHTABLE_POINTER,NULL,NULL); return WEECHAT_RC_OK; } @@ -593,7 +849,9 @@ int weechat_plugin_init(struct t_weechat_plugin *plugin,int argc,char **argv){ int weechat_plugin_end(struct t_weechat_plugin *plugin){ (void)plugin; weechat_hashtable_free(conntable); + weechat_hashtable_free(roomtable); conntable=NULL; + roomtable=NULL; debug_deinit(); return WEECHAT_RC_OK; } |