├── src ├── media │ ├── utils.c │ ├── media.h │ ├── posix_queue.c │ └── storage.c ├── notification │ ├── notification.h │ ├── gen.c │ ├── eve.c │ └── connection.c ├── websocket │ ├── slot.c │ ├── record.c │ └── auth.c └── shared │ ├── mapping.c │ ├── log.c │ ├── aux.c │ └── buffer.c ├── scripts ├── 007-pack-distribution.sh ├── 008-deploy-distribution.sh ├── 100-run-notification-debug.sh ├── 001-build-websocket-debug.sh ├── 002-build-websocket-release.sh ├── 003-build-media-debug.sh ├── 004-build-media-release.sh ├── 005-build-notification-debug.sh ├── telegram-import │ └── telegram_import_attachments.py ├── 006-build-notification-release.sh └── 000-build-toolchain.sh ├── .gitignore ├── public ├── images │ ├── 16.png │ ├── 16a.png │ ├── 32.png │ ├── 32a.png │ ├── 64.png │ ├── logo-s.png │ ├── ph.svg │ └── avatar_placeholder.svg ├── icons │ ├── perfect-bullet.png │ ├── maskable_icon_x192.png │ ├── maskable_icon_x512.png │ ├── perfect-bullet-192.png │ ├── perfect-bullet-512.png │ ├── perfect-attach.svg │ ├── tick.svg │ ├── down-arrow.svg │ ├── send.svg │ ├── video.svg │ ├── attach.svg │ ├── attach_ccc.svg │ ├── cross.svg │ ├── reply.svg │ ├── search.svg │ ├── search_333.svg │ ├── search_ccc.svg │ ├── tick2.svg │ ├── archive.svg │ ├── perfect_attach.svg │ ├── file.svg │ ├── plus.svg │ ├── cancel.svg │ ├── play.svg │ ├── goto.svg │ ├── failed.svg │ ├── more.svg │ ├── cancel_333.svg │ ├── pencil_new.svg │ ├── upload.svg │ ├── perfect-search.svg │ ├── audio.svg │ ├── pencil.svg │ ├── burger.svg │ ├── burger_ccc.svg │ ├── perfect-bullet.svg │ ├── bullet.svg │ ├── leftarrow.svg │ ├── perfect-burger.svg │ ├── perfect-white-burger.svg │ ├── bullet_black.svg │ ├── bullet_ccc_straight.svg │ ├── bullet_333.svg │ ├── bullet_ccc.svg │ ├── react2.svg │ ├── settings.svg │ ├── settings_333.svg │ ├── settings_ccc.svg │ ├── bullet_mark.svg │ ├── trash.svg │ ├── pin_aaa.svg │ ├── calendar.svg │ ├── people.svg │ ├── perfect-people.svg │ ├── pin.svg │ └── react.svg ├── sounds │ └── notification.wav ├── styles │ ├── fonts │ │ ├── Inter-Regular.woff │ │ ├── Inter-SemiBold.woff │ │ └── RobotoMono-Regular.ttf │ ├── login.css │ ├── dark.css │ └── settings.css ├── scripts │ ├── service-worker.js │ ├── config.js │ ├── mobile.js │ ├── upload.log │ ├── threads.js │ ├── spinner.js │ ├── search.js │ ├── settings.js │ ├── login.js │ └── notifications.js ├── manifest.json └── html │ ├── admin.html │ └── login.html ├── config ├── example.js └── Caddyfile ├── docs └── sources.txt ├── LICENCE └── telegram-import └── telegram_import_attachments.py /src/media/utils.c: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/007-pack-distribution.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/008-deploy-distribution.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /data 3 | /uploads 4 | /toolchain 5 | -------------------------------------------------------------------------------- /public/images/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/16.png -------------------------------------------------------------------------------- /public/images/16a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/16a.png -------------------------------------------------------------------------------- /public/images/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/32.png -------------------------------------------------------------------------------- /public/images/32a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/32a.png -------------------------------------------------------------------------------- /public/images/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/64.png -------------------------------------------------------------------------------- /public/images/logo-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/images/logo-s.png -------------------------------------------------------------------------------- /public/icons/perfect-bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/icons/perfect-bullet.png -------------------------------------------------------------------------------- /public/sounds/notification.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/sounds/notification.wav -------------------------------------------------------------------------------- /public/icons/maskable_icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/icons/maskable_icon_x192.png -------------------------------------------------------------------------------- /public/icons/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/icons/maskable_icon_x512.png -------------------------------------------------------------------------------- /public/icons/perfect-bullet-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/icons/perfect-bullet-192.png -------------------------------------------------------------------------------- /public/icons/perfect-bullet-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/icons/perfect-bullet-512.png -------------------------------------------------------------------------------- /public/styles/fonts/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/styles/fonts/Inter-Regular.woff -------------------------------------------------------------------------------- /public/styles/fonts/Inter-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/styles/fonts/Inter-SemiBold.woff -------------------------------------------------------------------------------- /public/styles/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aolo2/chat/HEAD/public/styles/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /scripts/100-run-notification-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | LD_LIBRARY_PATH="$(realpath $SCRIPT_DIR/../toolchain/gcc):$LD_LIBRARY_PATH" $SCRIPT_DIR/../build/notification-server-debug "$@" 5 | -------------------------------------------------------------------------------- /public/images/ph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I 5 | 6 | -------------------------------------------------------------------------------- /public/images/avatar_placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/scripts/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', (event) => { 2 | event.waitUntil(self.skipWaiting()); // Activate worker immediately 3 | }); 4 | 5 | self.addEventListener('activate', (event) => { 6 | event.waitUntil(self.clients.claim()); // Become available to all pages 7 | }); 8 | 9 | self.addEventListener('push', (event) => { 10 | console.log('push'); 11 | }) -------------------------------------------------------------------------------- /config/example.js: -------------------------------------------------------------------------------- 1 | const CONFIG_WS_URL = 'wss://localhost/ws/'; 2 | const CONFIG_WS_INITIAL_TIMEOUT = 1000; 3 | const CONFIG_WS_RESEND_INTERVAL = 10000; 4 | const CONFIG_MEDIA_URL = 'localhost/upload'; 5 | const CONFIG_STORAGE_SAVE_INTERVAL = 30000; 6 | const CONFIG_MESSAGE_PLACEHOLDER_DELAY = 500; 7 | const CONFIG_DEBUG_PRINT = false; 8 | const CONFIG_SEEN_TIMEOUT = 1000; 9 | const CONFIG_TOAST_DELAY_MS = 100; 10 | const CONFIG_IMAGE_TRIES = 3; 11 | const CONFIG_SW_SCOPE = 'https://localhost'; 12 | -------------------------------------------------------------------------------- /public/scripts/config.js: -------------------------------------------------------------------------------- 1 | const CONFIG_WS_URL = 'wss://localhost/ws/'; 2 | const CONFIG_WS_INITIAL_TIMEOUT = 1000; 3 | const CONFIG_WS_RESEND_INTERVAL = 10000; 4 | const CONFIG_MEDIA_URL = 'localhost/upload'; 5 | const CONFIG_STORAGE_SAVE_INTERVAL = 30000; 6 | const CONFIG_MESSAGE_PLACEHOLDER_DELAY = 500; 7 | const CONFIG_DEBUG_PRINT = false; 8 | const CONFIG_SEEN_TIMEOUT = 1000; 9 | const CONFIG_TOAST_DELAY_MS = 100; 10 | const CONFIG_IMAGE_TRIES = 3; 11 | const CONFIG_SW_SCOPE = 'https://localhost'; 12 | -------------------------------------------------------------------------------- /public/icons/perfect-attach.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /config/Caddyfile: -------------------------------------------------------------------------------- 1 | localhost { 2 | encode zstd gzip 3 | 4 | header { 5 | Service-Worker-Allowed / 6 | } 7 | 8 | handle_path /static/* { 9 | root * /code/chat-mit/public 10 | file_server 11 | } 12 | 13 | handle_path /storage/* { 14 | root * /code/chat-mit/uploads 15 | file_server 16 | } 17 | 18 | handle_path /notifications/* { 19 | reverse_proxy 127.0.0.1:7222 20 | } 21 | 22 | handle_path /upload/* { 23 | reverse_proxy 127.0.0.1:7000 24 | request_body { 25 | max_size 500MB 26 | } 27 | } 28 | 29 | handle_path /ws/* { 30 | reverse_proxy 127.0.0.1:7271 31 | } 32 | 33 | handle { 34 | root * /code/chat-mit/public/html 35 | file_server 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/001-build-websocket-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/websocket 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 10 | 11 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 12 | EXECUTABLE_NAME=ws-server-debug 13 | 14 | CFLAGS+=" -O0 -g " 15 | CFLAGS+=" -Wno-unused-function " 16 | CFLAGS+=" -I$LIBURING_DIR/src/include " 17 | #CFLAGS+=" -fsanitize=address,undefined " 18 | 19 | LDFLAGS=" -L$LIBURING_DIR/src " 20 | LDFLAGS+=" -luring -lcrypt " 21 | 22 | mkdir -p $BUILD_DIR 23 | pushd $SRC_DIR 24 | 25 | # cppcheck main.c 26 | gcc main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 27 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 28 | 29 | popd 30 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bullet.Chat PWA", 3 | "short_name": "Bullet.Chat", 4 | "description": "Fast and competent internal chat", 5 | "id": "bullet-chat.mobile.pwa", 6 | "icons": [ 7 | { 8 | "src": "icons/perfect-bullet.svg", 9 | "type": "image/svg+xml", 10 | "purpose": "any", 11 | "sizes": "any" 12 | }, 13 | { 14 | "src": "icons/maskable_icon_x512.png", 15 | "type": "image/png", 16 | "purpose": "maskable", 17 | "sizes": "512x512" 18 | }, 19 | { 20 | "src": "icons/maskable_icon_x192.png", 21 | "type": "image/png", 22 | "purpose": "maskable", 23 | "sizes": "192x192" 24 | } 25 | ], 26 | "start_url": "https://localhost/", 27 | "display": "fullscreen", 28 | "theme_color": "#2f343d", 29 | "background_color": "#2f343d", 30 | "orientation": "portrait-primary" 31 | } 32 | -------------------------------------------------------------------------------- /scripts/002-build-websocket-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/websocket 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 10 | CC=$TOOLCHAIN_DIR/x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc 11 | 12 | 13 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 14 | EXECUTABLE_NAME=ws-server-release 15 | 16 | CFLAGS+=" -g -static " 17 | CFLAGS+=" -Wno-unused-function " 18 | CFLAGS+=" -I$LIBURING_DIR/src/include " 19 | 20 | LDFLAGS=" -L$LIBURING_DIR/src " 21 | LDFLAGS+=" -luring -lcrypt " 22 | 23 | mkdir -p $BUILD_DIR 24 | pushd $SRC_DIR 25 | 26 | # cppcheck main.c 27 | $CC main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 28 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 29 | 30 | popd 31 | -------------------------------------------------------------------------------- /scripts/003-build-media-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/media 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 10 | 11 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 12 | EXECUTABLE_NAME=media-server-debug 13 | 14 | CFLAGS+=" -O0 -g " 15 | CFLAGS+=" -Wno-unused-function -Wno-discarded-qualifiers -Wno-bad-function-cast -Wno-float-equal " 16 | CFLAGS+=" -I$LIBURING_DIR/src/include " 17 | #CFLAGS+=" -fsanitize=address,undefined " 18 | 19 | LDFLAGS=" -L$LIBURING_DIR/src " 20 | LDFLAGS+=" -luring -lm " 21 | 22 | mkdir -p $BUILD_DIR 23 | pushd $SRC_DIR 24 | 25 | # cppcheck main.c 26 | gcc main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 27 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 28 | popd 29 | -------------------------------------------------------------------------------- /scripts/004-build-media-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/media 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 10 | CC=$TOOLCHAIN_DIR/x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc 11 | 12 | 13 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 14 | EXECUTABLE_NAME=media-server-release 15 | 16 | CFLAGS+=" -g -static " 17 | CFLAGS+=" -Wno-unused-function -Wno-discarded-qualifiers -Wno-bad-function-cast -Wno-float-equal" 18 | CFLAGS+=" -I$LIBURING_DIR/src/include " 19 | 20 | LDFLAGS=" -L$LIBURING_DIR/src " 21 | LDFLAGS+=" -luring -lm " 22 | 23 | mkdir -p $BUILD_DIR 24 | pushd $SRC_DIR 25 | 26 | # cppcheck main.c 27 | $CC main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 28 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 29 | 30 | popd 31 | -------------------------------------------------------------------------------- /docs/sources.txt: -------------------------------------------------------------------------------- 1 | io_uring: https://manpages.debian.org/unstable/liburing-dev/io_uring_enter.2.en.html 2 | Websocket RFC: https://datatracker.ietf.org/doc/html/rfc6455 3 | Bytes -> UTF8: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder 4 | UTF8 -> Bytes: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder 5 | IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API 6 | HTTP 1.1 RFC: https://datatracker.ietf.org/doc/html/rfc2616 7 | Offline/Online: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine 8 | SYN/ACK: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_segment_structure 9 | 10 | Separate mmap reserve and commit: https://stackoverflow.com/questions/2782628/any-way-to-reserve-but-not-commit-memory-in-linux 11 | 12 | mmap challenges (Sublime): 13 | https://www.sublimetext.com/blog/articles/use-mmap-with-care 14 | 15 | Resumable file upload: https://javascript.info/resume-upload 16 | CTRL+V pictures: https://stackoverflow.com/a/6338207/11420590 -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright G.Silverstov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /telegram-import/telegram_import_attachments.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import requests 4 | import json 5 | 6 | MEDIA_URL_BASE = 'https://bullet-chat.local/upload/' 7 | 8 | if __name__ == '__main__': 9 | if len(sys.argv) != 2: 10 | print(f'Usage: {sys.argv[0]} file_directory') 11 | sys.exit(1) 12 | 13 | print('TODO: CALCULATE FILE EXT LOCALLY') 14 | sys.exit(1) 15 | 16 | directory = sys.argv[1] 17 | 18 | result = [] 19 | 20 | for file in os.listdir(directory): 21 | filename = os.fsdecode(file) 22 | fullpath = os.path.join(directory, filename) 23 | resp = requests.post(MEDIA_URL_BASE + 'upload-req') 24 | file_id = resp.headers['x-file-id'] 25 | files = { 'file': open(fullpath, 'rb') } 26 | headers = {'X-File-Id': file_id} 27 | resp = requests.post(MEDIA_URL_BASE + 'upload-do', files=files, headers=headers) 28 | ext = resp.headers['x-file-ext'] 29 | 30 | result.append({ 31 | 'filename': filename, 32 | 'file_id': file_id, 33 | 'ext': ext, 34 | }) 35 | 36 | print(json.dumps(result, indent=4, ensure_ascii=False)) 37 | -------------------------------------------------------------------------------- /scripts/005-build-notification-debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/notification 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | OPENSSL_DIR=$(realpath $TOOLCHAIN_DIR/openssl-1.1.1v) 10 | CURL_DIR=$(realpath $TOOLCHAIN_DIR/curl-8.2.1) 11 | ECEC_DIR=$(realpath $TOOLCHAIN_DIR/ecec-master) 12 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 13 | LIBRARIES=$(realpath $TOOLCHAIN_DIR/gcc) 14 | 15 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 16 | EXECUTABLE_NAME=notification-server-debug 17 | 18 | CFLAGS+=" -O0 -g " 19 | CFLAGS+=" -Wno-unused-function " 20 | #CFLAGS+=" -fsanitize=address,undefined " 21 | CFLAGS+=" -I$ECEC_DIR/include -I$OPENSSL_DIR/include -I$CURL_DIR/include -I$LIBURING_DIR/src/include " 22 | 23 | LDFLAGS=" -L$LIBRARIES " 24 | LDFLAGS+=" -lcurl -lcrypto -lssl -lecec -luring -lpthread " 25 | 26 | mkdir -p $BUILD_DIR 27 | pushd $SRC_DIR 28 | 29 | # cppcheck main.c 30 | gcc main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 31 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 32 | 33 | popd 34 | -------------------------------------------------------------------------------- /scripts/telegram-import/telegram_import_attachments.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import requests 4 | import json 5 | 6 | MEDIA_URL_BASE = 'https://bullet-chat.local/upload/' 7 | 8 | if __name__ == '__main__': 9 | if len(sys.argv) != 2: 10 | print(f'Usage: {sys.argv[0]} file_directory') 11 | sys.exit(1) 12 | 13 | print('TODO: CALCULATE FILE EXT LOCALLY') 14 | sys.exit(1) 15 | 16 | directory = sys.argv[1] 17 | 18 | result = [] 19 | 20 | for file in os.listdir(directory): 21 | filename = os.fsdecode(file) 22 | fullpath = os.path.join(directory, filename) 23 | resp = requests.post(MEDIA_URL_BASE + 'upload-req') 24 | file_id = resp.headers['x-file-id'] 25 | files = { 'file': open(fullpath, 'rb') } 26 | headers = {'X-File-Id': file_id} 27 | resp = requests.post(MEDIA_URL_BASE + 'upload-do', files=files, headers=headers) 28 | ext = resp.headers['x-file-ext'] 29 | 30 | result.append({ 31 | 'filename': filename, 32 | 'file_id': file_id, 33 | 'ext': ext, 34 | }) 35 | 36 | print(json.dumps(result, indent=4, ensure_ascii=False)) 37 | -------------------------------------------------------------------------------- /src/notification/notification.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #define DEBUG_PRINT 1 15 | #define TIMEOUT_INTERVAL 5 16 | #define SUB_EXP_PADDING 60 17 | #define SUB_LENGTH_SECONDS (60 * 60 * 12) 18 | 19 | #define MAX_POSSIBLE_VAPID_CREDS 10000 20 | 21 | struct bc_subscription { 22 | u32 user_id; 23 | 24 | struct bc_str endpoint; 25 | struct bc_str p256dh; 26 | struct bc_str auth; 27 | }; 28 | 29 | struct bc_vapid { 30 | char *token; 31 | struct bc_str aud; /* "audience" - push service protocol://host */ 32 | struct bc_str sub; /* contact email */ 33 | u32 exp; 34 | }; 35 | 36 | struct bc_notifier { 37 | int fd; 38 | 39 | struct bc_vapid *vapid; 40 | struct bc_queue queue; 41 | struct bc_connection *connections; 42 | }; 43 | 44 | /* For passing to pthread_create */ 45 | struct bc_notification_work { 46 | char *vapid_token; 47 | struct bc_subscription subscription; 48 | u8 *payload; 49 | int payload_length; 50 | }; -------------------------------------------------------------------------------- /scripts/006-build-notification-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 6 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 7 | SRC_DIR=$SCRIPT_DIR/../src/notification 8 | BUILD_DIR=$SCRIPT_DIR/../build 9 | OPENSSL_DIR=$(realpath $TOOLCHAIN_DIR/openssl-1.1.1v) 10 | CURL_DIR=$(realpath $TOOLCHAIN_DIR/curl-8.2.1) 11 | ECEC_DIR=$(realpath $TOOLCHAIN_DIR/ecec-master) 12 | LIBURING_DIR=$(realpath $TOOLCHAIN_DIR/liburing-liburing-2.4) 13 | LIBRARIES=$(realpath $TOOLCHAIN_DIR/musl) 14 | CC=$TOOLCHAIN_DIR/x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc 15 | 16 | CFLAGS=" -std=gnu11 -Wall -Wextra -Werror " 17 | EXECUTABLE_NAME=notification-server-release 18 | 19 | CFLAGS+=" -g -static " 20 | CFLAGS+=" -Wno-unused-function " 21 | #CFLAGS+=" -fsanitize=address,undefined " 22 | CFLAGS+=" -I$LIBURING_DIR/src/include -I$ECEC_DIR/include -I$OPENSSL_DIR/include -I$CURL_DIR/include -I$LIBRARIES " 23 | 24 | LDFLAGS=" -L$LIBRARIES " 25 | LDFLAGS+=" -luring -lcurl -lssl -lcrypto -lece -lpthread " 26 | 27 | mkdir -p $BUILD_DIR 28 | pushd $SRC_DIR 29 | 30 | #cppcheck main.c 31 | $CC main.c $CFLAGS -o $BUILD_DIR/$EXECUTABLE_NAME $LDFLAGS 32 | 33 | echo "Built executable $(realpath $BUILD_DIR/$EXECUTABLE_NAME)" 34 | 35 | popd 36 | -------------------------------------------------------------------------------- /src/notification/gen.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int 4 | main() { 5 | // The subscription private key. This key should never be sent to the app 6 | // server. It should be persisted with the endpoint and auth secret, and used 7 | // to decrypt all messages sent to the subscription. 8 | uint8_t rawRecvPrivKey[ECE_WEBPUSH_PRIVATE_KEY_LENGTH]; 9 | 10 | // The subscription public key. This key should be sent to the app server, 11 | // and used to encrypt messages. The Push DOM API exposes the public key via 12 | // `pushSubscription.getKey("p256dh")`. 13 | uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; 14 | 15 | // The shared auth secret. This secret should be persisted with the 16 | // subscription information, and sent to the app server. The DOM API exposes 17 | // the auth secret via `pushSubscription.getKey("auth")`. 18 | uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; 19 | 20 | int err = ece_webpush_generate_keys( 21 | rawRecvPrivKey, ECE_WEBPUSH_PRIVATE_KEY_LENGTH, rawRecvPubKey, 22 | ECE_WEBPUSH_PUBLIC_KEY_LENGTH, authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); 23 | if (err) { 24 | return 1; 25 | } 26 | 27 | return 0; 28 | } -------------------------------------------------------------------------------- /src/media/media.h: -------------------------------------------------------------------------------- 1 | #include /* mq_open, mq_send, mq_receive, mq_close, mq_unlink */ 2 | #include /* EAGAIN */ 3 | 4 | #define DEBUG_PRINT 1 5 | #define IMAGE_PROCESS_COUNT 6 6 | #define MQ_NAME "/preview_image" 7 | #define MQ_MAX_MSG 10 8 | 9 | #define STBI_NO_PSD 10 | #define STBI_NO_TGA 11 | #define STBI_NO_HDR 12 | #define STBI_NO_PIC 13 | #define STBI_NO_PNM 14 | 15 | #define STBIR_ASSERT(boolval) 16 | 17 | #define PREVIEW_LVL0 768 18 | #define PREVIEW_LVL1 256 19 | #define PREVIEW_LVL2 128 20 | #define PREVIEW_LVL3 32 21 | 22 | static const int PREVIEW_LEVELS[] = { PREVIEW_LVL0, PREVIEW_LVL1, PREVIEW_LVL2, PREVIEW_LVL3 }; 23 | 24 | enum bc_extension { 25 | EXT_OTHER = 0, 26 | 27 | EXT_JPEG, 28 | EXT_PNG, 29 | EXT_BMP, 30 | EXT_GIF, 31 | 32 | EXT_AUDIO, 33 | EXT_VIDEO, 34 | EXT_IMAGE, 35 | EXT_IMAGE_VECTOR, 36 | EXT_TEXT, 37 | EXT_ARCHIVE, 38 | }; 39 | 40 | enum mq_msg_type { 41 | START_GENERATE_PREVIEW = 1, 42 | }; 43 | 44 | struct bc_image { 45 | unsigned char *data; 46 | char filename[16 + 1]; 47 | int width; 48 | int height; 49 | int comps; 50 | }; 51 | 52 | struct bc_server { 53 | int fd; 54 | int mq_desc; 55 | struct bc_queue queue; 56 | struct bc_connection *connections; 57 | }; 58 | 59 | struct mq_message { 60 | enum mq_msg_type type; 61 | u64 file_id; 62 | }; -------------------------------------------------------------------------------- /public/scripts/mobile.js: -------------------------------------------------------------------------------- 1 | const LONG_TOUCH = 500; 2 | 3 | let long_touch_timers = {}; 4 | 5 | function HTML(html) { 6 | const template = document.createElement('template'); 7 | template.innerHTML = html.trim(); 8 | return template.content.firstChild; 9 | } 10 | 11 | function start_touch(e, on_tap, on_hold, button_id) { 12 | const item = e.target; 13 | 14 | item.classList.add('active'); 15 | 16 | const clear_touch = (e) => { 17 | clearTimeout(long_touch_timers[button_id]); 18 | 19 | if (on_tap) { 20 | item.removeEventListener('touchend', on_tap); 21 | } 22 | 23 | item.classList.remove('active'); 24 | item.removeEventListener('touchend', clear_touch); 25 | item.removeEventListener('touchmove', clear_touch); 26 | item.removeEventListener('touchcancel', clear_touch); 27 | } 28 | 29 | long_touch_timers[button_id] = setTimeout(() => { 30 | if (on_hold) on_hold(e); 31 | clear_touch(e); // e? 32 | }, LONG_TOUCH); 33 | 34 | if (on_tap) item.addEventListener('touchend', on_tap); 35 | 36 | item.addEventListener('touchend', clear_touch); 37 | item.addEventListener('touchmove', clear_touch); 38 | item.addEventListener('touchcancel', clear_touch); 39 | } 40 | 41 | function make_button(item, on_tap = null, on_hold = null) { 42 | const button_id = random_string(); 43 | item.addEventListener('touchstart', (e) => start_touch(e, on_tap, on_hold, button_id)); 44 | } -------------------------------------------------------------------------------- /public/scripts/upload.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 20 APR 2023 16:41 2 | entering extended mode 3 | restricted \write18 enabled. 4 | %&-line parsing enabled. 5 | **/code/work/cobalt-chat/public/scripts/upload.js 6 | (/code/work/cobalt-chat/public/scripts/upload.js 7 | LaTeX2e <2022-11-01> patch level 1 8 | L3 programming layer <2023-01-16> 9 | 10 | ! LaTeX Error: Missing \begin{document}. 11 | 12 | See the LaTeX manual or LaTeX Companion for explanation. 13 | Type H for immediate help. 14 | ... 15 | 16 | l.1 c 17 | onst ATTACHMENT_TYPE = { 18 | ? 19 | ! Emergency stop. 20 | ... 21 | 22 | l.1 c 23 | onst ATTACHMENT_TYPE = { 24 | You're in trouble here. Try typing to proceed. 25 | If that doesn't work, type X to quit. 26 | 27 | 28 | Here is how much of TeX's memory you used: 29 | 19 strings out of 476091 30 | 632 string characters out of 5794081 31 | 1849330 words of memory out of 5000000 32 | 20499 multiletter control sequences out of 15000+600000 33 | 512287 words of font info for 32 fonts, out of 8000000 for 9000 34 | 1141 hyphenation exceptions out of 8191 35 | 13i,0n,12p,105b,14s stack positions out of 10000i,1000n,20000p,200000b,200000s 36 | ! ==> Fatal error occurred, no output PDF file produced! 37 | -------------------------------------------------------------------------------- /public/icons/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/websocket/slot.c: -------------------------------------------------------------------------------- 1 | static int 2 | slot_reserve(struct bc_slots *slots) 3 | { 4 | for (int i = 0; i < slots->count; ++i) { 5 | struct bc_slot *slot = slots->occupancy + i; 6 | if (!slot->taken) { 7 | if (!slot->commited) { 8 | if (!mapping_commit(&slots->vm, i * slots->slot_size, slots->slot_size)) { 9 | return(-1); 10 | } 11 | 12 | slot->commited = true; 13 | } 14 | 15 | slot->taken = true; 16 | 17 | return(i); 18 | } 19 | } 20 | 21 | return(-1); 22 | } 23 | 24 | static struct bc_str 25 | slot_buffer(struct bc_slots *slots, int slot_id) 26 | { 27 | struct bc_str result = { 0 }; 28 | 29 | result.data = (char *) slots->vm.base + slot_id * slots->slot_size; 30 | result.length = slots->slot_size; 31 | 32 | return(result); 33 | } 34 | 35 | static void 36 | slot_free(struct bc_slots *slots, int slot_id) 37 | { 38 | slots->occupancy[slot_id].taken = false; 39 | } 40 | 41 | static bool 42 | slot_init(struct bc_slots *slots, int nslots, int slot_size) 43 | { 44 | u64 size = round_up_to_page_size(nslots * slot_size); 45 | slots->vm = mapping_reserve(size); 46 | 47 | if (!slots->vm.size) { 48 | log_ferror(__func__, "Failed to create slot mapping\n"); 49 | return(false); 50 | } 51 | 52 | slots->occupancy = buffer_init(nslots, sizeof(struct bc_slot)); 53 | slots->slot_size = slot_size; 54 | slots->count = nslots; 55 | 56 | for (int i = 0; i < nslots; ++i) { 57 | struct bc_slot slot = { 0 }; 58 | buffer_push(slots->occupancy, slot); 59 | } 60 | 61 | return(true); 62 | } -------------------------------------------------------------------------------- /public/scripts/threads.js: -------------------------------------------------------------------------------- 1 | let thread_opened = false; 2 | let opened_thread_id = null; 3 | let thread_root = null; 4 | let thread_content_block = null; 5 | function thread_open(thread_id) { 6 | thread_opened = true; 7 | opened_thread_id = thread_id; 8 | const channel_id = ls_get_current_channel_id(); 9 | thread_content_block.set_channel(channel_id); 10 | thread_content_block.set_thread(opened_thread_id); 11 | thread_show(); 12 | } 13 | 14 | function thread_close() { 15 | thread_opened = false; 16 | opened_thread_id = null; 17 | thread_hide(); 18 | } 19 | 20 | function thread_init(root) { 21 | thread_root = root; 22 | let uploader_hidden_btn = root.querySelector('.upload-hidden-button'); 23 | thread_content_block = new ContentBlock({ 24 | scroller_root: root.querySelector('.scroller-container'), 25 | message_input_root: root.querySelector('.message-input'), 26 | block_root: root, 27 | uploader_btn: uploader_hidden_btn, 28 | drag_indicator: root.querySelector('.message-input-drag-indicator') 29 | }) 30 | thread_content_block.init(); 31 | let back_button = root.querySelector('.header-back-btn'); 32 | back_button.addEventListener('click', thread_close); 33 | } 34 | 35 | function thread_show() { 36 | divs['main-page'].classList.add('thread-opened'); 37 | thread_root.classList.remove('dhide'); 38 | } 39 | 40 | function thread_hide() { 41 | divs['main-page'].classList.remove('thread-opened'); 42 | thread_root.classList.add('dhide'); 43 | } -------------------------------------------------------------------------------- /public/icons/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/icons/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/scripts/spinner.js: -------------------------------------------------------------------------------- 1 | // Slightly modified from 2 | // https://stackoverflow.com/a/18473154 3 | function spinner_polar_to_cartesian(centerX, centerY, radius, angleInDegrees) { 4 | const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; 5 | 6 | return { 7 | 'x': centerX + (radius * Math.cos(angleInRadians)), 8 | 'y': centerY + (radius * Math.sin(angleInRadians)) 9 | }; 10 | } 11 | 12 | function spinner_describe_arc(x, y, radius, startAngle, endAngle) { 13 | const start = spinner_polar_to_cartesian(x, y, radius, endAngle); 14 | const end = spinner_polar_to_cartesian(x, y, radius, startAngle); 15 | 16 | var largeArcFlag = (Math.abs(endAngle - startAngle) % 360) <= 180 ? "0" : "1"; 17 | 18 | var d = [ 19 | 'M', start.x, start.y, 20 | 'A', radius, radius, 0, largeArcFlag, 0, end.x, end.y 21 | ].join(' '); 22 | 23 | return d; 24 | } 25 | 26 | function spinner_init(spinner) { 27 | let a_angel = 0; 28 | let b_angel = 180; 29 | let speed_a = 2; 30 | let speed_b = 6; 31 | let raf = null; 32 | 33 | const step_spinner = () => { 34 | const str = spinner_describe_arc(32, 32, 16, a_angel, b_angel); 35 | 36 | a_angel += speed_a; 37 | b_angel += speed_b; 38 | 39 | const diff = b_angel - a_angel; 40 | 41 | if (diff > 320) { 42 | speed_a = 6; 43 | speed_b = 2; 44 | } else if (diff < 40) { 45 | speed_a = 2; 46 | speed_b = 6; 47 | } 48 | 49 | spinner.setAttribute('d', str); 50 | 51 | raf = window.requestAnimationFrame(() => step_spinner()); 52 | } 53 | 54 | const stop_spinner = () => { 55 | if (raf !== null) { 56 | window.cancelAnimationFrame(raf); 57 | } 58 | } 59 | 60 | return { 61 | 'start': step_spinner, 62 | 'stop': stop_spinner 63 | }; 64 | } -------------------------------------------------------------------------------- /public/icons/attach.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/icons/attach_ccc.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 38 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/shared/mapping.c: -------------------------------------------------------------------------------- 1 | static struct bc_vm 2 | mapping_reserve(u64 size) 3 | { 4 | struct bc_vm result = { 0 }; 5 | 6 | u8 *region = mmap(NULL, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); /* PROT_NONE + MAP_ANONYMOUS = do not commit */ 7 | 8 | if (region == MAP_FAILED) { 9 | log_perror("mmap (reserve)"); 10 | return(result); 11 | } 12 | 13 | result.base = region; 14 | result.size = size; 15 | result.commited = 0; 16 | 17 | return(result); 18 | } 19 | 20 | static bool 21 | mapping_commit(struct bc_vm *vm, u64 offset, u64 size) 22 | { 23 | if (offset + size > vm->size) { 24 | log_ferror(__func__, "Mapping commit failed offset (%d) + commit_size (%d) > reserve_size (%d)\n", offset, size, vm->size); 25 | return(false); 26 | } 27 | 28 | if (mprotect(vm->base + offset, size, PROT_READ | PROT_WRITE)) { 29 | log_perror("mprotect (commit)"); 30 | return(false); 31 | } 32 | 33 | return(true); 34 | } 35 | 36 | static bool 37 | mapping_decommit(struct bc_vm *vm, u64 offset, u64 size) 38 | { 39 | if (offset + size > vm->size) { 40 | return(false); 41 | } 42 | 43 | if (mprotect(vm->base + offset, size, PROT_NONE)) { 44 | log_perror("mprotect (decommit)"); 45 | return(false); 46 | } 47 | 48 | return(true); 49 | } 50 | 51 | static bool 52 | mapping_release(struct bc_vm *vm) 53 | { 54 | if (munmap(vm->base, vm->size)) { 55 | log_perror("munmap (release)"); 56 | return(false); 57 | } 58 | 59 | return(true); 60 | } 61 | 62 | static bool 63 | mapping_expand(struct bc_vm *vm, u64 size) 64 | { 65 | size = round_up_to_page_size(size); 66 | 67 | if (!mapping_commit(vm, vm->commited, size)) { 68 | return(false); 69 | } 70 | 71 | vm->commited += size; 72 | 73 | return(true); 74 | } -------------------------------------------------------------------------------- /public/styles/login.css: -------------------------------------------------------------------------------- 1 | .page.login { 2 | height: 100%; 3 | background: var(--dark-blue); 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | align-items: center; 8 | gap: 40px; 9 | padding-bottom: var(--gap); 10 | box-sizing: border-box; 11 | } 12 | 13 | .login-center { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | gap: var(--gap); 18 | width: 100%; 19 | } 20 | 21 | .tagline { 22 | display: flex; 23 | width: 100%; 24 | justify-content: center; 25 | align-items: center; 26 | gap: var(--gap); 27 | color: var(--ascent-blue); 28 | font-family: 'Inter Semibold'; 29 | font-size: 32px; 30 | } 31 | 32 | .tagline img { 33 | height: 20px; 34 | position: relative; 35 | top: 1px; 36 | } 37 | 38 | .login-card { 39 | padding: 20px; 40 | border-radius: var(--radius); 41 | background: white; 42 | display: flex; 43 | flex-direction: column; 44 | align-items: stretch; 45 | gap: var(--gap); 46 | min-width: 400px; 47 | text-align: center; 48 | box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.2); 49 | } 50 | 51 | .login-card .primary-button { 52 | border: none; 53 | background: var(--ascent-blue); 54 | color: white; 55 | font-size: 14px; 56 | font-family: 'Inter Semibold'; 57 | height: 32px; 58 | cursor: pointer; 59 | box-shadow: 0px 1px 2px 0px var(--ascent-blue); 60 | border-radius: var(--radius); 61 | } 62 | 63 | .login-card .primary-button:active { 64 | filter: brightness(0.7); 65 | } 66 | 67 | .login-card .deemph-link { 68 | color: black; 69 | font-size: 14px; 70 | text-decoration: none; 71 | } 72 | 73 | .page.login .misc-info { 74 | color: var(--de-emph); 75 | font-size: 12px; 76 | text-align: center; 77 | } 78 | 79 | .login-fail-text { 80 | font-size: 14px; 81 | color: red; 82 | } -------------------------------------------------------------------------------- /public/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 46 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/icons/reply.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/scripts/search.js: -------------------------------------------------------------------------------- 1 | const SEARCH_MIN_QUERY_LENGTH = 2; 2 | 3 | let search_on = false; 4 | let search_really_on = false; 5 | let search_query = ''; 6 | 7 | function search_disable() { 8 | if (search_on) { 9 | search_toggle(); 10 | } 11 | } 12 | 13 | function search_toggle(on_complete = null) { 14 | search_on = !search_on; /* JOKE: This is literally always false */ 15 | 16 | if (search_on) { 17 | // Turn ON search mode 18 | divs['search-input'].value = ''; 19 | divs['search-input'].classList.remove('hidden'); 20 | divs['search-input'].focus(); 21 | } else { 22 | // Turn OFF search mode 23 | divs['search-input'].classList.add('hidden'); 24 | 25 | if (search_really_on) { 26 | set_search_filter(null, on_complete); 27 | search_really_on = false; 28 | } 29 | } 30 | } 31 | 32 | function search_perform() { 33 | const query_lc = search_query.toLowerCase(); 34 | set_search_filter(msg => msg.type === RECORD.MESSAGE && msg.text.toLowerCase().includes(query_lc)); 35 | } 36 | 37 | function search_keydown(e) { 38 | if (e.code === 'Enter') { 39 | search_query = divs['search-input'].value; 40 | if (search_query.length >= SEARCH_MIN_QUERY_LENGTH) { 41 | search_perform(); 42 | search_really_on = true; 43 | } 44 | } else if (e.code === 'Escape' && !e.shiftKey) { 45 | search_on = false; 46 | divs['search-input'].classList.add('hidden'); 47 | 48 | if (search_really_on) { 49 | set_search_filter(null); 50 | search_really_on = false; 51 | } 52 | } 53 | } 54 | 55 | function set_search_filter(filter, on_complete = null) { 56 | if (filter !== null) { 57 | divs['container'].classList.add('search'); 58 | channel_scroller.set_filter(filter, on_complete); 59 | } else { 60 | set_default_filter(on_complete); 61 | divs['container'].classList.remove('search'); 62 | } 63 | } -------------------------------------------------------------------------------- /public/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/icons/search_333.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/icons/search_ccc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 49 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/icons/tick2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/icons/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/icons/perfect_attach.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 38 | 43 | 44 | 49 | 50 | -------------------------------------------------------------------------------- /src/media/posix_queue.c: -------------------------------------------------------------------------------- 1 | static int 2 | generate_process(void) { 3 | int pid = fork(); 4 | return pid; 5 | } 6 | 7 | static int 8 | init_posix_queue(int parent) { 9 | struct mq_attr attr; 10 | 11 | attr.mq_flags = 0; 12 | attr.mq_maxmsg = MQ_MAX_MSG; 13 | attr.mq_msgsize = sizeof(struct mq_message); 14 | attr.mq_curmsgs = 0; 15 | int flags = parent ? O_WRONLY : O_RDONLY; 16 | flags |= O_CREAT; 17 | mqd_t mq_desc = mq_open(MQ_NAME, flags, S_IRWXU, &attr); 18 | 19 | if (mq_desc == -1) { 20 | perror("init posix queue"); 21 | exit(1); 22 | } 23 | 24 | return mq_desc; 25 | } 26 | 27 | static int 28 | send_msg(int mq_desc, struct mq_message *msg) { 29 | int rt = mq_send(mq_desc, (char *) msg, sizeof(*msg), 1); 30 | 31 | //printf("%ld\n", sizeof(*msg)); 32 | 33 | if (rt == -1) { 34 | perror("send mesage in queue"); 35 | return rt; 36 | } 37 | 38 | return rt; 39 | } 40 | 41 | static void 42 | receive_msg(int mq_desc, struct mq_message *dest) { 43 | int rt = mq_receive(mq_desc, (char *) dest, sizeof(*dest), NULL); 44 | if (rt == -1) { 45 | int errsv = errno; 46 | // EAGAIN means the queue is empty 47 | if (errsv != EAGAIN) { 48 | perror("receive msg from queue"); 49 | //sleep(1); 50 | } 51 | } else { 52 | log_info("Received a new file resize request (file id = %lx)\n", dest->file_id); 53 | } 54 | } 55 | 56 | static int 57 | start_generate_preview(struct mq_message *msg) { 58 | struct bc_image img = image_load(msg->file_id); 59 | 60 | if (img.data) { 61 | int rt = image_resize(img); 62 | return(rt); 63 | } 64 | 65 | return(0); 66 | } 67 | 68 | static void 69 | close_queue(int mq_desc) { 70 | int rt = mq_close(mq_desc); 71 | if (rt == -1) { 72 | perror("close posix queue"); 73 | } 74 | } 75 | 76 | static void 77 | unlink_name(void) { 78 | int rt = mq_unlink(MQ_NAME); 79 | if (rt == -1) { 80 | perror("close posix queue"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /public/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 48 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/media/storage.c: -------------------------------------------------------------------------------- 1 | static bool 2 | storage_init(struct bc_server *server) 3 | { 4 | server->storage.files = buffer_makevm(MAX_POSSIBLE_FILES * sizeof(struct bc_file)); 5 | 6 | if (!server->storage.files.vm.size) { 7 | return(false); 8 | } 9 | 10 | char *filename = "files.bmedia"; 11 | int fd = open(filename, O_RDWR | O_CREAT | O_SYNC | O_APPEND, S_IRUSR | S_IWUSR); 12 | if (fd == -1) { 13 | log_fperror(__func__, "open"); 14 | return(false); 15 | } 16 | 17 | server->storage.fd = fd; 18 | 19 | u64 file_size = get_file_size(fd); 20 | 21 | if (file_size > 0) { 22 | char *data = mmap(0, file_size, PROT_READ, MAP_PRIVATE, fd, 0); 23 | if (data == MAP_FAILED) { 24 | log_fperror(__func__, "mmap"); 25 | return(false); 26 | } 27 | 28 | if (!buffer_appendvm(&server->storage.files, data, file_size)) return(false); 29 | } 30 | 31 | return(true); 32 | } 33 | 34 | static bool 35 | storage_add_file(struct bc_server *server, struct bc_file *file) 36 | { 37 | if (!file) return(false); 38 | 39 | struct bc_file *saved = (struct bc_file *) buffer_head(&server->storage.files); 40 | 41 | if (!saved || !buffer_appendvm(&server->storage.files, file, sizeof(*file))) { 42 | return(false); 43 | } 44 | 45 | struct bc_io *req = queue_push(&server->queue, IOU_WRITE); 46 | 47 | req->write.fd = server->storage.fd; 48 | req->write.buf = (char *) saved; 49 | req->write.size = sizeof(*saved); 50 | 51 | queue_write(&server->queue, req); 52 | 53 | return(true); 54 | } 55 | 56 | static struct bc_file * 57 | storage_find_file(struct bc_server *server, u64 file_id) 58 | { 59 | struct bc_file *files = (struct bc_file *) (server->storage.files.data); 60 | int file_count = server->storage.files.use / sizeof(struct bc_file); 61 | 62 | for (int i = 0; i < file_count; ++i) { 63 | struct bc_file *file = files + i; 64 | if (file->id == file_id) { 65 | return(file); 66 | } 67 | } 68 | 69 | return(0); 70 | } -------------------------------------------------------------------------------- /public/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 41 | 42 | 44 | 49 | 54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/html/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bullet.Chat | ADMIN 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Bullet.Chat admin panel

