/* ** 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" /* Settings that can be used to control chat */ /* ** SETTING: chat-initial-history width=10 default=50 ** ** If this setting has an integer value of N, then when /chat first ** starts up it initializes the screen with the N most recent chat ** messages. If N is zero, then all chat messages are loaded. */ /* ** SETTING: chat-keep-count width=10 default=50 ** ** When /chat is cleaning up older messages, it will always keep ** the most recent chat-keep-count messages, even if some of those ** messages are older than the discard threshold. If this value ** is zero, then /chat is free to delete all historic messages once ** they are old enough. */ /* ** SETTING: chat-keep-days width=10 default=7 ** ** The /chat subsystem will try to discard messages that are older then ** chat-keep-days. The value of chat-keep-days can be a floating point ** number. So, for example, if you only want to keep chat messages for ** 12 hours, set this value to 0.5. ** ** A value of 0.0 or less means that messages are retained forever. */ /* ** WEBPAGE: chat ** ** Start up a browser-based chat session. ** ** This is the main page that humans use to access the chatroom. Simply ** point a web-browser at /chat and the screen fills with the latest ** chat messages, and waits for new one. ** ** Other /chat-OP pages are used by XHR requests from this page to ** send new chat message, delete older messages, or poll for changes. */ void chat_webpage(void){ int iPingTcp; login_check_credentials(); if( !g.perm.Chat ){ login_needed(g.anon.Chat); return; } iPingTcp = atoi(PD("ping","0")); if( iPingTcp<1000 || iPingTcp>65535 ) iPingTcp = 0; style_set_current_feature("chat"); style_header("Chat"); @ @
@
@
@ @ @
@
@
@
@ Select a file to upload, 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, -- Time for this entry - Julianday Zulu @ xfrom TEXT, -- Login of the sender @ xmsg TEXT, -- Raw, unformatted text of the message @ file BLOB, -- Text of the uploaded file, or NULL @ fname TEXT, -- Filename of the uploaded file, or NULL @ fmime TEXT, -- MIMEType of the upload file, or NULL @ mdel INT -- msgid of another message to delete @ ); ; /* ** 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:""*/); }else if( !db_table_has_column("repository","chat","mdel") ){ db_multi_exec("ALTER TABLE chat ADD COLUMN mdel INT"); } } /* ** 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); } } /* ** This routine receives raw (user-entered) message text and transforms ** it into HTML that is safe to insert using innerHTML. ** ** * HTML in the original text is escaped. ** ** * Hyperlinks are identified and tagged. Hyperlinks are: ** ** - Undelimited text of the form https:... or http:... ** - Any text enclosed within [...] ** ** Space to hold the returned string is obtained from fossil_malloc() ** and must be freed by the caller. */ static char *chat_format_to_html(const char *zMsg){ char *zSafe = mprintf("%h", zMsg); int i, j, k; Blob out; char zClose[20]; blob_init(&out, 0, 0); for(i=j=0; zSafe[i]; i++){ if( zSafe[i]=='[' ){ for(k=i+1; zSafe[k] && zSafe[k]!=']'; k++){} if( zSafe[k]==']' ){ zSafe[k] = 0; if( ji+7 ){ char c = zSafe[k]; if( !fossil_isalnum(zSafe[k-1]) && zSafe[k-1]!='/' ){ k--; c = zSafe[k]; } if( j%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); int nByte = db_column_int(&q1, 4); const char *zFName = db_column_text(&q1, 5); const char *zFMime = db_column_text(&q1, 6); int iToDel = db_column_int(&q1, 7); 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)); zMsg = chat_format_to_html(zRawMsg ? 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( iToDel ){ blob_appendf(&json, ",\"mdel\":%d}", iToDel); }else{ blob_append(&json, "}", 1); } } db_reset(&q1); if( cnt ){ blob_append(&json, "\n]}", 3); cgi_set_content(&json); break; } sqlite3_sleep(iDelay); while( 1 ){ sqlite3_int64 newDataVers = db_int64(0,"PRAGMA repository.data_version"); if( newDataVers!=dataVersion ){ dataVersion = newDataVers; break; } sqlite3_sleep(iDelay); } } /* 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); } /* ** WEBPAGE: chat-delete ** ** Delete the chat entry identified by the name query parameter. ** Invoking fetch("chat-delete/"+msgid) from javascript in the client ** will delete a chat entry from the CHAT table. ** ** This routine both deletes the identified chat entry and also inserts ** a new entry with the current timestamp and with: ** ** * xmsg = NULL ** * file = NULL ** * mdel = The msgid of the row that was deleted ** ** This new entry will then be propagated to all listeners so that they ** will know to delete their copies of the message too. */ void chat_delete_webpage(void){ int mdel; char *zOwner; login_check_credentials(); if( !g.perm.Chat ) return; chat_create_tables(); mdel = atoi(PD("name","0")); zOwner = db_text(0, "SELECT xfrom FROM chat WHERE msgid=%d", mdel); if( zOwner==0 ) return; if( fossil_strcmp(zOwner, g.zLogin)!=0 && !g.perm.Admin ) return; db_multi_exec( "BEGIN;\n" "DELETE FROM chat WHERE msgid=%d;\n" "INSERT INTO chat(mtime, xfrom, mdel)" " VALUES(julianday('now'), %Q, %d);\n" "COMMIT;", mdel, g.zLogin, mdel ); } /* ** WEBPAGE: chat-ping ** ** HTTP requests coming to this page from a loopback IP address cause ** a single \007 (bel) character to be written on the controlling TTY. ** This is used to implement an audiable alert by local web clients. */ void chat_ping_webpage(void){ const char *zIpAddr = PD("REMOTE_ADDR","nil"); if( cgi_is_loopback(zIpAddr) ) fputc(7, stderr); }