/* ** Copyright (c) 2020 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to implement the Fossil chatroom. ** ** Initial design goals: ** ** * Keep it simple. This chatroom is not intended as a competitor ** or replacement for IRC, Discord, Telegram, Slack, etc. The goal ** is zero- or near-zero-configuration, not an abundance of features. ** ** * Intended as a place for insiders to have ephemeral conversations ** about a project. This is not a public gather place. Think ** "boardroom", not "corner pub". ** ** * One chatroom per repository. ** ** * Chat content lives in a single repository. It is never synced. ** Content expires and is deleted after a set interval (a week or so). ** ** Notification is accomplished using the "hanging GET" or "long poll" design ** in which a GET request is issued but the server does not send a reply until ** new content arrives. Newer Web Sockets and Server Sent Event protocols are ** more elegant, but are not compatible with CGI, and would thus complicate ** configuration. */ #include "config.h" #include #include "chat.h" /* ** WEBPAGE: chat ** ** Start up a browser-based chat session. */ void chat_webpage(void){ login_check_credentials(); style_set_current_feature("chat"); if( !g.perm.Chat ){ style_header("Chat Not Authorized"); @

Not Authorized

@

You do not have permission to use the chatroom on this @ repository.

style_finish_page(); return; } style_header("Chat"); @ @
@
@
@ @ @
@
@ @
@ Drag/drop a file into this spot, or paste an image from @ the clipboard if supported by your environment. @
@
@
@
@
@
/* New chat messages get inserted immediately after this element */ @ builtin_fossil_js_bundle_or("popupwidget", NULL); /* Always in-line the javascript for the chat page */ @ style_finish_page(); } /* Definition of repository tables used by chat */ static const char zChatSchema1[] = @ CREATE TABLE repository.chat( @ msgid INTEGER PRIMARY KEY AUTOINCREMENT, @ mtime JULIANDAY, @ xfrom TEXT, @ xmsg TEXT, @ file BLOB, @ fname TEXT, @ fmime TEXT @ ); ; /* ** Make sure the repository data tables used by chat exist. Create them ** if they do not. */ static void chat_create_tables(void){ if( !db_table_exists("repository","chat") ){ db_multi_exec(zChatSchema1/*works-like:""*/); } } /* ** WEBPAGE: chat-send ** ** This page receives (via XHR) a new chat-message and/or a new file ** to be entered into the chat history. */ void chat_send_webpage(void){ int nByte; const char *zMsg; login_check_credentials(); if( !g.perm.Chat ) return; chat_create_tables(); nByte = atoi(PD("file:bytes",0)); zMsg = PD("msg",""); if( nByte==0 ){ if( zMsg[0] ){ db_multi_exec( "INSERT INTO chat(mtime,xfrom,xmsg)" "VALUES(julianday('now'),%Q,%Q)", g.zLogin, zMsg ); } }else{ Stmt q; Blob b; db_prepare(&q, "INSERT INTO chat(mtime, xfrom,xmsg,file,fname,fmime)" "VALUES(julianday('now'),%Q,%Q,:file,%Q,%Q)", g.zLogin, zMsg, PD("file:filename",""), PD("file:mimetype","application/octet-stream")); blob_init(&b, P("file"), nByte); db_bind_blob(&q, ":file", &b); db_step(&q); db_finalize(&q); blob_reset(&b); } } /* ** WEBPAGE: chat-poll ** ** The chat page generated by /chat using a XHR to this page in order ** to ask for new chat content. The "name" argument should begin with ** an integer which is the largest "msgid" that the chat page currently ** holds. If newer content is available, this routine returns that ** content straight away. If no new content is available, this webpage ** blocks until the new content becomes available. In this way, the ** system implements "hanging-GET" or "long-poll" style event notification. ** ** The reply from this webpage is JSON that describes the new content. ** Format of the json: ** ** | { ** | "msg":[ ** | { ** | "msgid": integer // message id ** | "mtime": text // When sent: YYYY-MM-DD HH:MM:SS UTC ** | "xfrom": text // Login name of sender ** | "uclr": text // Color string associated with the user ** | "xmsg": text // HTML text of the message ** | "fsize": integer // file attachment size in bytes ** | "fname": text // Name of file attachment ** | "fmime": text // MIME-type of file attachment ** | } ** | ] ** | } ** ** The "fname" and "fmime" fields are only present if "fsize" is greater ** than zero. The "xmsg" field may be an empty string if "fsize" is zero. ** ** The "msgid" values will be in increasing order. */ void chat_poll_webpage(void){ Blob json; /* The json to be constructed and returned */ sqlite3_int64 dataVersion; /* Data version. Used for polling. */ sqlite3_int64 newDataVers; int iDelay = 1000; /* Delay until next poll (milliseconds) */ const char *zSep = "{\"msgs\":[\n"; /* List separator */ int msgid = atoi(PD("name","0")); Stmt q1; login_check_credentials(); if( !g.perm.Chat ) return; chat_create_tables(); cgi_set_content_type("text/json"); dataVersion = db_int64(0, "PRAGMA data_version"); db_prepare(&q1, "SELECT msgid, datetime(mtime), xfrom, xmsg, length(file), fname, fmime" " FROM chat" " WHERE msgid>%d" " ORDER BY msgid", msgid ); blob_init(&json, 0, 0); while(1){ int cnt = 0; while( db_step(&q1)==SQLITE_ROW ){ int id = db_column_int(&q1, 0); const char *zDate = db_column_text(&q1, 1); const char *zFrom = db_column_text(&q1, 2); const char *zRawMsg = db_column_text(&q1, 3); const int nByte = db_column_int(&q1, 4); const char *zFName = db_column_text(&q1, 5); const char *zFMime = db_column_text(&q1, 6); char *zMsg; cnt++; blob_append(&json, zSep, -1); zSep = ",\n"; blob_appendf(&json, "{\"msgid\":%d,\"mtime\":%!j,", id, zDate); blob_appendf(&json, "\"xfrom\":%!j,", zFrom); blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom)); /* TBD: Convert the raw message into HTML, perhaps by running it ** through a text formatter, or putting markup on @name phrases, ** etc. */ zMsg = mprintf("%h", zRawMsg); blob_appendf(&json, "\"xmsg\":%!j,", zMsg); fossil_free(zMsg); if( nByte==0 ){ blob_appendf(&json, "\"fsize\":0}"); }else{ blob_appendf(&json, "\"fsize\":%d,\"fname\":%!j,\"fmime\":%!j}", nByte, zFName, zFMime); } } if( cnt ){ blob_append(&json, "\n]}", 3); cgi_set_content(&json); break; } sqlite3_sleep(iDelay); while( (newDataVers = db_int64(0,"PRAGMA data_version"))==dataVersion ){ sqlite3_sleep(iDelay); } dataVersion = newDataVers; } /* Exit by "break" */ db_finalize(&q1); return; } /* ** WEBPAGE: chat-download ** ** Download the CHAT.FILE attachment associated with a single chat ** entry. The "name" query parameter begins with an integer that ** identifies the particular chat message. */ void chat_download_webpage(void){ int msgid; Blob r; const char *zMime; login_check_credentials(); if( !g.perm.Chat ){ style_header("Chat Not Authorized"); @

Not Authorized

@

You do not have permission to use the chatroom on this @ repository.

style_finish_page(); return; } chat_create_tables(); msgid = atoi(PD("name","0")); blob_zero(&r); zMime = db_text(0, "SELECT fmime FROM chat wHERE msgid=%d", msgid); if( zMime==0 ) return; db_blob(&r, "SELECT file FROM chat WHERE msgid=%d", msgid); cgi_set_content_type(zMime); cgi_set_content(&r); }