16 | 17 | 18 |
19 | 28 |
29 | 30 | 31 | 32 | 33 |
34 | 35 | 40 |
41 | 42 | 46 | 47 | 48 | 49 | Login page 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/icons/goto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 47 | 52 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/icons/failed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 42 | 43 | 45 | 50 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 44 | 50 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/icons/cancel_333.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 44 | 50 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /public/icons/pencil_new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 41 | 46 | 51 | 55 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 47 | 52 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /public/icons/perfect-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 42 | 43 | 45 | 50 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/scripts/settings.js: -------------------------------------------------------------------------------- 1 | function l_avatar_upload_started(file) { 2 | const url = URL.createObjectURL(file); 3 | document.querySelector('#avatar-big-preview').src = url; 4 | } 5 | 6 | function l_avatar_upload_complete(file) { 7 | websocket_send_set_user_avatar(file.id); 8 | } 9 | 10 | function settings_init() { 11 | const uploader = uploader_create(document.getElementById('avatar-upload-hidden-button'), { 12 | 'url_create': '/upload/upload-req', 13 | 'url_status': '/upload/upload-status', 14 | 'url_upload': '/upload/upload-do', 15 | 'on_start': l_avatar_upload_started, 16 | 'on_complete': l_avatar_upload_complete, 17 | }); 18 | 19 | const me = ls_get_me(); 20 | if (me && me.avatar !== '0') { 21 | const avatar_element = find('avatar-big-preview'); 22 | if (avatar_element) { 23 | avatar_element.src = `storage/${me.avatar}-2`; 24 | } 25 | } 26 | 27 | const displayname_element = find('settings-me-displayname'); 28 | const login_element = find('settings-me-login'); 29 | if (displayname_element) displayname_element.innerText = me.display; 30 | if (login_element) login_element.innerText = '@' + me.login; 31 | 32 | restore_settings(); 33 | } 34 | 35 | function settings_open_avatar_selection_dialog() { 36 | document.querySelector('#avatar-upload-hidden-button').click(); 37 | } 38 | 39 | function settings_reset_avatar() { 40 | websocket_send_set_user_avatar('0'); 41 | } 42 | 43 | function restore_settings() { 44 | const me = ls_get_me(); 45 | websocket_request_utf8((buffer, dataview) => { 46 | const d = deserializer_create(buffer, dataview); 47 | const nonce = deserializer_u32(d); 48 | const length = deserializer_u32(d); 49 | const settings_string = deserializer_text(d, length); 50 | ls_save_settings(me.blob, settings_string); 51 | }); 52 | } 53 | 54 | function save_settings() { 55 | const settings_data = {'sound': 'off'}; 56 | const settings_string = JSON.stringify(settings_data); 57 | const me = ls_get_me(); 58 | 59 | websocket_save_utf8(settings_string, (buffer, dataview) => { 60 | const d = deserializer_create(buffer, dataview); 61 | const nonce = deserializer_u32(d); 62 | const settings_key = deserializer_u32(d); 63 | ls_save_settings(settings_key, settings_string); 64 | }); 65 | } -------------------------------------------------------------------------------- /public/icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 54 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /public/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 44 | 49 | 54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Bullet.Chat | Login 22 | 23 | 24 | 27 | 28 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/icons/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 43 | 44 | 46 | 51 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /public/icons/burger_ccc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 43 | 44 | 46 | 51 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /public/icons/perfect-bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 63 | -------------------------------------------------------------------------------- /src/websocket/record.c: -------------------------------------------------------------------------------- 1 | static char * 2 | record_data(struct bc_persist_record *record) 3 | { 4 | if (!record) return(0); 5 | 6 | // Waiting for fixed-width enums got me like 7 | switch ((enum bc_record_type) record->type) { // cast type to enum for -Wswitch 8 | case WS_TITLE_CHANGED: 9 | case WS_MESSAGE: 10 | case WS_EDIT: 11 | case WS_ATTACH: { 12 | return(POINTER_INC(record, sizeof(*record))); 13 | } 14 | 15 | case WS_REPLY: 16 | case WS_PIN: 17 | case WS_DELETE: 18 | case WS_REACTION_REMOVE: 19 | case WS_REACTION_ADD: 20 | case WS_USER_LEFT: 21 | case WS_USER_JOINED: { 22 | break; 23 | } 24 | 25 | case WS_RECORD_TYPE_COUNT: { 26 | break; 27 | } 28 | 29 | // NOTE(aolo2): do NOT add a default case, so that -Wswitch catches new message types 30 | } 31 | 32 | return(0); 33 | } 34 | 35 | static bool 36 | record_attach_ext_is_supported_image(u32 ext) 37 | { 38 | bool result = (1 <= ext && ext <= 4) || (ext == 7); 39 | return(result); 40 | } 41 | 42 | static int 43 | record_size(struct bc_persist_record *record) 44 | { 45 | if (!record) return(0); 46 | 47 | int result = sizeof(*record); 48 | 49 | switch ((enum bc_record_type) record->type) { // cast type to enum for -Wswitch 50 | case WS_MESSAGE: { 51 | result += record->message.length; 52 | break; 53 | } 54 | 55 | case WS_EDIT: { 56 | result += record->edit.length; 57 | break; 58 | } 59 | 60 | case WS_TITLE_CHANGED: { 61 | result += record->title_changed.length; 62 | break; 63 | } 64 | 65 | case WS_ATTACH: { 66 | if (!record_attach_ext_is_supported_image(record->attach.file_ext)) { 67 | /* exts 1-4, 7 mean its a supported image with width and height and no filename */ 68 | result += record->attach.filename_length; 69 | } 70 | 71 | break; 72 | } 73 | 74 | case WS_REPLY: 75 | case WS_PIN: 76 | case WS_DELETE: 77 | case WS_REACTION_REMOVE: 78 | case WS_REACTION_ADD: 79 | case WS_USER_LEFT: 80 | case WS_USER_JOINED: { 81 | break; 82 | } 83 | 84 | case WS_RECORD_TYPE_COUNT: { 85 | break; 86 | } 87 | 88 | // NOTE(aolo2): do NOT add a default case, so that -Wswitch catches new message types 89 | } 90 | 91 | return(result); 92 | } -------------------------------------------------------------------------------- /public/icons/bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 43 | 48 | 53 | 62 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/icons/leftarrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 54 | 55 | 60 | 64 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/icons/perfect-burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 43 | 44 | 46 | 51 | 59 | 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /public/icons/perfect-white-burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 43 | 44 | 46 | 51 | 59 | 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/websocket/auth.c: -------------------------------------------------------------------------------- 1 | static bool 2 | auth_check_password(struct bc_persist_user *user, struct bc_str password) 3 | { 4 | if (!user) { 5 | return(false); 6 | } 7 | 8 | if (password.length > 64) { 9 | log_warning("Password too long (check)\n"); 10 | return(false); 11 | } 12 | 13 | char password_buf[65] = { 0 }; 14 | memcpy(password_buf, password.data, password.length); 15 | password_buf[password.length] = 0; 16 | 17 | char *hash = crypt(password_buf, user->password_hash); /* hash starts with prefix, options, and salt (!), so the hash can be used as setting for subsequient crypt calls */ 18 | int hash_length = strlen(hash); 19 | 20 | if (memcmp(hash, user->password_hash, hash_length) == 0) { 21 | return(true); 22 | } 23 | 24 | return(false); 25 | } 26 | 27 | static bool 28 | auth_write_hash(struct bc_persist_user *user, struct bc_str password) 29 | { 30 | if (password.length > 64) { 31 | log_warning("Provided password too long\n"); 32 | return(false); 33 | } 34 | 35 | char password_buf[65] = { 0 }; 36 | memcpy(password_buf, password.data, password.length); 37 | password_buf[password.length] = 0; 38 | 39 | char salt[25] = { 0 }; 40 | unsigned char salt_random[24]; 41 | 42 | if (getrandom(salt_random, 24, 0) != 24) { 43 | log_error("Didn't get enough entropy\n"); 44 | return(false); 45 | } 46 | 47 | static char asciibytes[] = { 48 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 49 | 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 50 | 'u', 'v', 'w', 'x', 'y', 'z', 51 | 52 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 53 | 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 54 | 'U', 'V', 'W', 'X', 'Y', 'Z', 55 | 56 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 57 | 58 | '.', '/' 59 | }; 60 | 61 | for (int i = 0; i < 24; ++i) { 62 | salt[i] = asciibytes[salt_random[i] % 64]; 63 | } 64 | 65 | char setting[32] = { 0 }; 66 | snprintf(setting, 32, "$2b$10$%.24s", salt); /* NOTE(aolo2): bcrypt, 2^10 iterations */ 67 | 68 | char *hash = crypt(password_buf, setting); 69 | if (!hash || hash[0] == '*') { 70 | log_perror("crypt"); 71 | return(false); 72 | } 73 | 74 | int hash_length = strlen(hash); 75 | 76 | if (hash_length > PASSWORD_MAX_HASH_LENGTH) { 77 | log_warning("Crypt returned a passphrase too long\n"); 78 | return(false); 79 | } 80 | 81 | memcpy(user->password_hash, hash, hash_length); /* set hash */ 82 | 83 | return(true); 84 | } 85 | 86 | static u64 87 | auth_generate_session(void) 88 | { 89 | u64 session_id; 90 | getrandom(&session_id, 8, 0); 91 | return(session_id); 92 | } -------------------------------------------------------------------------------- /public/icons/bullet_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 41 | 46 | 51 | 56 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/bullet_ccc_straight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 41 | 46 | 51 | 56 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/bullet_333.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 45 | 50 | 55 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/bullet_ccc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 45 | 50 | 55 | 65 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/react2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 54 | 67 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 50 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/icons/settings_333.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 50 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/icons/settings_ccc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 50 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/icons/bullet_mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 45 | 50 | 55 | 64 | 69 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 44 | 49 | 61 | 66 | 71 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /public/icons/pin_aaa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 53 | 67 | 68 | 73 | 82 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/notification/eve.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | int 9 | main(int argc, char **argv) { 10 | if (argc != 4) { 11 | fprintf(stderr, "Usage: %s endpoint p256dh auth\n", argv[0]); 12 | return(1); 13 | } 14 | 15 | // The endpoint, public key, and auth secret for the push subscription. These 16 | // are exposed via `JSON.stringify(pushSubscription)` in the browser. 17 | const char* endpoint = argv[1]; 18 | const char* p256dh = argv[2]; 19 | const char* auth = argv[3]; 20 | 21 | // The message to encrypt. 22 | const void* plaintext = "{\"title\": \"FROM C!\", \"options\": {\"body\": \"yep..\"}}"; 23 | size_t plaintextLen = strlen(plaintext); 24 | 25 | // How many bytes of padding to include in the encrypted message. Padding 26 | // obfuscates the plaintext length, making it harder to guess the contents 27 | // based on the encrypted payload length. 28 | size_t padLen = 0; 29 | 30 | // Base64url-decode the subscription public key and auth secret. `recv` is 31 | // short for "receiver", which, in our case, is the browser. 32 | uint8_t rawRecvPubKey[ECE_WEBPUSH_PUBLIC_KEY_LENGTH]; 33 | size_t rawRecvPubKeyLen = 34 | ece_base64url_decode(p256dh, strlen(p256dh), ECE_BASE64URL_REJECT_PADDING, 35 | rawRecvPubKey, ECE_WEBPUSH_PUBLIC_KEY_LENGTH); 36 | assert(rawRecvPubKeyLen > 0); 37 | uint8_t authSecret[ECE_WEBPUSH_AUTH_SECRET_LENGTH]; 38 | size_t authSecretLen = 39 | ece_base64url_decode(auth, strlen(auth), ECE_BASE64URL_REJECT_PADDING, 40 | authSecret, ECE_WEBPUSH_AUTH_SECRET_LENGTH); 41 | assert(authSecretLen > 0); 42 | 43 | // Allocate a buffer large enough to hold the encrypted payload. The payload 44 | // length depends on the record size, padding, and plaintext length, plus a 45 | // fixed-length header block. Smaller records and additional padding take 46 | // more space. The maximum payload length rounds up to the nearest whole 47 | // record, so the actual length after encryption might be smaller. 48 | size_t payloadLen = ece_aes128gcm_payload_max_length(ECE_WEBPUSH_DEFAULT_RS, 49 | padLen, plaintextLen); 50 | assert(payloadLen > 0); 51 | uint8_t* payload = calloc(payloadLen, sizeof(uint8_t)); 52 | assert(payload); 53 | 54 | // Encrypt the plaintext. `payload` holds the header block and ciphertext; 55 | // `payloadLen` is an in-out parameter set to the actual payload length. 56 | int err = ece_webpush_aes128gcm_encrypt( 57 | rawRecvPubKey, rawRecvPubKeyLen, authSecret, authSecretLen, 58 | ECE_WEBPUSH_DEFAULT_RS, padLen, plaintext, plaintextLen, payload, 59 | &payloadLen); 60 | assert(err == ECE_OK); 61 | 62 | // Write the payload out to a file. 63 | const char* filename = "aes128gcm.bin"; 64 | FILE* payloadFile = fopen(filename, "wb"); 65 | assert(payloadFile); 66 | size_t payloadFileLen = 67 | fwrite(payload, sizeof(uint8_t), payloadLen, payloadFile); 68 | assert(payloadLen == payloadFileLen); 69 | fclose(payloadFile); 70 | 71 | printf( 72 | "curl -v -X POST -H \"TTL: 30\" -H \"Content-Encoding: aes128gcm\" --data-binary @%s %s\n", 73 | filename, endpoint); 74 | 75 | free(payload); 76 | 77 | return 0; 78 | } -------------------------------------------------------------------------------- /public/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 51 | 56 | 61 | 66 | 73 | 80 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /public/icons/people.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 39 | 41 | 46 | 51 | 56 | 62 | 67 | 73 | 79 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /public/icons/perfect-people.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 41 | 46 | 47 | 49 | 54 | 59 | 64 | 70 | 75 | 81 | 87 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/shared/log.c: -------------------------------------------------------------------------------- 1 | static bool LOGS_ENABLED = true; 2 | static bool LOGS_PROD_MODE = false; 3 | 4 | static void 5 | log_silence(void) 6 | { 7 | LOGS_ENABLED = false; 8 | } 9 | 10 | static void 11 | log_unsilence(void) 12 | { 13 | LOGS_ENABLED = true; 14 | } 15 | 16 | static void 17 | log_time(FILE *restrict stream) 18 | { 19 | if (LOGS_PROD_MODE) return; 20 | if (!LOGS_ENABLED) return; 21 | time_t now = unix_utcnow(); 22 | struct tm *local = localtime(&now); 23 | fprintf(stream, "[%d.%02d.%02d %02d:%02d:%02d] ", 24 | local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, 25 | local->tm_hour, local->tm_min, local->tm_sec); 26 | } 27 | 28 | static void 29 | log_perror(const char *text) 30 | { 31 | if (!LOGS_ENABLED) return; 32 | fprintf(stderr, "\033[1m\033[31m[ERRNO] "); 33 | log_time(stderr); 34 | fprintf(stderr, "%s: %s\033[0m\n", text, strerror(errno)); 35 | } 36 | 37 | static void 38 | log_critical_die(const char* format, ...) 39 | { 40 | if (!LOGS_ENABLED) exit(1); 41 | va_list argptr; 42 | va_start(argptr, format); 43 | fprintf(stderr, "\033[1m\033[31m[CRITICAL] "); 44 | log_time(stderr); 45 | vfprintf(stderr, format, argptr); 46 | fprintf(stderr, "\033[0m"); 47 | va_end(argptr); 48 | exit(1); 49 | } 50 | 51 | static void 52 | log_error(const char* format, ...) 53 | { 54 | if (!LOGS_ENABLED) return; 55 | va_list argptr; 56 | va_start(argptr, format); 57 | fprintf(stderr, "\033[1m\033[31m[ERROR] "); 58 | log_time(stderr); 59 | vfprintf(stderr, format, argptr); 60 | fprintf(stderr, "\033[0m"); 61 | va_end(argptr); 62 | } 63 | 64 | static void 65 | log_ferror(const char *func, const char* format, ...) 66 | { 67 | if (!LOGS_ENABLED) return; 68 | va_list argptr; 69 | va_start(argptr, format); 70 | fprintf(stderr, "\033[1m\033[31m[ERROR] "); 71 | log_time(stderr); 72 | fprintf(stderr, "%s: ", func); 73 | vfprintf(stderr, format, argptr); 74 | fprintf(stderr, "\033[0m"); 75 | va_end(argptr); 76 | } 77 | 78 | static void 79 | log_fwarning(const char *func, const char* format, ...) 80 | { 81 | if (!LOGS_ENABLED) return; 82 | va_list argptr; 83 | va_start(argptr, format); 84 | fprintf(stderr, "[WARN] "); 85 | log_time(stderr); 86 | fprintf(stderr, "%s: ", func); 87 | vfprintf(stderr, format, argptr); 88 | va_end(argptr); 89 | } 90 | 91 | static void 92 | log_fperror(const char *func, const char *text) 93 | { 94 | if (!LOGS_ENABLED) return; 95 | fprintf(stderr, "\033[1m\033[31m[ERRNO] "); 96 | log_time(stderr); 97 | fprintf(stderr, "%s: %s: %s\033[0m\n", func, text, strerror(errno)); 98 | } 99 | 100 | static void 101 | log_warning(const char* format, ...) 102 | { 103 | if (!LOGS_ENABLED) return; 104 | va_list argptr; 105 | va_start(argptr, format); 106 | fprintf(stderr, "[WARN] "); 107 | log_time(stderr); 108 | vfprintf(stderr, format, argptr); 109 | va_end(argptr); 110 | } 111 | 112 | static void 113 | log_info(const char* format, ...) 114 | { 115 | if (!LOGS_ENABLED) return; 116 | va_list argptr; 117 | va_start(argptr, format); 118 | printf("[INFO] "); 119 | log_time(stdout); 120 | vfprintf(stdout, format, argptr); 121 | va_end(argptr); 122 | } 123 | 124 | static void 125 | log_debug(const char* format, ...) 126 | { 127 | if (!LOGS_ENABLED) return; 128 | #if DEBUG_PRINT 129 | va_list argptr; 130 | va_start(argptr, format); 131 | fprintf(stdout, "[DEBUG] "); 132 | log_time(stdout); 133 | vfprintf(stdout, format, argptr); 134 | va_end(argptr); 135 | #else 136 | (void) format; 137 | #endif 138 | } 139 | -------------------------------------------------------------------------------- /public/icons/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 42 | 43 | 45 | 61 | 77 | 78 | 83 | 92 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /public/styles/dark.css: -------------------------------------------------------------------------------- 1 | .dark .content-block { 2 | background: var(--dark-blue); 3 | } 4 | 5 | .dark .channel-header { 6 | color: var(--dark-de-de-emph); 7 | } 8 | 9 | .dark .channel-header .action img { 10 | filter: invert(100%); 11 | } 12 | 13 | .dark .channel-header .action:hover { 14 | background: var(--dark-hover); 15 | } 16 | 17 | .dark .message .indicator { 18 | background: var(--dark-blue); 19 | } 20 | 21 | .dark .message .content { 22 | background: var(--de-dark-blue); 23 | border-color: var(--dark-blue); 24 | } 25 | 26 | .dark .message .message-header { 27 | background: var(--dark-blue); 28 | border-color: var(--dark-blue); 29 | } 30 | 31 | .dark #search-input, 32 | .dark .message-input-textarea { 33 | background: var(--de-dark-blue); 34 | color: white; 35 | } 36 | 37 | .dark .channel-header .row1 { 38 | border-color: var(--dark-border); 39 | } 40 | 41 | .dark .date-separator-hr { 42 | border-color: #3c434f; 43 | } 44 | 45 | .dark .date-separator-date { 46 | background: var(--dark-blue); 47 | color: var(--dark-de-emph); 48 | } 49 | 50 | .dark .message .message-header .author { 51 | color: var(--dark-de-de-emph); 52 | } 53 | 54 | .dark .message .content .text { 55 | color: var(--dark-de-de-emph); 56 | } 57 | 58 | .dark .message.mine .content { 59 | background: var(--dark-ascent-blue); 60 | } 61 | 62 | .dark .message .overtop-row .reply-author { 63 | color: var(--dark-de-de-emph); 64 | } 65 | 66 | .dark .message .overtop-row .reply-preview { 67 | color: var(--dark-de-emph); 68 | } 69 | 70 | .dark .message .overtop-row .reply-angle { 71 | border-color: var(--dark-de-emph); 72 | } 73 | 74 | .dark .message .reactions .reaction { 75 | background: var(--de-dark-blue); 76 | } 77 | 78 | .dark .message .text a, 79 | .dark .message .overtop-row .reply-preview a, 80 | .dark .message-input .reply-preview-text a { 81 | color: var(--de-de-emph); 82 | } 83 | 84 | .dark .reactions .reaction-count { 85 | color: var(--dark-de-de-emph); 86 | } 87 | 88 | .dark .channel-header .row2 { 89 | background: var(--dark-blue); 90 | border-bottom-color: var(--dark-border); 91 | border-left-color: var(--dark-de-emph); 92 | } 93 | 94 | .dark .message-input .row1 { 95 | background: var(--dark-blue); 96 | border-color: var(--dark-de-emph); 97 | color: var(--dark-de-de-emph); 98 | } 99 | 100 | .dark .file-attachment-info { 101 | color: var(--dark-de-de-emph); 102 | } 103 | 104 | .dark .message .attachments .item .icon { 105 | border-color: var(--dark-de-de-emph); 106 | } 107 | 108 | .dark .message .attachments .item img { 109 | filter: invert(15%); 110 | } 111 | 112 | .dark .message.mine .attachments { 113 | border-color: var(--dark-de-de-emph); 114 | } 115 | 116 | .dark .message.mine .attachments .item .text { 117 | color: var(--dark-de-de-emph); 118 | } 119 | 120 | .dark .message .content .mention { 121 | background: var(--dark-ascent-blue); 122 | } 123 | 124 | .dark .message.mine .content .mention { 125 | background: #7288bb; 126 | } 127 | 128 | .dark .message.mine .content .mention.me { 129 | color: white; 130 | } 131 | 132 | .dark .message-input { 133 | background: var(--dark-blue); 134 | } 135 | 136 | .dark .content-block .godown { 137 | background: var(--de-dark-blue); 138 | } 139 | 140 | .dark .message-input .row2 { 141 | background: var(--dark-blue); 142 | } 143 | 144 | .dark .spinner-container { 145 | background: var(--dark-blue); 146 | } 147 | 148 | .dark .hovering-date-indicator-wrapper .value { 149 | background: var(--de-dark-blue); 150 | box-shadow: 0px 0px 2px 1px rgba(0, 0, 0, 0.15); 151 | border: none; 152 | } 153 | 154 | .dark .hovering-date-indicator-wrapper { 155 | color: var(--dark-de-de-emph); 156 | } -------------------------------------------------------------------------------- /public/icons/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 48 | 54 | 60 | 64 | 68 | 72 | 73 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /public/scripts/login.js: -------------------------------------------------------------------------------- 1 | const touch_device = (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); 2 | 3 | let login_form = null; 4 | let login_field = null; 5 | let submit_button = null; 6 | let password_field = null; 7 | let failed_message = null; 8 | 9 | let online = false; 10 | 11 | function submit_login(e) { 12 | e.preventDefault(); 13 | 14 | const login = login_field.value.trim(); 15 | const password = password_field.value.trim(); 16 | 17 | if (login.length === 0 || password.length === 0) { 18 | return; 19 | } 20 | 21 | if (online) { 22 | websocket_send_auth(login, password); 23 | } 24 | } 25 | 26 | function reenable_submit() { 27 | const login = login_field.value.trim(); 28 | const password = password_field.value.trim(); 29 | 30 | if (login.length === 0 || password.length === 0) { 31 | submit_button.classList.add('tdisabled'); 32 | return false; 33 | } else { 34 | submit_button.classList.remove('tdisabled'); 35 | return true; 36 | } 37 | } 38 | 39 | async function auth_success(buffer, dataview) { 40 | const session_id = dataview.getBigUint64(1, true); 41 | const user_id = dataview.getUint32(9, true); 42 | 43 | const me = { 44 | 'session_id': session_id.toString(), 45 | 'user_id': user_id, 46 | }; 47 | 48 | const old_me = ls_get_me(); 49 | 50 | if (old_me && old_me.user_id) { 51 | if (old_me.user_id !== user_id) { 52 | // NOTE(aolo2): if we are already logged in as another user - clear everything. We don't 53 | // want data from different sessions to get mixed up 54 | ls_clear(); 55 | await idb_clear(); 56 | ls_set_me(me); 57 | window.location.href = '/'; 58 | } 59 | } else { 60 | // NOTE(aolo2): if we have already logged in as this user, then don't to it again 61 | ls_set_me(me); 62 | } 63 | 64 | window.location.href = '/'; 65 | } 66 | 67 | function auth_fail() { 68 | failed_message.classList.remove('dhide'); 69 | } 70 | 71 | function find_divs() { 72 | login_form = find('login-form'); 73 | login_field = find('login-form-login'); 74 | password_field = find('login-form-password'); 75 | submit_button = find('login-form-submit'); 76 | failed_message = find('login-form-fail'); 77 | } 78 | 79 | function bind_listeners() { 80 | login_field.addEventListener('input', reenable_submit); 81 | password_field.addEventListener('input', reenable_submit); 82 | 83 | const i = setInterval(() => { 84 | const enabled = reenable_submit(); 85 | if (enabled) { 86 | clearInterval(i); 87 | } 88 | }, 500); 89 | 90 | make_clickable(submit_button, submit_login); 91 | 92 | window.addEventListener('keydown', (e) => { 93 | if (e.code === 'Slash') { 94 | login_field.focus(); 95 | e.preventDefault(); 96 | } 97 | }) 98 | } 99 | 100 | function set_style() { 101 | if (window.screen.height > window.screen.width) { 102 | document.body.classList.add('vertical'); 103 | } 104 | } 105 | 106 | async function main() { 107 | if (localStorage.getItem('bc-session-id')) { 108 | window.location.href = '/'; 109 | } 110 | 111 | set_style(); 112 | find_divs(); 113 | bind_listeners(); 114 | ls_init(); 115 | 116 | await idb_init(); 117 | 118 | websocket_register_handler(WS_MESSAGE_TYPE.SERVER.AUTH_SUCCESS, auth_success); 119 | websocket_register_handler(WS_MESSAGE_TYPE.SERVER.AUTH_FAIL, auth_fail); 120 | 121 | websocket_connect(CONFIG_WS_URL, () => { 122 | online = true; 123 | }); 124 | } 125 | 126 | document.addEventListener('DOMContentLoaded', () => { 127 | document.fonts.ready.then(main); 128 | }, false); 129 | -------------------------------------------------------------------------------- /src/shared/aux.c: -------------------------------------------------------------------------------- 1 | // Temporary/helper functions which don't have a home for now.. 2 | 3 | static int 4 | readall(int fd, void *buf, int size) 5 | { 6 | int consumed = 0; 7 | 8 | while (consumed < size) { 9 | int rv = read(fd, POINTER_INC(buf, consumed), size - consumed); 10 | if (rv < 0) { 11 | log_perror("[ERROR] read"); 12 | return(-1); 13 | } 14 | consumed += rv; 15 | } 16 | 17 | return(consumed); 18 | } 19 | 20 | static int 21 | writeall(int fd, void *buf, int size) 22 | { 23 | int consumed = 0; 24 | 25 | while (consumed < size) { 26 | int rv = write(fd, POINTER_INC(buf, consumed), size - consumed); 27 | if (rv < 0) { 28 | log_perror("[ERROR] write"); 29 | return(-1); 30 | } 31 | consumed += rv; 32 | } 33 | 34 | return(consumed); 35 | } 36 | 37 | static void 38 | prst(struct bc_str str) 39 | { 40 | printf("%.*s\n", str.length, str.data); 41 | } 42 | 43 | #if 0 44 | static void 45 | hex_dump(struct bc_str str) 46 | { 47 | for (int i = 0; i < str.length; ++i) { 48 | if (i > 0) { 49 | printf(" %hhx", str.data[i]); 50 | } else { 51 | printf("%hhx", str.data[i]); 52 | } 53 | } 54 | printf("\n"); 55 | } 56 | #endif 57 | 58 | static bool 59 | streq(struct bc_str a, struct bc_str b) 60 | { 61 | if (a.length != b.length) { 62 | return(false); 63 | } 64 | 65 | return(strncmp(a.data, b.data, a.length) == 0); 66 | } 67 | 68 | static u64 69 | msec_now(void) 70 | { 71 | struct timespec ts = { 0 }; 72 | clock_gettime(CLOCK_MONOTONIC, &ts); 73 | return(ts.tv_nsec / 1000000ULL + ts.tv_sec * 1000ULL); 74 | } 75 | 76 | static bool 77 | little_endian(void) 78 | { 79 | u32 v = 1; 80 | char vc[4]; 81 | 82 | memcpy(vc, &v, 4); 83 | 84 | if (vc[0] == 1) { 85 | return(true); 86 | } 87 | 88 | return(false); 89 | } 90 | 91 | static bool 92 | directory_exists(const char *dir) 93 | { 94 | struct stat ds; 95 | 96 | if (stat(dir, &ds) == 0 && S_ISDIR(ds.st_mode)) { 97 | return(true); 98 | } 99 | 100 | return(false); 101 | } 102 | 103 | static u32 104 | random_u31(void) 105 | { 106 | u32 r = 0; 107 | 108 | if (getrandom(&r, 4, 0) != 4) { 109 | log_error("Didn't get enough entropy\n"); 110 | return(0); 111 | } 112 | 113 | return(r & 0x7FFFFFFF); 114 | } 115 | 116 | static u64 117 | round_up_to_page_size(u64 size) 118 | { 119 | if (size & (PAGE_SIZE - 1)) { 120 | size &= ~(PAGE_SIZE - 1); 121 | size += PAGE_SIZE; 122 | } 123 | 124 | return(size); 125 | } 126 | 127 | static u16 128 | readu16(void *at) 129 | { 130 | u16 result; 131 | memcpy(&result, at, 2); 132 | return(result); 133 | } 134 | 135 | static u32 136 | readu32(void *at) 137 | { 138 | u32 result; 139 | memcpy(&result, at, 4); 140 | return(result); 141 | } 142 | 143 | static s32 144 | reads32(void *at) 145 | { 146 | s32 result; 147 | memcpy(&result, at, 4); 148 | return(result); 149 | } 150 | 151 | static u64 152 | readu64(void *at) 153 | { 154 | u64 result; 155 | memcpy(&result, at, 8); 156 | return(result); 157 | } 158 | 159 | static u64 160 | unix_utcnow(void) 161 | { 162 | struct timeval tv = { 0 }; 163 | gettimeofday(&tv, NULL); /* tz = NULL means UTC */ 164 | return(tv.tv_sec); 165 | } 166 | 167 | static u64 168 | get_file_size(int fd) 169 | { 170 | struct stat st = { 0 }; 171 | fstat(fd, &st); 172 | return(st.st_size); 173 | } 174 | 175 | static bool 176 | fd_valid(int fd) 177 | { 178 | return fcntl(fd, F_GETFD) != -1 || errno != EBADF; 179 | } 180 | 181 | static struct bc_str 182 | str_from_literal(char *literal) 183 | { 184 | struct bc_str result = { 0 }; 185 | 186 | result.data = literal; 187 | result.length = strlen(literal); 188 | 189 | return(result); 190 | } 191 | -------------------------------------------------------------------------------- /public/scripts/notifications.js: -------------------------------------------------------------------------------- 1 | let sent_sound = null; 2 | let sent_ready = false; 3 | 4 | let notification_sound = null; 5 | let notification_ready = false; 6 | let notification_timer = null; 7 | let notification_disabled = false; 8 | let notification_serviceworker_registration = null; 9 | 10 | let blinking_title_interval = null; 11 | let blinking_title_cancel_timeout = null; 12 | 13 | function sent_play() { 14 | if (sent_ready) { 15 | sent_sound.play(); 16 | } 17 | } 18 | 19 | function notification_toggle_sound(e) { 20 | notification_disabled = !e.checked; 21 | } 22 | 23 | function notification_init() { 24 | notification_sound = new Audio('/static/sounds/notification.wav'); 25 | notification_sound.addEventListener('canplaythrough', (e) => { 26 | notification_ready = true; 27 | }); 28 | 29 | navigator.serviceWorker.register('/static/scripts/service-worker.js', { scope: '/' }).then((r) => { 30 | notification_serviceworker_registration = r; 31 | }, (err) => { 32 | console.error(err); 33 | }); 34 | 35 | // sent_sound = new Audio('/static/sounds/select.wav'); 36 | // sent_sound.addEventListener('canplaythrough', (e) => { 37 | // sent_ready = true; 38 | // }); 39 | 40 | document.addEventListener("visibilitychange", () => { 41 | if (!document.hidden) { 42 | clearTimeout(blinking_title_cancel_timeout); 43 | clearInterval(blinking_title_interval); 44 | document.getElementById('favicon').href = '/static/icons/bullet.svg'; 45 | document.title = 'Bullet.Chat'; 46 | } 47 | }); 48 | } 49 | 50 | function notification_push(record) { 51 | if ('Notification' in window && window.Notification.permission === 'granted') { 52 | if (notification_serviceworker_registration) { 53 | if (record) { 54 | const author = ls_find_user(record.author_id); 55 | if (author) { 56 | notification_serviceworker_registration.showNotification( 57 | author.name, 58 | { 59 | 'body': record.text, 60 | 'timestamp': record.timestamp, 61 | }, 62 | 63 | ); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | function notification_all_read() { 71 | document.querySelectorAll('.toggle-sidebar').forEach(i => i.classList.remove('unread')); 72 | } 73 | 74 | function notification_new_unread(silent = false) { 75 | document.querySelectorAll('.toggle-sidebar').forEach(i => i.classList.add('unread')); 76 | 77 | if (!document.hidden) { 78 | return; 79 | } 80 | 81 | document.getElementById('favicon').href = '/static/icons/bullet_mark.svg'; 82 | 83 | if (silent) { 84 | return; 85 | } 86 | 87 | if (notification_ready && !notification_disabled) { 88 | if (notification_timer !== null) { 89 | clearTimeout(notification_timer); 90 | } 91 | 92 | notification_timer = setTimeout(() => { 93 | notification_sound.play(); 94 | notification_timer = null; 95 | }, 500); 96 | } 97 | 98 | notification_blink_tab_from_some_time(); 99 | } 100 | 101 | function notification_blink_tab_from_some_time(unread_count) { 102 | const old_title = 'Bullet.Chat'; 103 | let blink_step = 0; 104 | 105 | let unread_string = 'New messages'; 106 | 107 | clearTimeout(blinking_title_cancel_timeout); 108 | clearInterval(blinking_title_interval); 109 | 110 | blinking_title_interval = setInterval(() => { 111 | if (blink_step === 0) { 112 | document.title = unread_string; 113 | blink_step = 1; 114 | } else if (blink_step === 1) { 115 | document.title = old_title; 116 | blink_step = 0; 117 | } 118 | }, 800); 119 | 120 | blinking_title_cancel_timeout = setTimeout(() => { 121 | clearInterval(blinking_title_interval); 122 | document.title = old_title; 123 | }, 5000); 124 | } 125 | -------------------------------------------------------------------------------- /scripts/000-build-toolchain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # apt-get install gcc g++ make cmake git wget 4 | 5 | set -e 6 | # set -x 7 | 8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 9 | TOOLCHAIN_DIR=$SCRIPT_DIR/../toolchain 10 | 11 | OPENSSL_DIR=openssl-1.1.1v 12 | CURL_DIR=curl-8.2.1 13 | ECEC_DIR=ecec-master 14 | MUSL_DIR=x86_64-linux-musl-native 15 | LIBURING_DIR=liburing-2.4 16 | 17 | GCC_LIBS_DIR=gcc 18 | MUSL_LIBS_DIR=musl 19 | 20 | MAKEFLAGS="-j$(nproc) -l$(nproc)" 21 | export MAKEFLAGS 22 | 23 | DONE_MARKER=.done 24 | 25 | mkdir -p $TOOLCHAIN_DIR 26 | pushd $TOOLCHAIN_DIR 27 | 28 | if [ -f $DONE_MARKER ]; then 29 | echo 'Toolchain already built (.done file present). Exiting' 30 | exit 0 31 | fi 32 | 33 | rm -rf $GCC_LIBS_DIR $MUSL_LIBS_DIR $OPENSSL_DIR.tar.gz $CURL_DIR.tar.gz $ECEC_DIR.tar.gz $MUSL_DIR.tar.gz $LIBURING_DIR.tar.gz $OPENSSL_DIR $CURL_DIR $ECEC_DIR $MUSL_DIR $LIBURING_DIR 34 | 35 | wget -O $OPENSSL_DIR.tar.gz https://www.openssl.org/source/openssl-1.1.1v.tar.gz 36 | wget -O $CURL_DIR.tar.gz https://curl.se/download/curl-8.2.1.tar.gz 37 | wget -O $ECEC_DIR.tar.gz https://github.com/web-push-libs/ecec/archive/refs/heads/master.tar.gz 38 | wget -O $LIBURING_DIR.tar.gz https://github.com/axboe/liburing/archive/refs/tags/liburing-2.4.tar.gz 39 | wget -O $MUSL_DIR.tar.gz https://musl.cc/x86_64-linux-musl-native.tgz 40 | 41 | tar -xf $OPENSSL_DIR.tar.gz 42 | tar -xf $CURL_DIR.tar.gz 43 | tar -xf $ECEC_DIR.tar.gz 44 | tar -xf $LIBURING_DIR.tar.gz 45 | tar -xf $MUSL_DIR.tar.gz 46 | 47 | OPENSSL_FULLPATH=$(realpath $OPENSSL_DIR) 48 | CURL_FULLPATH=$(realpath $CURL_DIR) 49 | MUSL_FULLPATH=$(realpath $MUSL_DIR) 50 | 51 | mkdir $GCC_LIBS_DIR 52 | mkdir $MUSL_LIBS_DIR 53 | 54 | GCC_LIBRARY_PATH=$(realpath $GCC_LIBS_DIR) 55 | MUSL_LIBRARY_PATH=$(realpath $MUSL_LIBS_DIR) 56 | 57 | # Build openssl-1.1.1v with gcc 58 | pushd $OPENSSL_DIR 59 | ./config 60 | make 61 | cp *.so* $GCC_LIBRARY_PATH 62 | popd 63 | 64 | # Build libcurl with gcc (with dynamic openssl compiled by gcc) 65 | pushd $CURL_DIR 66 | LD_LIBRARY_PATH=$GCC_LIBRARY_PATH CPPFLAGS="-I$OPENSSL_FULLPATH/include" LDFLAGS="-L$GCC_LIBRARY_PATH" ./configure --with-openssl --disable-ldap 67 | LD_LIBRARY_PATH=$GCC_LIBRARY_PATH make 68 | cp lib/.libs/*.so* $GCC_LIBRARY_PATH 69 | make clean 70 | popd 71 | 72 | # Build static openssl with musl 73 | pushd $OPENSSL_DIR 74 | make clean 75 | CC=$MUSL_FULLPATH/bin/x86_64-linux-musl-gcc ./Configure linux-x86_64 76 | make 77 | cp *.a $MUSL_LIBRARY_PATH 78 | popd 79 | 80 | # Build static libcurl with musl (with static openssl compiled by musl) 81 | pushd $CURL_DIR 82 | LD_LIBRARY_PATH="$MUSL_LIBRARY_PATH:$LD_LIBRARY_PATH" CC=$MUSL_FULLPATH/bin/x86_64-linux-musl-gcc CPPFLAGS="-I$OPENSSL_FULLPATH/include" LDFLAGS="-static -L$MUSL_LIBRARY_PATH" ./configure --with-openssl -disable-shared --enable-static --disable-ldap 83 | make 84 | cp lib/.libs/*.a $MUSL_LIBRARY_PATH 85 | make clean 86 | popd 87 | 88 | # Build ecec with gcc (with openssl and libcurl compiled by gcc) 89 | pushd $ECEC_DIR 90 | gcc -Iinclude/ -I$CURL_FULLPATH/include -L$CURL_FULLPATH/lib/.libs/ -I$OPENSSL_FULLPATH/include -L$GCC_LIBRARY_PATH src/*.c -shared -o libecec.so -lcrypto 91 | cp libecec.so $GCC_LIBRARY_PATH 92 | 93 | # Build static ecec with musl (with openssl andlibcurl compiled by musl) 94 | mkdir -p build 95 | pushd build 96 | OPENSSL_ROOT_DIR=$OPENSSL_FULLPATH CC=$MUSL_FULLPATH/bin/x86_64-linux-musl-gcc cmake .. 97 | make 98 | cp libece.a $MUSL_LIBRARY_PATH 99 | popd 100 | popd 101 | 102 | # Build liburing (with gcc) 103 | pushd liburing-$LIBURING_DIR # idk why they called it liburing-liburing-2.4 104 | ./configure 105 | make 106 | cp src/liburing.so.2.4 $GCC_LIBRARY_PATH 107 | pushd $GCC_LIBRARY_PATH 108 | ln -s liburing.so.2.4 liburing.so.2 109 | ln -s liburing.so.2 liburing.so 110 | popd 111 | popd 112 | 113 | # Build liburing (with musl) 114 | pushd liburing-$LIBURING_DIR # idk why they called it liburing-liburing-2.4 115 | ./configure --cc=$MUSL_FULLPATH/bin/x86_64-linux-musl-gcc 116 | make clean 117 | make 118 | cp src/liburing.a $MUSL_LIBRARY_PATH 119 | popd 120 | 121 | touch $DONE_MARKER 122 | 123 | popd 124 | -------------------------------------------------------------------------------- /src/shared/buffer.c: -------------------------------------------------------------------------------- 1 | #define buffer_overhead (sizeof(s64) + sizeof(struct bc_vm)) 2 | #define buffer_vmp(buf) (struct bc_vm *) (POINTER_DEC(buf, buffer_overhead)) 3 | #define buffer_sizep(buf) (s64 *) (POINTER_DEC(buf, sizeof(s64))) 4 | #define buffer_size(buf) (buf ? *buffer_sizep(buf) : 0) 5 | 6 | #define buffer_maybegrow(buf, itemsize) do { \ 7 | u64 used = buffer_size(buf) * itemsize + buffer_overhead; \ 8 | struct bc_vm *vm = buffer_vmp(buf); \ 9 | if (used + itemsize > vm->commited) { \ 10 | u64 by = round_up_to_page_size(used + itemsize - vm->commited); \ 11 | buffer_grow((void **) &buf, by); \ 12 | } \ 13 | } while (0) 14 | 15 | #define buffer_push(buf, item) do { \ 16 | buffer_maybegrow(buf, sizeof(item)); \ 17 | u64 size__ = buffer_size(buf); \ 18 | buf[size__] = item; \ 19 | s64 *psize = buffer_sizep(buf); \ 20 | (*psize)++; \ 21 | } while (0) 22 | 23 | #define buffer_push_typeless(buf, data, size) do { \ 24 | buffer_maybegrow(buf, size); \ 25 | u64 buf_size = buffer_size(buf); \ 26 | memcpy(POINTER_INC(buf, size * buf_size), data, size); \ 27 | s64 *psize = buffer_sizep(buf); \ 28 | (*psize)++; \ 29 | } while (0) 30 | 31 | #define buffer_insert(buf, index, item) do { \ 32 | buffer_maybegrow(buf, sizeof(item)); \ 33 | u64 size = buffer_size(buf); \ 34 | memmove(buf + index + 1, buf + index, (size - index) * sizeof(item)); \ 35 | buf[index] = item; \ 36 | s64 *psize = buffer_sizep(buf); \ 37 | (*psize)++; \ 38 | } while (0) 39 | 40 | #define buffer_remove(buf, index) do { \ 41 | u64 size = buffer_size(buf); \ 42 | memmove(buf + index, buf + index + 1, (size - index - 1) * sizeof(*buf)); \ 43 | s64 *psize = buffer_sizep(buf); \ 44 | (*psize)--; \ 45 | } while (0) 46 | 47 | static void * 48 | buffer_init(u64 max_count, int itemsize) 49 | { 50 | u64 reserve_size = round_up_to_page_size(max_count * itemsize); 51 | struct bc_vm vm = mapping_reserve(reserve_size); 52 | if (!vm.size) return(0); 53 | if (!mapping_expand(&vm, PAGE_SIZE)) return(0); /* Commit one page immediately to use for metadata */ 54 | 55 | memcpy(vm.base, &vm, sizeof(vm)); /* vm metadata */ 56 | /* size = 0, so no need to memcpy anything */ 57 | 58 | return(vm.base + sizeof(vm) + sizeof(s64)); 59 | } 60 | 61 | // TODO: buffer_initzero (init + commit whole buffer) 62 | 63 | static void 64 | buffer_grow(void **data, u64 by) 65 | { 66 | /* Commit more */ 67 | u64 commit_size = MAX(by, PAGE_SIZE); 68 | struct bc_vm vm = { 0 }; 69 | memcpy(&vm, POINTER_DEC(*data, buffer_overhead), sizeof(vm)); 70 | if (!mapping_expand(&vm, commit_size)) { 71 | log_critical_die("Failed to expand buffer mapping. vm.reserve = %lu, vm.commited = %lu, commit_size = %lu\n", 72 | vm.size, vm.commited, commit_size); 73 | return; 74 | } 75 | } 76 | 77 | static void 78 | buffer_append(char *buf, void *data, s64 size) 79 | { 80 | s64 used = buffer_size(buf); 81 | struct bc_vm *vm = buffer_vmp(buf); 82 | if ((s64) (used + size + buffer_overhead) > (s64) vm->commited) { 83 | u64 by = round_up_to_page_size(used + size + buffer_overhead - vm->commited); 84 | buffer_grow((void **) &buf, by); 85 | } 86 | 87 | memcpy(buf + used, data, size); 88 | 89 | s64 *pused = buffer_sizep(buf); 90 | (*pused) += size; 91 | } 92 | static bool 93 | buffer_release(void *buf) 94 | { 95 | if (!buf) return(true); 96 | struct bc_vm *vm = buffer_vmp(buf); 97 | return(mapping_release(vm)); 98 | } -------------------------------------------------------------------------------- /public/styles/settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: var(--gap); 3 | } 4 | 5 | .settings h1 { 6 | margin: 0; 7 | font-size: 18px; 8 | font-family: 'Inter Semibold'; 9 | display: flex; 10 | gap: var(--halfgap); 11 | align-items: center; 12 | } 13 | 14 | .settings h1 img { 15 | height: 32px; 16 | } 17 | 18 | .settings-sections { 19 | display: flex; 20 | flex-direction: column; 21 | padding: var(--gap); 22 | padding-left: 0; 23 | gap: var(--gap); 24 | } 25 | 26 | .settings-section summary { 27 | box-sizing: border-box; 28 | border: 1px solid #eee; 29 | box-shadow: var(--subtle-shadow); 30 | cursor: pointer; 31 | user-select: none; 32 | -webkit-user-select: none; 33 | padding: var(--gap); 34 | color: var(--de-dark); 35 | border-radius: var(--radius); 36 | background: #eee; 37 | } 38 | 39 | .settings-section summary:focus { 40 | outline: none; 41 | } 42 | 43 | .settings-section-details { 44 | padding-top: var(--gap); 45 | display: flex; 46 | flex-direction: column; 47 | gap: var(--gap); 48 | /*margin-top: var(--halfgap);*/ 49 | /*box-sizing: border-box;*/ 50 | /*border: 1px solid var(--de-de-emph);*/ 51 | /*border-radius: var(--radius);*/ 52 | } 53 | 54 | .settings-avatar-wrapper { 55 | display: flex; 56 | gap: var(--gap); 57 | } 58 | 59 | .settings-avatar-wrapper .avatar-block { 60 | width: 128px; 61 | height: 128px; 62 | } 63 | 64 | .settings-avatar-wrapper .avatar-block img { 65 | width: 128px; 66 | height: 128px; 67 | border-radius: var(--radius); 68 | object-fit: cover; 69 | } 70 | 71 | .settings-avatar-wrapper .input-blocks { 72 | display: flex; 73 | flex-direction: column; 74 | justify-content: space-between; 75 | } 76 | 77 | .input-blocks .top { 78 | display: flex; 79 | flex-direction: column; 80 | gap: var(--halfgap); 81 | } 82 | 83 | .settings .labeled-input { 84 | display: flex; 85 | flex-direction: column; 86 | align-items: flex-start; 87 | gap: var(--hhgap); 88 | } 89 | 90 | .settings .labeled-input input[type=text] { 91 | font-family: sans-serif; 92 | border: 1px solid var(--de-de-emph); 93 | font-size: 14px; 94 | height: 30px; 95 | padding: 0; 96 | padding-left: 5px; 97 | padding-right: 5px; 98 | width: 40ch; 99 | } 100 | 101 | .settings .labeled-input input[type=text]:focus { 102 | outline: none; 103 | border: 1px solid var(--ascent-blue); 104 | } 105 | 106 | .settings .labeled-input .label-text { 107 | font-size: 13px; 108 | } 109 | 110 | .settings .me-line { 111 | display: flex; 112 | gap: var(--halfgap); 113 | } 114 | 115 | .settings .login-linkable { 116 | color: var(--de-emph); 117 | cursor: pointer; 118 | } 119 | 120 | .settings .actions { 121 | display: flex; 122 | gap: var(--halfgap); 123 | user-select: none; 124 | -webkit-user-select: none; 125 | align-items: center; 126 | } 127 | 128 | .settings .actions img { 129 | height: 14px; 130 | width: 14px; 131 | } 132 | 133 | .settings .actions .action { 134 | user-select: none; 135 | -webkit-user-select: none; 136 | cursor: pointer; 137 | padding: var(--radius); 138 | border-radius: var(--radius); 139 | } 140 | 141 | .settings h3 { 142 | margin: 0; 143 | } 144 | 145 | .settings .change-password-wrapper { 146 | display: flex; 147 | flex-direction: column; 148 | gap: var(--gap); 149 | } 150 | 151 | .settings .change-password-row { 152 | display: flex; 153 | gap: var(--gap); 154 | align-items: center; 155 | } 156 | 157 | .settings .change-password-wrapper label { 158 | width: 150px; 159 | } 160 | 161 | button#confirm-password-change { 162 | width: 100px; 163 | } 164 | 165 | @media (pointer:fine) { 166 | .settings .action:hover { 167 | background: var(--de-de-emph); 168 | } 169 | 170 | .settings .login-linkable:hover { 171 | text-decoration: underline; 172 | } 173 | 174 | .settings-section summary:hover { 175 | border-color: var(--ascent-blue); 176 | } 177 | 178 | .settings .action:active { 179 | background: #bbb; 180 | } 181 | } -------------------------------------------------------------------------------- /src/notification/connection.c: -------------------------------------------------------------------------------- 1 | static int 2 | _create_server_socket(const char *port) 3 | { 4 | int status = 0; 5 | 6 | struct addrinfo hints = { 0 }; 7 | struct addrinfo *servinfo = 0; 8 | 9 | hints.ai_family = AF_UNSPEC; /* Don't care if IPv4 or IPv6 */ 10 | hints.ai_socktype = SOCK_STREAM; /* TCP */ 11 | hints.ai_flags = AI_PASSIVE; /* Set my IP for me */ 12 | 13 | if ((status = getaddrinfo(0, port, &hints, &servinfo)) != 0) { 14 | log_error("getaddrinfo: %s\n", gai_strerror(status)); 15 | return(-1); 16 | } 17 | 18 | int result = -1; 19 | 20 | for (struct addrinfo *info = servinfo; info; info = info->ai_next) { 21 | result = socket(info->ai_family, info->ai_socktype, info->ai_protocol); 22 | 23 | if (result != -1) { 24 | int yes = 1; 25 | if (setsockopt(result, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1) { 26 | log_perror("setsockopt"); 27 | return(-1); 28 | } 29 | 30 | if (bind(result, info->ai_addr, info->ai_addrlen) == -1) { 31 | log_perror("bind"); 32 | return(-1); 33 | } 34 | 35 | if (listen(result, LISTEN_BACKLOG) == -1) { 36 | log_perror("bind"); 37 | return(-1); 38 | } 39 | 40 | break; 41 | } 42 | } 43 | 44 | freeaddrinfo(servinfo); 45 | 46 | return(result); 47 | } 48 | 49 | static struct bc_connection * 50 | connection_get(struct bc_notifier *server, int connection_socket) 51 | { 52 | for (int i = 0; i < buffer_size(server->connections); ++i) { 53 | struct bc_connection *connection = server->connections + i; 54 | if (connection->state != CONNECTION_CLOSED && connection->socket == connection_socket) { 55 | return(connection); 56 | } 57 | } 58 | 59 | return(0); 60 | } 61 | 62 | static struct bc_connection * 63 | connection_create(struct bc_notifier *server, int socket) 64 | { 65 | struct bc_connection *result = 0; 66 | 67 | for (int i = 0; i < buffer_size(server->connections); ++i) { 68 | struct bc_connection *connection = server->connections + i; 69 | if (connection->state == CONNECTION_CLOSED) { 70 | result = connection; 71 | break; 72 | } 73 | } 74 | 75 | if (!result) { 76 | struct bc_connection new_connection = { 0 }; 77 | buffer_push(server->connections, new_connection); 78 | result = server->connections + buffer_size(server->connections) - 1; 79 | } 80 | 81 | result->state = CONNECTION_CREATED; 82 | result->socket = socket; 83 | 84 | return(result); 85 | } 86 | 87 | static bool 88 | connection_init(struct bc_notifier *server, const char *port) 89 | { 90 | server->connections = buffer_init(MAX_POSSIBLE_CONNECTIONS, sizeof(struct bc_connection)); 91 | 92 | if (!server->connections) { 93 | log_error("Connection init failed"); 94 | return(false); 95 | } 96 | 97 | int server_socket = _create_server_socket(port); 98 | if (server_socket == -1) { 99 | log_error("Failed to create the websocket server socket\n"); 100 | return(false); 101 | } 102 | 103 | server->fd = server_socket; 104 | 105 | return(true); 106 | } 107 | 108 | static bool 109 | connection_remove(struct bc_notifier *server, int socket) 110 | { 111 | struct bc_connection *connection = connection_get(server, socket); 112 | 113 | if (connection) { 114 | connection->state = CONNECTION_CLOSED; 115 | return(true); 116 | } 117 | 118 | log_warning("Attempt to remove a non-existent connection\n"); 119 | 120 | return(false); 121 | } 122 | 123 | static void 124 | connection_drop(struct bc_notifier *server, struct bc_connection *connection) 125 | { 126 | int socket = connection->socket; 127 | 128 | if (socket != -1) { 129 | close(socket); 130 | close(connection->pipe_in); 131 | close(connection->pipe_out); 132 | connection_remove(server, socket); 133 | } 134 | } --------------------------------------------------------------------------------