From 5aa77335153dec68f2aa98e1fa2579fe4e1ceaab Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 11 Aug 2026 10:29:38 +0800 Subject: [PATCH] chore: sync project files --- .gitattributes | 0 .github/ISSUE_TEMPLATE/bug_report.md | 0 .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 0 .gitignore | 36 - CONTRIBUTING.md | 0 INSTALL.md | 0 LICENSE | 0 README_ko.md | 0 SECURITY.md | 0 bin/server | 0 bin/tinode-db | 0 build-all.sh | 0 build-py-grpc.sh | 0 chatbot/LICENSE | 0 chatbot/README.md | 0 chatbot/csharp/README.md | 0 chatbot/python/.gitignore | 0 chatbot/python/README.md | 0 chatbot/python/basic-cookie.sample | 0 chatbot/python/chatbot.py | 0 chatbot/python/quotes.txt | 0 chatbot/python/requirements.txt | 0 chatbot/python/setup.py | 0 chatbot/python/token-cookie.sample | 0 docker-build.sh | 0 docker-release.sh | 0 docker/README.md | 0 docker/chatbot/Dockerfile | 0 docker/docker-compose/README.md | 0 docker/docker-compose/cluster.mongodb.yml | 38 + docker/docker-compose/cluster.postgres.yml | 24 + docker/docker-compose/cluster.rethinkdb.yml | 24 + docker/docker-compose/cluster.yml | 187 +++ .../single-instance.mongodb.yml | 30 + .../single-instance.postgres.yml | 16 + .../single-instance.rethinkdb.yml | 16 + docker/docker-compose/single-instance.yml | 111 ++ docker/exporter/Dockerfile | 0 docker/exporter/entrypoint.sh | 0 docker/tinode/Dockerfile | 0 docker/tinode/config.template | 0 docker/tinode/entrypoint.sh | 0 docs/API-CN.md | 0 docs/API.md | 0 docs/CLA-CN.md | 0 docs/CLA.md | 0 docs/CODE-WALKTHROUGH.md | 0 docs/EXTEND-WEBSOCKET-CN.md | 0 docs/FEATURES.md | 0 docs/IM-WS-API-CN.md | 0 docs/IM-WS-TECH-DESIGN-CN.md | 0 docs/MESSAGE-PROTOCOL.md | 0 docs/MESSAGE-STORAGE.md | 0 docs/PROJECT-STRUCTURE.md | 0 docs/PROTOCOLS.md | 0 docs/REDIS-REFACTOR-CN.md | 0 docs/TEST-REPORT.md | 0 docs/TEST-RUN-2026-07-31-MYSQL-CN.md | 0 docs/TINODE-ANALYSIS.md | 0 docs/TOPICS-CN.md | 0 docs/android-chat.png | 0 docs/android-contacts.png | 0 docs/android-video-call.png | 0 docs/app-store.svg | 1 + docs/call-establishment-CN.md | 0 docs/call-establishment.md | 0 docs/drafty-CN.md | 0 docs/drafty.md | 0 docs/faq-CN.md | 0 docs/faq.md | 0 docs/ios-chat.png | 0 docs/ios-contacts.png | 0 docs/ios-pill-128.png | 0 docs/ios-video-call.png | 0 docs/logo.svg | 1 + docs/monitoring-CN.md | 0 docs/monitoring.md | 0 docs/play-store.svg | 178 +++ docs/tech-design/REDIS-REFACTOR.md | 0 docs/thecard-CN.md | 0 docs/thecard.md | 0 docs/translations-CN.md | 0 docs/translations.md | 0 docs/web-app.svg | 1 + docs/web-desktop.jpg | 0 docs/web-mob-chat.png | 0 docs/web-mob-contacts.png | 0 docs/web-mob-video-call.png | 0 go.mod | 121 ++ go.sum | 0 keygen/README.md | 0 keygen/keygen.go | 0 loadtest/LICENSE | 0 loadtest/README.md | 0 loadtest/loadtest.scala | 0 loadtest/tinode.beam | 0 loadtest/tinode.erl | 0 loadtest/tinode.scala | 0 loadtest/tsung.xml | 0 loadtest/users.csv | 0 monitoring/LICENSE | 0 monitoring/README.md | 0 monitoring/exporter/README.md | 0 monitoring/exporter/build.sh | 0 monitoring/exporter/influxdb_exporter.go | 118 ++ monitoring/exporter/main.go | 0 monitoring/exporter/prom_exporter.go | 0 monitoring/exporter/scraper.go | 0 pbx/README.md | 0 pbx/go-generate.sh | 0 pbx/model.pb.go | 0 pbx/model.proto | 0 pbx/model_grpc.pb.go | 0 pbx/py-generate.sh | 0 pbx/py_fix.py | 0 py_grpc/.gitignore | 0 py_grpc/LICENSE | 0 py_grpc/README.md | 0 py_grpc/pyproject.toml | 0 py_grpc/tinode_grpc/__init__.py | 0 py_grpc/tinode_grpc/model_pb2.py | 0 py_grpc/tinode_grpc/model_pb2.pyi | 0 py_grpc/tinode_grpc/model_pb2_grpc.py | 0 py_grpc/version.py | 0 rest-auth/README.md | 0 rest-auth/auth.py | 0 rest-auth/dummy_data.json | 0 rest-auth/requirements.txt | 0 server/.golangci.yml | 45 + server/api_key.go | 74 + server/auth/anon/auth_anon.go | 99 ++ server/auth/auth.go | 0 server/auth/basic/auth_basic.go | 0 server/auth/code/auth_code.go | 0 server/auth/mock_auth/mock_auth.go | 0 server/auth/rest/README.md | 0 server/auth/rest/auth_rest.go | 0 server/auth/token/auth_token.go | 0 server/calls.go | 0 server/cluster.go | 1265 +++++++++++++++++ server/cluster_leader.go | 0 server/concurrency/goroutinepool.go | 56 + server/concurrency/simplemutex.go | 30 + server/datamodel.go | 0 server/db/adapter.go | 0 server/db/common/common.go | 0 server/db/common/common_test.go | 0 server/db/common/test_data/test_data.go | 0 server/db/mongodb/adapter.go | 0 server/db/mongodb/blank.go | 7 + server/db/mongodb/schema.md | 0 server/db/mongodb/tests/mongo_test.go | 0 server/db/mongodb/tests/test.conf | 0 server/db/mysql/adapter.go | 0 server/db/mysql/blank.go | 8 + server/db/mysql/schema.sql | 0 server/db/mysql/tests/mysql_test.go | 0 server/db/mysql/tests/test.conf | 0 server/db/postgres/adapter.go | 0 server/db/postgres/blank.go | 8 + server/db/postgres/schema.sql | 0 server/db/postgres/tests/postgres_test.go | 0 server/db/postgres/tests/test.conf | 0 server/db/rethinkdb/adapter.go | 0 server/db/rethinkdb/blank.go | 8 + server/db/rethinkdb/schema.md | 0 server/db/rethinkdb/tests/rethink_test.go | 0 server/db/rethinkdb/tests/test.conf | 0 server/drafty/drafty.go | 0 server/drafty/drafty_test.go | 0 server/drafty/grapheme.go | 77 + server/hdl_files.go | 0 server/hdl_grpc.go | 0 server/hdl_longpoll.go | 0 server/hdl_websock.go | 0 server/http.go | 0 server/http_pprof.go | 53 + server/hub.go | 0 server/init_topic.go | 0 server/logs/logs.go | 55 + server/main.go | 0 server/media/fs/filesys.go | 0 server/media/media.go | 0 server/media/media_test.go | 0 server/media/s3/s3.go | 0 server/pbconverter.go | 0 server/plugins.go | 0 server/pres.go | 0 server/push.go | 0 server/push/common/typedef.go | 0 server/push/fcm/README.md | 0 server/push/fcm/payload.go | 0 server/push/fcm/push_fcm.go | 0 server/push/push.go | 0 server/push/stdout/README.md | 0 server/push/stdout/push_stdout.go | 98 ++ server/push/tnpg/README.md | 0 server/push/tnpg/push_tnpg.go | 0 server/ringhash/ringhash.go | 134 ++ server/ringhash/ringhash_test.go | 0 server/run-cluster.sh | 0 server/sanity-test.sh | 0 server/session.go | 0 server/session_test.go | 0 server/sessionstore.go | 0 server/stats.go | 167 +++ server/store/mock_store/mock_store.go | 0 server/store/store.go | 0 server/store/types/types.go | 0 server/store/types/uidgen.go | 105 ++ server/store/types/uidgen_test.go | 0 server/templ/email-password-reset-en.templ | 0 server/templ/email-password-reset-es.templ | 0 server/templ/email-password-reset-fr.templ | 0 server/templ/email-password-reset-pt.templ | 0 server/templ/email-password-reset-ru.templ | 0 server/templ/email-password-reset-uk.templ | 0 server/templ/email-password-reset-vi.templ | 0 server/templ/email-password-reset-zh-TW.templ | 0 server/templ/email-password-reset-zh.templ | 0 server/templ/email-validation-en.templ | 0 server/templ/email-validation-es.templ | 0 server/templ/email-validation-fr.templ | 0 server/templ/email-validation-pt.templ | 0 server/templ/email-validation-ru.templ | 0 server/templ/email-validation-uk.templ | 0 server/templ/email-validation-vi.templ | 0 server/templ/email-validation-zh-TW.templ | 0 server/templ/email-validation-zh.templ | 0 server/templ/sms-universal-en.templ | 0 server/templ/sms-universal-es.templ | 0 server/templ/sms-universal-fr.templ | 0 server/templ/sms-universal-pt.templ | 0 server/templ/sms-universal-ru.templ | 0 server/templ/sms-universal-uk.templ | 0 server/templ/sms-universal-vi.templ | 0 server/templ/sms-universal-zh-TW.templ | 0 server/templ/sms-universal-zh.templ | 0 server/tinode.conf | 0 server/topic.go | 0 server/topic_proxy.go | 0 server/topic_test.go | 0 server/user.go | 0 server/utils.go | 0 server/utils_test.go | 0 server/validate/email/validate.go | 0 server/validate/tel/twilio.go | 39 + server/validate/tel/validate.go | 0 server/validate/validator.go | 128 ++ templ/email-password-reset-en.templ | 0 templ/email-password-reset-es.templ | 0 templ/email-password-reset-fr.templ | 0 templ/email-password-reset-pt.templ | 0 templ/email-password-reset-ru.templ | 0 templ/email-password-reset-uk.templ | 0 templ/email-password-reset-vi.templ | 0 templ/email-password-reset-zh-TW.templ | 0 templ/email-password-reset-zh.templ | 0 templ/email-validation-en.templ | 0 templ/email-validation-es.templ | 0 templ/email-validation-fr.templ | 0 templ/email-validation-pt.templ | 0 templ/email-validation-ru.templ | 0 templ/email-validation-uk.templ | 0 templ/email-validation-vi.templ | 0 templ/email-validation-zh-TW.templ | 0 templ/email-validation-zh.templ | 0 templ/sms-universal-en.templ | 0 templ/sms-universal-es.templ | 0 templ/sms-universal-fr.templ | 0 templ/sms-universal-pt.templ | 0 templ/sms-universal-ru.templ | 0 templ/sms-universal-uk.templ | 0 templ/sms-universal-vi.templ | 0 templ/sms-universal-zh-TW.templ | 0 templ/sms-universal-zh.templ | 0 tinode-db/README.md | 0 tinode-db/abc-128.jpg | 0 tinode-db/abcdef-128.jpg | 0 tinode-db/alice-128.jpg | 0 tinode-db/bf-128.jpg | 0 tinode-db/bob-128.jpg | 0 tinode-db/carol-128.jpg | 0 tinode-db/chan-128.jpg | 0 tinode-db/credentials.sh | 0 tinode-db/data.json | 0 tinode-db/dave-128.jpg | 0 tinode-db/eve-128.jpg | 0 tinode-db/gendb.go | 0 tinode-db/generate_dataset.py | 0 tinode-db/main.go | 0 tinode-db/support-128.jpg | 0 tinode-db/tino-128.jpg | 0 tinode-db/tinode.conf | 0 tinode-db/xena-128.jpg | 0 tn-cli/CODE-STRUCTURE.md | 0 tn-cli/LICENSE | 0 tn-cli/README.md | 0 tn-cli/client.py | 0 tn-cli/commands.py | 0 tn-cli/input_handler.py | 0 tn-cli/macros.py | 0 tn-cli/requirements.txt | 0 tn-cli/sample-macro-script.txt | 0 tn-cli/sample-script.txt | 0 tn-cli/test-128.jpg | 0 tn-cli/tn-cli.py | 0 tn-cli/tn_globals.py | 0 tn-cli/utils.py | 0 310 files changed, 3323 insertions(+), 36 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 README_ko.md create mode 100644 SECURITY.md create mode 100644 bin/server create mode 100644 bin/tinode-db create mode 100644 build-all.sh create mode 100644 build-py-grpc.sh create mode 100644 chatbot/LICENSE create mode 100644 chatbot/README.md create mode 100644 chatbot/csharp/README.md create mode 100644 chatbot/python/.gitignore create mode 100644 chatbot/python/README.md create mode 100644 chatbot/python/basic-cookie.sample create mode 100644 chatbot/python/chatbot.py create mode 100644 chatbot/python/quotes.txt create mode 100644 chatbot/python/requirements.txt create mode 100644 chatbot/python/setup.py create mode 100644 chatbot/python/token-cookie.sample create mode 100644 docker-build.sh create mode 100644 docker-release.sh create mode 100644 docker/README.md create mode 100644 docker/chatbot/Dockerfile create mode 100644 docker/docker-compose/README.md create mode 100644 docker/docker-compose/cluster.mongodb.yml create mode 100644 docker/docker-compose/cluster.postgres.yml create mode 100644 docker/docker-compose/cluster.rethinkdb.yml create mode 100644 docker/docker-compose/cluster.yml create mode 100644 docker/docker-compose/single-instance.mongodb.yml create mode 100644 docker/docker-compose/single-instance.postgres.yml create mode 100644 docker/docker-compose/single-instance.rethinkdb.yml create mode 100644 docker/docker-compose/single-instance.yml create mode 100644 docker/exporter/Dockerfile create mode 100644 docker/exporter/entrypoint.sh create mode 100644 docker/tinode/Dockerfile create mode 100644 docker/tinode/config.template create mode 100644 docker/tinode/entrypoint.sh create mode 100644 docs/API-CN.md create mode 100644 docs/API.md create mode 100644 docs/CLA-CN.md create mode 100644 docs/CLA.md create mode 100644 docs/CODE-WALKTHROUGH.md create mode 100644 docs/EXTEND-WEBSOCKET-CN.md create mode 100644 docs/FEATURES.md create mode 100644 docs/IM-WS-API-CN.md create mode 100644 docs/IM-WS-TECH-DESIGN-CN.md create mode 100644 docs/MESSAGE-PROTOCOL.md create mode 100644 docs/MESSAGE-STORAGE.md create mode 100644 docs/PROJECT-STRUCTURE.md create mode 100644 docs/PROTOCOLS.md create mode 100644 docs/REDIS-REFACTOR-CN.md create mode 100644 docs/TEST-REPORT.md create mode 100644 docs/TEST-RUN-2026-07-31-MYSQL-CN.md create mode 100644 docs/TINODE-ANALYSIS.md create mode 100644 docs/TOPICS-CN.md create mode 100644 docs/android-chat.png create mode 100644 docs/android-contacts.png create mode 100644 docs/android-video-call.png create mode 100644 docs/app-store.svg create mode 100644 docs/call-establishment-CN.md create mode 100644 docs/call-establishment.md create mode 100644 docs/drafty-CN.md create mode 100644 docs/drafty.md create mode 100644 docs/faq-CN.md create mode 100644 docs/faq.md create mode 100644 docs/ios-chat.png create mode 100644 docs/ios-contacts.png create mode 100644 docs/ios-pill-128.png create mode 100644 docs/ios-video-call.png create mode 100644 docs/logo.svg create mode 100644 docs/monitoring-CN.md create mode 100644 docs/monitoring.md create mode 100644 docs/play-store.svg create mode 100644 docs/tech-design/REDIS-REFACTOR.md create mode 100644 docs/thecard-CN.md create mode 100644 docs/thecard.md create mode 100644 docs/translations-CN.md create mode 100644 docs/translations.md create mode 100644 docs/web-app.svg create mode 100644 docs/web-desktop.jpg create mode 100644 docs/web-mob-chat.png create mode 100644 docs/web-mob-contacts.png create mode 100644 docs/web-mob-video-call.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 keygen/README.md create mode 100644 keygen/keygen.go create mode 100644 loadtest/LICENSE create mode 100644 loadtest/README.md create mode 100644 loadtest/loadtest.scala create mode 100644 loadtest/tinode.beam create mode 100644 loadtest/tinode.erl create mode 100644 loadtest/tinode.scala create mode 100644 loadtest/tsung.xml create mode 100644 loadtest/users.csv create mode 100644 monitoring/LICENSE create mode 100644 monitoring/README.md create mode 100644 monitoring/exporter/README.md create mode 100644 monitoring/exporter/build.sh create mode 100644 monitoring/exporter/influxdb_exporter.go create mode 100644 monitoring/exporter/main.go create mode 100644 monitoring/exporter/prom_exporter.go create mode 100644 monitoring/exporter/scraper.go create mode 100644 pbx/README.md create mode 100644 pbx/go-generate.sh create mode 100644 pbx/model.pb.go create mode 100644 pbx/model.proto create mode 100644 pbx/model_grpc.pb.go create mode 100644 pbx/py-generate.sh create mode 100644 pbx/py_fix.py create mode 100644 py_grpc/.gitignore create mode 100644 py_grpc/LICENSE create mode 100644 py_grpc/README.md create mode 100644 py_grpc/pyproject.toml create mode 100644 py_grpc/tinode_grpc/__init__.py create mode 100644 py_grpc/tinode_grpc/model_pb2.py create mode 100644 py_grpc/tinode_grpc/model_pb2.pyi create mode 100644 py_grpc/tinode_grpc/model_pb2_grpc.py create mode 100644 py_grpc/version.py create mode 100644 rest-auth/README.md create mode 100644 rest-auth/auth.py create mode 100644 rest-auth/dummy_data.json create mode 100644 rest-auth/requirements.txt create mode 100644 server/.golangci.yml create mode 100644 server/api_key.go create mode 100644 server/auth/anon/auth_anon.go create mode 100644 server/auth/auth.go create mode 100644 server/auth/basic/auth_basic.go create mode 100644 server/auth/code/auth_code.go create mode 100644 server/auth/mock_auth/mock_auth.go create mode 100644 server/auth/rest/README.md create mode 100644 server/auth/rest/auth_rest.go create mode 100644 server/auth/token/auth_token.go create mode 100644 server/calls.go create mode 100644 server/cluster.go create mode 100644 server/cluster_leader.go create mode 100644 server/concurrency/goroutinepool.go create mode 100644 server/concurrency/simplemutex.go create mode 100644 server/datamodel.go create mode 100644 server/db/adapter.go create mode 100644 server/db/common/common.go create mode 100644 server/db/common/common_test.go create mode 100644 server/db/common/test_data/test_data.go create mode 100644 server/db/mongodb/adapter.go create mode 100644 server/db/mongodb/blank.go create mode 100644 server/db/mongodb/schema.md create mode 100644 server/db/mongodb/tests/mongo_test.go create mode 100644 server/db/mongodb/tests/test.conf create mode 100644 server/db/mysql/adapter.go create mode 100644 server/db/mysql/blank.go create mode 100644 server/db/mysql/schema.sql create mode 100644 server/db/mysql/tests/mysql_test.go create mode 100644 server/db/mysql/tests/test.conf create mode 100644 server/db/postgres/adapter.go create mode 100644 server/db/postgres/blank.go create mode 100644 server/db/postgres/schema.sql create mode 100644 server/db/postgres/tests/postgres_test.go create mode 100644 server/db/postgres/tests/test.conf create mode 100644 server/db/rethinkdb/adapter.go create mode 100644 server/db/rethinkdb/blank.go create mode 100644 server/db/rethinkdb/schema.md create mode 100644 server/db/rethinkdb/tests/rethink_test.go create mode 100644 server/db/rethinkdb/tests/test.conf create mode 100644 server/drafty/drafty.go create mode 100644 server/drafty/drafty_test.go create mode 100644 server/drafty/grapheme.go create mode 100644 server/hdl_files.go create mode 100644 server/hdl_grpc.go create mode 100644 server/hdl_longpoll.go create mode 100644 server/hdl_websock.go create mode 100644 server/http.go create mode 100644 server/http_pprof.go create mode 100644 server/hub.go create mode 100644 server/init_topic.go create mode 100644 server/logs/logs.go create mode 100644 server/main.go create mode 100644 server/media/fs/filesys.go create mode 100644 server/media/media.go create mode 100644 server/media/media_test.go create mode 100644 server/media/s3/s3.go create mode 100644 server/pbconverter.go create mode 100644 server/plugins.go create mode 100644 server/pres.go create mode 100644 server/push.go create mode 100644 server/push/common/typedef.go create mode 100644 server/push/fcm/README.md create mode 100644 server/push/fcm/payload.go create mode 100644 server/push/fcm/push_fcm.go create mode 100644 server/push/push.go create mode 100644 server/push/stdout/README.md create mode 100644 server/push/stdout/push_stdout.go create mode 100644 server/push/tnpg/README.md create mode 100644 server/push/tnpg/push_tnpg.go create mode 100644 server/ringhash/ringhash.go create mode 100644 server/ringhash/ringhash_test.go create mode 100644 server/run-cluster.sh create mode 100644 server/sanity-test.sh create mode 100644 server/session.go create mode 100644 server/session_test.go create mode 100644 server/sessionstore.go create mode 100644 server/stats.go create mode 100644 server/store/mock_store/mock_store.go create mode 100644 server/store/store.go create mode 100644 server/store/types/types.go create mode 100644 server/store/types/uidgen.go create mode 100644 server/store/types/uidgen_test.go create mode 100644 server/templ/email-password-reset-en.templ create mode 100644 server/templ/email-password-reset-es.templ create mode 100644 server/templ/email-password-reset-fr.templ create mode 100644 server/templ/email-password-reset-pt.templ create mode 100644 server/templ/email-password-reset-ru.templ create mode 100644 server/templ/email-password-reset-uk.templ create mode 100644 server/templ/email-password-reset-vi.templ create mode 100644 server/templ/email-password-reset-zh-TW.templ create mode 100644 server/templ/email-password-reset-zh.templ create mode 100644 server/templ/email-validation-en.templ create mode 100644 server/templ/email-validation-es.templ create mode 100644 server/templ/email-validation-fr.templ create mode 100644 server/templ/email-validation-pt.templ create mode 100644 server/templ/email-validation-ru.templ create mode 100644 server/templ/email-validation-uk.templ create mode 100644 server/templ/email-validation-vi.templ create mode 100644 server/templ/email-validation-zh-TW.templ create mode 100644 server/templ/email-validation-zh.templ create mode 100644 server/templ/sms-universal-en.templ create mode 100644 server/templ/sms-universal-es.templ create mode 100644 server/templ/sms-universal-fr.templ create mode 100644 server/templ/sms-universal-pt.templ create mode 100644 server/templ/sms-universal-ru.templ create mode 100644 server/templ/sms-universal-uk.templ create mode 100644 server/templ/sms-universal-vi.templ create mode 100644 server/templ/sms-universal-zh-TW.templ create mode 100644 server/templ/sms-universal-zh.templ create mode 100644 server/tinode.conf create mode 100644 server/topic.go create mode 100644 server/topic_proxy.go create mode 100644 server/topic_test.go create mode 100644 server/user.go create mode 100644 server/utils.go create mode 100644 server/utils_test.go create mode 100644 server/validate/email/validate.go create mode 100644 server/validate/tel/twilio.go create mode 100644 server/validate/tel/validate.go create mode 100644 server/validate/validator.go create mode 100644 templ/email-password-reset-en.templ create mode 100644 templ/email-password-reset-es.templ create mode 100644 templ/email-password-reset-fr.templ create mode 100644 templ/email-password-reset-pt.templ create mode 100644 templ/email-password-reset-ru.templ create mode 100644 templ/email-password-reset-uk.templ create mode 100644 templ/email-password-reset-vi.templ create mode 100644 templ/email-password-reset-zh-TW.templ create mode 100644 templ/email-password-reset-zh.templ create mode 100644 templ/email-validation-en.templ create mode 100644 templ/email-validation-es.templ create mode 100644 templ/email-validation-fr.templ create mode 100644 templ/email-validation-pt.templ create mode 100644 templ/email-validation-ru.templ create mode 100644 templ/email-validation-uk.templ create mode 100644 templ/email-validation-vi.templ create mode 100644 templ/email-validation-zh-TW.templ create mode 100644 templ/email-validation-zh.templ create mode 100644 templ/sms-universal-en.templ create mode 100644 templ/sms-universal-es.templ create mode 100644 templ/sms-universal-fr.templ create mode 100644 templ/sms-universal-pt.templ create mode 100644 templ/sms-universal-ru.templ create mode 100644 templ/sms-universal-uk.templ create mode 100644 templ/sms-universal-vi.templ create mode 100644 templ/sms-universal-zh-TW.templ create mode 100644 templ/sms-universal-zh.templ create mode 100644 tinode-db/README.md create mode 100644 tinode-db/abc-128.jpg create mode 100644 tinode-db/abcdef-128.jpg create mode 100644 tinode-db/alice-128.jpg create mode 100644 tinode-db/bf-128.jpg create mode 100644 tinode-db/bob-128.jpg create mode 100644 tinode-db/carol-128.jpg create mode 100644 tinode-db/chan-128.jpg create mode 100644 tinode-db/credentials.sh create mode 100644 tinode-db/data.json create mode 100644 tinode-db/dave-128.jpg create mode 100644 tinode-db/eve-128.jpg create mode 100644 tinode-db/gendb.go create mode 100644 tinode-db/generate_dataset.py create mode 100644 tinode-db/main.go create mode 100644 tinode-db/support-128.jpg create mode 100644 tinode-db/tino-128.jpg create mode 100644 tinode-db/tinode.conf create mode 100644 tinode-db/xena-128.jpg create mode 100644 tn-cli/CODE-STRUCTURE.md create mode 100644 tn-cli/LICENSE create mode 100644 tn-cli/README.md create mode 100644 tn-cli/client.py create mode 100644 tn-cli/commands.py create mode 100644 tn-cli/input_handler.py create mode 100644 tn-cli/macros.py create mode 100644 tn-cli/requirements.txt create mode 100644 tn-cli/sample-macro-script.txt create mode 100644 tn-cli/sample-script.txt create mode 100644 tn-cli/test-128.jpg create mode 100644 tn-cli/tn-cli.py create mode 100644 tn-cli/tn_globals.py create mode 100644 tn-cli/utils.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e69de29 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 92aa957..0000000 --- a/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -go.work.sum - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# env file -.env - -# ---> TortoiseGit -# Project-level settings -/.tgitconfig diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README_ko.md b/README_ko.md new file mode 100644 index 0000000..e69de29 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e69de29 diff --git a/bin/server b/bin/server new file mode 100644 index 0000000..e69de29 diff --git a/bin/tinode-db b/bin/tinode-db new file mode 100644 index 0000000..e69de29 diff --git a/build-all.sh b/build-all.sh new file mode 100644 index 0000000..e69de29 diff --git a/build-py-grpc.sh b/build-py-grpc.sh new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/LICENSE b/chatbot/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/README.md b/chatbot/README.md new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/csharp/README.md b/chatbot/csharp/README.md new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/.gitignore b/chatbot/python/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/README.md b/chatbot/python/README.md new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/basic-cookie.sample b/chatbot/python/basic-cookie.sample new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/chatbot.py b/chatbot/python/chatbot.py new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/quotes.txt b/chatbot/python/quotes.txt new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/requirements.txt b/chatbot/python/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/setup.py b/chatbot/python/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/chatbot/python/token-cookie.sample b/chatbot/python/token-cookie.sample new file mode 100644 index 0000000..e69de29 diff --git a/docker-build.sh b/docker-build.sh new file mode 100644 index 0000000..e69de29 diff --git a/docker-release.sh b/docker-release.sh new file mode 100644 index 0000000..e69de29 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker/chatbot/Dockerfile b/docker/chatbot/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docker/docker-compose/README.md b/docker/docker-compose/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker/docker-compose/cluster.mongodb.yml b/docker/docker-compose/cluster.mongodb.yml new file mode 100644 index 0000000..7a66691 --- /dev/null +++ b/docker/docker-compose/cluster.mongodb.yml @@ -0,0 +1,38 @@ +version: '3.8' + +x-mongodb-tinode-env-vars: &mongodb-tinode-env-vars + "STORE_USE_ADAPTER": "mongodb" + +services: + db: + image: mongo:4.2.3 + container_name: mongodb + entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs0" ] + healthcheck: + test: ["CMD", "curl -f http://localhost:28017/ || exit 1"] + + # Initializes MongoDb replicaset. + initdb: + image: mongo:4.2.3 + container_name: initdb + depends_on: + - db + command: > + bash -c "echo 'Starting replica set initialize'; + until mongo --host mongodb --eval 'print(\"waited for connection\")'; do sleep 2; done; + echo 'Connection finished'; + echo 'Creating replica set'; + echo \"rs.initiate({'_id': 'rs0', "members": [ {'_id': 0, 'host': 'mongodb:27017'} ]})\" | mongo --host mongodb" + + tinode-0: + environment: + << : *mongodb-tinode-env-vars + "WAIT_FOR": "mongodb:27017" + + tinode-1: + environment: + << : *mongodb-tinode-env-vars + + tinode-2: + environment: + << : *mongodb-tinode-env-vars diff --git a/docker/docker-compose/cluster.postgres.yml b/docker/docker-compose/cluster.postgres.yml new file mode 100644 index 0000000..dbf9ee8 --- /dev/null +++ b/docker/docker-compose/cluster.postgres.yml @@ -0,0 +1,24 @@ +version: '3.8' + +x-postgres-tinode-env-vars: &postgres-tinode-env-vars + "STORE_USE_ADAPTER": "postgres" + +services: + db: + image: postgres:15.2 + container_name: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + + tinode-0: + environment: + << : *postgres-tinode-env-vars + "WAIT_FOR": "postgres:5432" + + tinode-1: + environment: + << : *postgres-tinode-env-vars + + tinode-2: + environment: + << : *postgres-tinode-env-vars diff --git a/docker/docker-compose/cluster.rethinkdb.yml b/docker/docker-compose/cluster.rethinkdb.yml new file mode 100644 index 0000000..68b0a93 --- /dev/null +++ b/docker/docker-compose/cluster.rethinkdb.yml @@ -0,0 +1,24 @@ +version: '3.8' + +x-rethinkdb-tinode-env-vars: &rethinkdb-tinode-env-vars + "STORE_USE_ADAPTER": "rethinkdb" + +services: + db: + image: rethinkdb:2.4.2 + container_name: rethinkdb + healthcheck: + test: ["CMD", "curl -f http://localhost:8080/ || exit 1"] + + tinode-0: + environment: + << : *rethinkdb-tinode-env-vars + "WAIT_FOR": "rethinkdb:8080" + + tinode-1: + environment: + << : *rethinkdb-tinode-env-vars + + tinode-2: + environment: + << : *rethinkdb-tinode-env-vars diff --git a/docker/docker-compose/cluster.yml b/docker/docker-compose/cluster.yml new file mode 100644 index 0000000..b0549c5 --- /dev/null +++ b/docker/docker-compose/cluster.yml @@ -0,0 +1,187 @@ +# Reference configuration for a simple 3-node Tinode cluster. +# Includes: +# * Mysql database +# * 3 Tinode servers +# * 3 exporters + +version: '3.8' + +# Base Tinode template. +x-tinode: + &tinode-base + depends_on: + - db + image: tinode/tinode:latest + restart: always + +x-exporter: + &exporter-base + image: tinode/exporter:latest + restart: always + +x-tinode-env-vars: &tinode-env-vars + "STORE_USE_ADAPTER": "mysql" + "PPROF_URL": "/pprof" + # You can provide your own tinode config by setting EXT_CONFIG env var and binding your configuration file to + # "EXT_CONFIG": "/etc/tinode/tinode.conf" + "WAIT_FOR": "mysql:3306" + # Push notifications. + # Modify as appropriate. + # Tinode Push Gateway configuration. + "TNPG_PUSH_ENABLED": "false" + # "TNPG_USER": "" + # "TNPG_AUTH_TOKEN": "" + # FCM specific server configuration. + "FCM_PUSH_ENABLED": "false" + # "FCM_CRED_FILE": "" + # "FCM_INCLUDE_ANDROID_NOTIFICATION": false + # + # FCM Web client configuration. + "FCM_API_KEY": "AIzaSyD6X4ULR-RUsobvs1zZ2bHdJuPz39q2tbQ" + "FCM_APP_ID": "1:114126160546:web:aca6ea2981feb81fb44dfb" + "FCM_PROJECT_ID": "tinode-1000" + "FCM_SENDER_ID": 114126160546 + "FCM_VAPID_KEY": "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58" + "FCM_MEASUREMENT_ID": "G-WNJDQR34L3" + # iOS app universal links configuration. + # "IOS_UNIV_LINKS_APP_ID": "" + # Video calls + "WEBRTC_ENABLED": "false" + # "ICE_SERVERS_FILE": "" + +x-exporter-env-vars: &exporter-env-vars + "TINODE_ADDR": "http://tinode.host:18080/stats/expvar/" + # InfluxDB configation: + "SERVE_FOR": "influxdb" + "INFLUXDB_VERSION": 1.7 + "INFLUXDB_ORGANIZATION": "" + "INFLUXDB_PUSH_INTERVAL": 30 + "INFLUXDB_PUSH_ADDRESS": "https://mon.tinode.co/intake" + "INFLUXDB_AUTH_TOKEN": "" + # Prometheus configuration: + # "SERVE_FOR": "prometheus" + # "PROM_NAMESPACE": "tinode" + # "PROM_METRICS_PATH": "/metrics" + +services: + db: + image: mysql:8.0 + container_name: mysql + restart: always + # Use your own volume. + # volumes: + # - :/var/lib/mysql + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 5s + retries: 10 + security_opt: + - seccomp=unconfined + + # Tinode servers. + tinode-0: + << : *tinode-base + container_name: tinode-0 + hostname: tinode-0 + # You can mount your volumes as necessary: + # volumes: + # # E.g. external config (assuming EXT_CONFIG is set). + # - :/etc/tinode/tinode.conf + # # Logs directory. + # - :/var/log + ports: + - "6060:6060" + environment: + << : *tinode-env-vars + "CLUSTER_SELF": "tinode-0" + "RESET_DB": ${RESET_DB:-false} + "UPGRADE_DB": ${UPGRADE_DB:-false} + + tinode-1: + << : *tinode-base + container_name: tinode-1 + hostname: tinode-1 + # You can mount your volumes as necessary: + # volumes: + # # E.g. external config (assuming EXT_CONFIG is set). + # - :/etc/tinode/tinode.conf + # # Logs directory. + # - :/var/log + ports: + - "6061:6060" + environment: + << : *tinode-env-vars + "CLUSTER_SELF": "tinode-1" + # Wait for tinode-0, not the database since + # we let tinode-0 perform all database initialization and upgrade work. + "WAIT_FOR": "tinode-0:6060" + "NO_DB_INIT": "true" + + tinode-2: + << : *tinode-base + container_name: tinode-2 + hostname: tinode-2 + # You can mount your volumes as necessary: + # volumes: + # # E.g. external config (assuming EXT_CONFIG is set). + # - :/etc/tinode/tinode.conf + # # Logs directory. + # - :/var/log + ports: + - "6062:6060" + environment: + << : *tinode-env-vars + "CLUSTER_SELF": "tinode-2" + # Wait for tinode-0, not the database since + # we let tinode-0 perform all database initialization and upgrade work. + "WAIT_FOR": "tinode-0:6060" + "NO_DB_INIT": "true" + + # Monitoring. + # Exporters are paired with tinode instances. + exporter-0: + << : *exporter-base + container_name: exporter-0 + hostname: exporter-0 + depends_on: + - tinode-0 + ports: + - 6222:6222 + links: + - tinode-0:tinode.host + environment: + << : *exporter-env-vars + "INSTANCE": "tinode-0" + "WAIT_FOR": "tinode-0:6060" + + exporter-1: + << : *exporter-base + container_name: exporter-1 + hostname: exporter-1 + depends_on: + - tinode-1 + ports: + - 6223:6222 + links: + - tinode-1:tinode.host + environment: + << : *exporter-env-vars + "INSTANCE": "tinode-1" + "WAIT_FOR": "tinode-1:6060" + + exporter-2: + << : *exporter-base + container_name: exporter-2 + hostname: exporter-2 + depends_on: + - tinode-2 + ports: + - 6224:6222 + links: + - tinode-2:tinode.host + environment: + << : *exporter-env-vars + "INSTANCE": "tinode-2" + "WAIT_FOR": "tinode-2:6060" diff --git a/docker/docker-compose/single-instance.mongodb.yml b/docker/docker-compose/single-instance.mongodb.yml new file mode 100644 index 0000000..9233aa4 --- /dev/null +++ b/docker/docker-compose/single-instance.mongodb.yml @@ -0,0 +1,30 @@ +version: '3.8' + +x-mongodb-tinode-env-vars: &mongodb-tinode-env-vars + "STORE_USE_ADAPTER": "mongodb" + +services: + db: + image: mongo:4.2.3 + container_name: mongodb + entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs0" ] + healthcheck: + test: ["CMD", "curl -f http://localhost:28017/ || exit 1"] + + # Initializes MongoDb replicaset. + initdb: + image: mongo:4.2.3 + container_name: initdb + depends_on: + - db + command: > + bash -c "echo 'Starting replica set initialize'; + until mongo --host mongodb --eval 'print(\"waited for connection\")'; do sleep 2; done; + echo 'Connection finished'; + echo 'Creating replica set'; + echo \"rs.initiate({'_id': 'rs0', "members": [ {'_id': 0, 'host': 'mongodb:27017'} ]})\" | mongo --host mongodb" + + tinode-0: + environment: + << : *mongodb-tinode-env-vars + "WAIT_FOR": "mongodb:27017" diff --git a/docker/docker-compose/single-instance.postgres.yml b/docker/docker-compose/single-instance.postgres.yml new file mode 100644 index 0000000..ef7e184 --- /dev/null +++ b/docker/docker-compose/single-instance.postgres.yml @@ -0,0 +1,16 @@ +version: '3.8' + +x-postgres-tinode-env-vars: &postgres-tinode-env-vars + "STORE_USE_ADAPTER": "postgres" + +services: + db: + image: postgres:15.2 + container_name: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + + tinode-0: + environment: + << : *postgres-tinode-env-vars + "WAIT_FOR": "postgres:5432" diff --git a/docker/docker-compose/single-instance.rethinkdb.yml b/docker/docker-compose/single-instance.rethinkdb.yml new file mode 100644 index 0000000..f3df412 --- /dev/null +++ b/docker/docker-compose/single-instance.rethinkdb.yml @@ -0,0 +1,16 @@ +version: '3.8' + +x-rethinkdb-tinode-env-vars: &rethinkdb-tinode-env-vars + "STORE_USE_ADAPTER": "rethinkdb" + +services: + db: + image: rethinkdb:2.4.0 + container_name: rethinkdb + healthcheck: + test: ["CMD", "curl -f http://localhost:8080/ || exit 1"] + + tinode-0: + environment: + << : *rethinkdb-tinode-env-vars + "WAIT_FOR": "rethinkdb:8080" diff --git a/docker/docker-compose/single-instance.yml b/docker/docker-compose/single-instance.yml new file mode 100644 index 0000000..d38cb8b --- /dev/null +++ b/docker/docker-compose/single-instance.yml @@ -0,0 +1,111 @@ +# Reference configuration for a simple Tinode server. +# Includes: +# * Mysql database +# * Tinode server +# * Tinode exporters + +version: '3.8' + +# Base Tinode template. +x-tinode: + &tinode-base + depends_on: + - db + image: tinode/tinode:latest + restart: always + +x-tinode-env-vars: &tinode-env-vars + "STORE_USE_ADAPTER": "mysql" + "PPROF_URL": "/pprof" + # You can provide your own tinode config by setting EXT_CONFIG env var and binding your configuration file to + # "EXT_CONFIG": "/etc/tinode/tinode.conf" + "WAIT_FOR": "mysql:3306" + # Push notifications. + # Modify as appropriate. + # Tinode Push Gateway configuration. + "TNPG_PUSH_ENABLED": "false" + # "TNPG_USER": "" + # "TNPG_AUTH_TOKEN": "" + # FCM specific server configuration. + "FCM_PUSH_ENABLED": "false" + # "FCM_CRED_FILE": "" + # "FCM_INCLUDE_ANDROID_NOTIFICATION": false + # + # FCM Web client configuration. + "FCM_API_KEY": "AIzaSyD6X4ULR-RUsobvs1zZ2bHdJuPz39q2tbQ" + "FCM_APP_ID": "1:114126160546:web:aca6ea2981feb81fb44dfb" + "FCM_PROJECT_ID": "tinode-1000" + "FCM_SENDER_ID": 114126160546 + "FCM_VAPID_KEY": "BOgQVPOMzIMXUpsYGpbVkZoEBc0ifKY_f2kSU5DNDGYI6i6CoKqqxDd7w7PJ3FaGRBgVGJffldETumOx831jl58" + "FCM_MEASUREMENT_ID": "G-WNJDQR34L3" + # iOS app universal links configuration. + # "IOS_UNIV_LINKS_APP_ID": "" + # Video calls + "WEBRTC_ENABLED": "false" + # "ICE_SERVERS_FILE": "" + +x-exporter-env-vars: &exporter-env-vars + "TINODE_ADDR": "http://tinode.host:6060/stats/expvar/" + # InfluxDB configation: + "SERVE_FOR": "influxdb" + "INFLUXDB_VERSION": 1.7 + "INFLUXDB_ORGANIZATION": "" + "INFLUXDB_PUSH_INTERVAL": 30 + "INFLUXDB_PUSH_ADDRESS": "https://mon.tinode.co/intake" + "INFLUXDB_AUTH_TOKEN": "" + # Prometheus configuration: + # "SERVE_FOR": "prometheus" + # "PROM_NAMESPACE": "tinode" + # "PROM_METRICS_PATH": "/metrics" + +services: + db: + image: mysql:8.0 + container_name: mysql + restart: always + # Use your own volume. + # volumes: + # - :/var/lib/mysql + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 5s + retries: 10 + security_opt: + - seccomp=unconfined + + # Tinode. + tinode-0: + << : *tinode-base + container_name: tinode-0 + hostname: tinode-0 + # You can mount your volumes as necessary: + # volumes: + # # E.g. external config (assuming EXT_CONFIG is set). + # - :/etc/tinode/tinode.conf + # # Logs directory. + # - :/var/log + ports: + - "6060:6060" + environment: + << : *tinode-env-vars + "RESET_DB": ${RESET_DB:-false} + "UPGRADE_DB": ${UPGRADE_DB:-false} + + # Monitoring. + # Exporters are paired with tinode instances. + exporter-0: + container_name: exporter-0 + hostname: exporter-0 + depends_on: + - tinode-0 + image: tinode/exporter:latest + restart: always + ports: + - "6222:6222" + links: + - tinode-0:tinode.host + environment: + << : *exporter-env-vars + "WAIT_FOR": "tinode-0:6060" diff --git a/docker/exporter/Dockerfile b/docker/exporter/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docker/exporter/entrypoint.sh b/docker/exporter/entrypoint.sh new file mode 100644 index 0000000..e69de29 diff --git a/docker/tinode/Dockerfile b/docker/tinode/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/docker/tinode/config.template b/docker/tinode/config.template new file mode 100644 index 0000000..e69de29 diff --git a/docker/tinode/entrypoint.sh b/docker/tinode/entrypoint.sh new file mode 100644 index 0000000..e69de29 diff --git a/docs/API-CN.md b/docs/API-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/CLA-CN.md b/docs/CLA-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/CLA.md b/docs/CLA.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/CODE-WALKTHROUGH.md b/docs/CODE-WALKTHROUGH.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/EXTEND-WEBSOCKET-CN.md b/docs/EXTEND-WEBSOCKET-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/IM-WS-API-CN.md b/docs/IM-WS-API-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/IM-WS-TECH-DESIGN-CN.md b/docs/IM-WS-TECH-DESIGN-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MESSAGE-PROTOCOL.md b/docs/MESSAGE-PROTOCOL.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/MESSAGE-STORAGE.md b/docs/MESSAGE-STORAGE.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/PROTOCOLS.md b/docs/PROTOCOLS.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/REDIS-REFACTOR-CN.md b/docs/REDIS-REFACTOR-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/TEST-REPORT.md b/docs/TEST-REPORT.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/TEST-RUN-2026-07-31-MYSQL-CN.md b/docs/TEST-RUN-2026-07-31-MYSQL-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/TINODE-ANALYSIS.md b/docs/TINODE-ANALYSIS.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/TOPICS-CN.md b/docs/TOPICS-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/android-chat.png b/docs/android-chat.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/android-contacts.png b/docs/android-contacts.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/android-video-call.png b/docs/android-video-call.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/app-store.svg b/docs/app-store.svg new file mode 100644 index 0000000..dd3811e --- /dev/null +++ b/docs/app-store.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/call-establishment-CN.md b/docs/call-establishment-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/call-establishment.md b/docs/call-establishment.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/drafty-CN.md b/docs/drafty-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/drafty.md b/docs/drafty.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/faq-CN.md b/docs/faq-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/ios-chat.png b/docs/ios-chat.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/ios-contacts.png b/docs/ios-contacts.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/ios-pill-128.png b/docs/ios-pill-128.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/ios-video-call.png b/docs/ios-video-call.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..69d84b9 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1 @@ +t \ No newline at end of file diff --git a/docs/monitoring-CN.md b/docs/monitoring-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/monitoring.md b/docs/monitoring.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/play-store.svg b/docs/play-store.svg new file mode 100644 index 0000000..5b08dde --- /dev/null +++ b/docs/play-store.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/tech-design/REDIS-REFACTOR.md b/docs/tech-design/REDIS-REFACTOR.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/thecard-CN.md b/docs/thecard-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/thecard.md b/docs/thecard.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/translations-CN.md b/docs/translations-CN.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/translations.md b/docs/translations.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/web-app.svg b/docs/web-app.svg new file mode 100644 index 0000000..bb0cd54 --- /dev/null +++ b/docs/web-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/web-desktop.jpg b/docs/web-desktop.jpg new file mode 100644 index 0000000..e69de29 diff --git a/docs/web-mob-chat.png b/docs/web-mob-chat.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/web-mob-contacts.png b/docs/web-mob-contacts.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/web-mob-video-call.png b/docs/web-mob-video-call.png new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a5a33a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,121 @@ +module github.com/tinode/chat + +go 1.26.0 + +require ( + firebase.google.com/go v3.13.0+incompatible + github.com/aws/aws-sdk-go-v2 v1.42.1 + github.com/aws/aws-sdk-go-v2/config v1.32.27 + github.com/aws/aws-sdk-go-v2/credentials v1.19.26 + github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.13 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.2 + github.com/aws/smithy-go v1.27.3 + github.com/go-sql-driver/mysql v1.10.0 + github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.7.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/websocket v1.5.3 + github.com/jackc/pgx/v5 v5.10.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/nyaruka/phonenumbers v1.8.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.69.0 + github.com/rivo/uniseg v0.4.7 + github.com/tinode/jsonco v1.0.0 + github.com/tinode/snowflake v1.0.0 + go.mongodb.org/mongo-driver v1.17.9 + golang.org/x/crypto v0.53.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/text v0.38.0 + google.golang.org/api v0.287.0 + google.golang.org/grpc v1.82.0 + google.golang.org/protobuf v1.36.11 + gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2 +) + +require ( + cel.dev/expr v0.25.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/monitoring v1.29.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.5 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + golang.org/x/net v0.56.0 // indirect +) + +require ( + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/firestore v1.22.0 // indirect + cloud.google.com/go/iam v1.11.0 // indirect + cloud.google.com/go/longrunning v1.1.0 // indirect + cloud.google.com/go/storage v1.63.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/klauspost/compress v1.19.0 // indirect + github.com/montanaflynn/stats v0.9.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/procfs v0.21.1 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/twilio/twilio-go v1.30.9 + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect + gopkg.in/cenkalti/backoff.v2 v2.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/keygen/README.md b/keygen/README.md new file mode 100644 index 0000000..e69de29 diff --git a/keygen/keygen.go b/keygen/keygen.go new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/LICENSE b/loadtest/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/README.md b/loadtest/README.md new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/loadtest.scala b/loadtest/loadtest.scala new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/tinode.beam b/loadtest/tinode.beam new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/tinode.erl b/loadtest/tinode.erl new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/tinode.scala b/loadtest/tinode.scala new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/tsung.xml b/loadtest/tsung.xml new file mode 100644 index 0000000..e69de29 diff --git a/loadtest/users.csv b/loadtest/users.csv new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/LICENSE b/monitoring/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/README.md b/monitoring/README.md new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/exporter/README.md b/monitoring/exporter/README.md new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/exporter/build.sh b/monitoring/exporter/build.sh new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/exporter/influxdb_exporter.go b/monitoring/exporter/influxdb_exporter.go new file mode 100644 index 0000000..f83ce66 --- /dev/null +++ b/monitoring/exporter/influxdb_exporter.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" +) + +// InfluxDBExporter collects metrics from a Tinode server and pushes them to InfluxDB. +type InfluxDBExporter struct { + targetAddress string + organization string + bucket string + tokenHeader string + instance string + scraper *Scraper +} + +// NewInfluxDBExporter returns an initialized InfluxDB exporter. +func NewInfluxDBExporter(influxDBVersion, pushBaseAddress, organization, + bucket, token, instance string, scraper *Scraper) *InfluxDBExporter { + + targetAddress := formPushTargetAddress(influxDBVersion, pushBaseAddress, organization, bucket) + tokenHeader := formAuthorizationHeaderValue(influxDBVersion, token) + return &InfluxDBExporter{ + targetAddress: targetAddress, + organization: organization, + bucket: bucket, + tokenHeader: tokenHeader, + instance: instance, + scraper: scraper, + } +} + +// Push scrapes metrics from Tinode server and pushes these metrics to InfluxDB. +func (e *InfluxDBExporter) Push() error { + metrics, err := e.scraper.CollectRaw() + if err != nil { + return err + } + b := new(bytes.Buffer) + ts := time.Now().UnixNano() + for k, v := range metrics { + switch val := v.(type) { + case float64: + fmt.Fprintf(b, "%s,instance=%s value=%f %d\n", k, e.instance, val, ts) + case *histogram: + fmt.Fprintf(b, "%s,instance=%s count=%d %d\n", k, e.instance, val.count, ts) + fmt.Fprintf(b, "%s,instance=%s sum=%f %d\n", k, e.instance, val.sum, ts) + for bucket, count := range val.buckets { + fmt.Fprintf(b, "%s,instance=%s le=%f,value=%d %d\n", k, e.instance, bucket, count, ts) + } + default: + log.Panicln("Invalid metric type: ", v) + } + } + req, err := http.NewRequest("POST", e.targetAddress, b) + if err != nil { + return err + } + req.Header.Add("Authorization", e.tokenHeader) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + var body string + if rb, err := io.ReadAll(resp.Body); err != nil { + body = err.Error() + } else { + body = strings.TrimSpace(string(rb)) + } + + return fmt.Errorf("HTTP %s: %s", resp.Status, body) + } + return nil +} + +func formPushTargetAddress(influxDBVersion, baseAddr, organization, bucket string) string { + url, err := url.ParseRequestURI(baseAddr) + if err != nil { + log.Fatal("Invalid push_addr", err) + } + // Url format + // - in 2.0: /api/v2/write?org=organization&bucket=bucket + // - in 1.7: /write?db=organization + organizationParamName := "org" + bucketParamName := "bucket" + if influxDBVersion == "1.7" { + organizationParamName = "db" + // Concept of explicit bucket in 1.7 is absent. + bucketParamName = "" + } + q := url.Query() + q.Add(organizationParamName, organization) + if bucketParamName != "" { + q.Add(bucketParamName, bucket) + } + url.RawQuery = q.Encode() + return url.String() +} + +func formAuthorizationHeaderValue(influxDBVersion, token string) string { + // Authorization header has value + // - in 2.0: Token + // - in 1.7: Bearer + if influxDBVersion == "2.0" { + return fmt.Sprintf("Token %s", token) + } + return fmt.Sprintf("Bearer %s", token) +} diff --git a/monitoring/exporter/main.go b/monitoring/exporter/main.go new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/exporter/prom_exporter.go b/monitoring/exporter/prom_exporter.go new file mode 100644 index 0000000..e69de29 diff --git a/monitoring/exporter/scraper.go b/monitoring/exporter/scraper.go new file mode 100644 index 0000000..e69de29 diff --git a/pbx/README.md b/pbx/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pbx/go-generate.sh b/pbx/go-generate.sh new file mode 100644 index 0000000..e69de29 diff --git a/pbx/model.pb.go b/pbx/model.pb.go new file mode 100644 index 0000000..e69de29 diff --git a/pbx/model.proto b/pbx/model.proto new file mode 100644 index 0000000..e69de29 diff --git a/pbx/model_grpc.pb.go b/pbx/model_grpc.pb.go new file mode 100644 index 0000000..e69de29 diff --git a/pbx/py-generate.sh b/pbx/py-generate.sh new file mode 100644 index 0000000..e69de29 diff --git a/pbx/py_fix.py b/pbx/py_fix.py new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/.gitignore b/py_grpc/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/LICENSE b/py_grpc/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/README.md b/py_grpc/README.md new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/pyproject.toml b/py_grpc/pyproject.toml new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/tinode_grpc/__init__.py b/py_grpc/tinode_grpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/tinode_grpc/model_pb2.py b/py_grpc/tinode_grpc/model_pb2.py new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/tinode_grpc/model_pb2.pyi b/py_grpc/tinode_grpc/model_pb2.pyi new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/tinode_grpc/model_pb2_grpc.py b/py_grpc/tinode_grpc/model_pb2_grpc.py new file mode 100644 index 0000000..e69de29 diff --git a/py_grpc/version.py b/py_grpc/version.py new file mode 100644 index 0000000..e69de29 diff --git a/rest-auth/README.md b/rest-auth/README.md new file mode 100644 index 0000000..e69de29 diff --git a/rest-auth/auth.py b/rest-auth/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/rest-auth/dummy_data.json b/rest-auth/dummy_data.json new file mode 100644 index 0000000..e69de29 diff --git a/rest-auth/requirements.txt b/rest-auth/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/.golangci.yml b/server/.golangci.yml new file mode 100644 index 0000000..6b68a87 --- /dev/null +++ b/server/.golangci.yml @@ -0,0 +1,45 @@ +linters-settings: + govet: + check-shadowing: true + disable: + - composites + golint: + min-confidence: 0 + gocyclo: + min-complexity: 10 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + misspell: + locale: US + lll: + line-length: 140 + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + - commentFormatting # https://github.com/go-critic/go-critic/issues/755 + +linters: + enable-all: true + disable: + - errcheck + - maligned + - prealloc + - gosec + - gochecknoglobals + +# options for analysis running +run: + # list of build tags, all linters use it. Default is empty list. + build-tags: + - mysql + - rethinkdb + - mongodb diff --git a/server/api_key.go b/server/api_key.go new file mode 100644 index 0000000..423f64c --- /dev/null +++ b/server/api_key.go @@ -0,0 +1,74 @@ +/****************************************************************************** + * + * Description : + * + * Authentication + * + *****************************************************************************/ + +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "encoding/base64" + + "github.com/tinode/chat/server/logs" +) + +// Singned AppID. Composition: +// +// [1:algorithm version][4:appid][2:key sequence][1:isRoot][16:signature] = 24 bytes +// +// convertible to base64 without padding. All integers are little-endian. +// Definitions for byte lengths of key's parts. +const ( + // apikeyVersion is the version of this API scheme. + apikeyVersion = 1 + // apikeyAppID is deprecated and will be removed in the future. + apikeyAppID = 4 + // apikeySequence is the serial number of the key. + apikeySequence = 2 + // apikeyWho indicates if the key grants root privileges. + apikeyWho = 1 + // apikeySignature is key's cryptographic (HMAC) signature. + apikeySignature = 16 + // apikeyLength is the length of the key in bytes. + apikeyLength = apikeyVersion + apikeyAppID + apikeySequence + apikeyWho + apikeySignature +) + +// Client signature validation +// +// key: client's secret key +// +// Returns application id, key type. +func checkAPIKey(apikey string) (isValid, isRoot bool) { + if declen := base64.URLEncoding.DecodedLen(len(apikey)); declen != apikeyLength { + return + } + + data, err := base64.URLEncoding.DecodeString(apikey) + if err != nil { + logs.Warn.Println("failed to decode.base64 appid ", err) + return + } + if data[0] != 1 { + logs.Warn.Println("unknown appid signature algorithm ", data[0]) + return + } + + hasher := hmac.New(md5.New, globals.apiKeySalt) + hasher.Write(data[:apikeyVersion+apikeyAppID+apikeySequence+apikeyWho]) + check := hasher.Sum(nil) + if !bytes.Equal(data[apikeyVersion+apikeyAppID+apikeySequence+apikeyWho:], check) { + logs.Warn.Println("invalid apikey signature") + return + } + + isRoot = (data[apikeyVersion+apikeyAppID+apikeySequence] == 1) + + isValid = true + + return +} diff --git a/server/auth/anon/auth_anon.go b/server/auth/anon/auth_anon.go new file mode 100644 index 0000000..c65fff6 --- /dev/null +++ b/server/auth/anon/auth_anon.go @@ -0,0 +1,99 @@ +// Package anon provides authentication without credentials. Most useful for customer support. +// Anonymous authentication is used only at the account creation time. +package anon + +import ( + "encoding/json" + "errors" + "time" + + "github.com/tinode/chat/server/auth" + "github.com/tinode/chat/server/store" + "github.com/tinode/chat/server/store/types" +) + +// authenticator is the singleton instance of the anonymous authorizer. +type authenticator struct { + name string +} + +// Init is a noop, always returns success. +func (a *authenticator) Init(_ json.RawMessage, name string) error { + if name == "" { + return errors.New("auth_anonymous: authenticator name cannot be blank") + } + + if a.name != "" { + return errors.New("auth_anonymous: already initialized as " + a.name + "; " + name) + } + + a.name = name + return nil +} + +// IsInitialized returns true if the handler is initialized. +func (a *authenticator) IsInitialized() bool { + return a.name != "" +} + +// AddRecord checks authLevel and assigns default LevelAnon. Otherwise it +// just reports success. +func (authenticator) AddRecord(rec *auth.Rec, secret []byte, remoteAddr string) (*auth.Rec, error) { + if rec.AuthLevel == auth.LevelNone { + rec.AuthLevel = auth.LevelAnon + } + rec.State = types.StateOK + return rec, nil +} + +// UpdateRecord is a noop. Just report success. +func (authenticator) UpdateRecord(rec *auth.Rec, secret []byte, remoteAddr string) (*auth.Rec, error) { + return rec, nil +} + +// Authenticate is not supported. This authenticator is used only at account creation time. +func (authenticator) Authenticate(secret []byte, remoteAddr string) (*auth.Rec, []byte, error) { + return nil, nil, types.ErrUnsupported +} + +// AsTag is not supported, will produce an empty string. +func (authenticator) AsTag(token string) string { + return "" +} + +// IsUnique for a noop. Anonymous login does not use secret, any secret is fine. +func (authenticator) IsUnique(secret []byte, remoteAddr string) (bool, error) { + return true, nil +} + +// GenSecret always fails. +func (authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) { + return nil, time.Time{}, types.ErrUnsupported +} + +// DelRecords is a noop which always succeeds. +func (authenticator) DelRecords(uid types.Uid) error { + return nil +} + +// RestrictedTags returns tag namespaces restricted by this authenticator (none for anonymous). +func (authenticator) RestrictedTags() ([]string, error) { + return nil, nil +} + +// GetResetParams returns authenticator parameters passed to password reset handler +// (none for anonymous). +func (authenticator) GetResetParams(uid types.Uid) (map[string]any, error) { + return nil, nil +} + +const realName = "anonymous" + +// GetRealName returns the hardcoded name of the authenticator. +func (authenticator) GetRealName() string { + return realName +} + +func init() { + store.RegisterAuthScheme(realName, &authenticator{}) +} diff --git a/server/auth/auth.go b/server/auth/auth.go new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/basic/auth_basic.go b/server/auth/basic/auth_basic.go new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/code/auth_code.go b/server/auth/code/auth_code.go new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/mock_auth/mock_auth.go b/server/auth/mock_auth/mock_auth.go new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/rest/README.md b/server/auth/rest/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/rest/auth_rest.go b/server/auth/rest/auth_rest.go new file mode 100644 index 0000000..e69de29 diff --git a/server/auth/token/auth_token.go b/server/auth/token/auth_token.go new file mode 100644 index 0000000..e69de29 diff --git a/server/calls.go b/server/calls.go new file mode 100644 index 0000000..e69de29 diff --git a/server/cluster.go b/server/cluster.go new file mode 100644 index 0000000..5552978 --- /dev/null +++ b/server/cluster.go @@ -0,0 +1,1265 @@ +package main + +import ( + "encoding/gob" + "encoding/json" + "errors" + "net" + "net/rpc" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/tinode/chat/server/auth" + "github.com/tinode/chat/server/concurrency" + "github.com/tinode/chat/server/logs" + "github.com/tinode/chat/server/push" + rh "github.com/tinode/chat/server/ringhash" + "github.com/tinode/chat/server/store/types" +) + +const ( + // Network connection timeout. + clusterNetworkTimeout = 3 * time.Second + // Default timeout before attempting to reconnect to a node. + clusterDefaultReconnectTime = 200 * time.Millisecond + // Number of replicas in ringhash. + clusterHashReplicas = 20 + // Buffer size for sending requests from proxy to master. + clusterProxyToMasterBuffer = 64 + // Expand buffer size by this value for nodes over the basic 3-node setup. + clusterProxyToMasterBufferPerNode = 16 + // Timeout for attempting to enqueue a proxy-to-master request when the buffer is full. + clusterP2MTimeout = 20 * time.Millisecond + // Buffer size for receiving responses from other nodes, per node. + clusterRpcCompletionBuffer = 64 +) + +// ProxyReqType is the type of proxy requests. +type ProxyReqType int + +// Individual request types. +const ( + ProxyReqNone ProxyReqType = iota + ProxyReqJoin // {sub}. + ProxyReqLeave // {leave} + ProxyReqMeta // {meta set|get} + ProxyReqBroadcast // {pub}, {note} + ProxyReqBgSession + ProxyReqMeUserAgent + ProxyReqCall // Used in video call proxy sessions for routing call events. +) + +type clusterNodeConfig struct { + Name string `json:"name"` + Addr string `json:"addr"` +} + +type clusterConfig struct { + // List of all members of the cluster, including this member + Nodes []clusterNodeConfig `json:"nodes"` + // Name of this cluster node + ThisName string `json:"self"` + // Deprecated: this field is no longer used. + NumProxyEventGoRoutines int `json:"-"` + // Failover configuration + Failover *clusterFailoverConfig +} + +// ClusterNode is a client's connection to another node. +type ClusterNode struct { + lock sync.Mutex + + // RPC endpoint + endpoint *rpc.Client + // True if the endpoint is believed to be connected + connected bool + // True if a go routine is trying to reconnect the node + reconnecting bool + // TCP address in the form host:port + address string + // Name of the node + name string + // Fingerprint of the node: unique value which changes when the node restarts. + fingerprint int64 + + // A number of times this node has failed in a row + failCount int + + // Channel for shutting down the runner; buffered, 1. + done chan bool + + // IDs of multiplexing sessions belonging to this node. + msess map[string]struct{} + + // Default channel for receiving responses to RPC calls issued by this node. + // Buffered, clusterRpcCompletionBuffer * number_of_nodes. + rpcDone chan *rpc.Call + + // Channel for sending proxy to master requests; buffered, clusterProxyToMasterBuffer. + p2mSender chan *ClusterReq +} + +func (n *ClusterNode) asyncRpcLoop() { + for call := range n.rpcDone { + n.handleRpcResponse(call) + } +} + +func (n *ClusterNode) p2mSenderLoop() { + for req := range n.p2mSender { + if req == nil { + // Stop + return + } + + if err := n.proxyToMaster(req); err != nil { + logs.Warn.Println("p2mSenderLoop: call failed", n.name, err) + } + } +} + +// ClusterSess is a basic info on a remote session where the message was created. +type ClusterSess struct { + // IP address of the client. For long polling this is the IP of the last poll + RemoteAddr string + + // User agent, a string provived by an authenticated client in {login} packet + UserAgent string + + // ID of the current user or 0 + Uid types.Uid + + // User's authentication level + AuthLvl auth.Level + + // Protocol version of the client: ((major & 0xff) << 8) | (minor & 0xff) + Ver int + + // Human language of the client + Lang string + // Country of the client + CountryCode string + + // Device ID + DeviceID string + + // Device platform: "web", "ios", "android" + Platform string + + // Session ID + Sid string + + // Background session + Background bool +} + +// ClusterSessUpdate represents a request to update a session. +// User Agent change or background session comes to foreground. +type ClusterSessUpdate struct { + // User this session represents. + Uid types.Uid + // Session id. + Sid string + // Session user agent. + UserAgent string +} + +// ClusterReq is either a Proxy to Master or Topic Proxy to Topic Master or intra-cluster routing request message. +type ClusterReq struct { + // Name of the node sending this request + Node string + + // Ring hash signature of the node sending this request + // Signature must match the signature of the receiver, otherwise the + // Cluster is desynchronized. + Signature string + + // Fingerprint of the node sending this request. + // Fingerprint changes when the node is restarted. + Fingerprint int64 + + // Type of request. + ReqType ProxyReqType + + // Client message. Set for C2S requests. + CliMsg *ClientComMessage + // Message to be routed. Set for intra-cluster route requests. + SrvMsg *ServerComMessage + + // Expanded (routable) topic name + RcptTo string + // Originating session + Sess *ClusterSess + // True when the topic proxy is gone. + Gone bool +} + +// ClusterRoute is intra-cluster routing request message. +type ClusterRoute struct { + // Name of the node sending this request + Node string + + // Ring hash signature of the node sending this request + // Signature must match the signature of the receiver, otherwise the + // Cluster is desynchronized. + Signature string + + // Fingerprint of the node sending this request. + // Fingerprint changes when the node is restarted. + Fingerprint int64 + + // Message to be routed. Set for intra-cluster route requests. + SrvMsg *ServerComMessage + + // Originating session + Sess *ClusterSess +} + +// ClusterResp is a Master to Proxy response message. +type ClusterResp struct { + // Server message with the response. + SrvMsg *ServerComMessage + // Originating session ID to forward response to, if any. + OrigSid string + // Expanded (routable) topic name + RcptTo string + + // Parameters sent back by the topic master in response a topic proxy request. + + // Original request type. + OrigReqType ProxyReqType +} + +// ClusterPing is used to detect node restarts. +type ClusterPing struct { + // Name of the node sending this request. + Node string + + // Fingerprint of the node sending this request. + // Fingerprint changes when the node restarts. + Fingerprint int64 +} + +// Handle outbound node communication: read messages from the channel, forward to remote nodes. +// FIXME(gene): this will drain the outbound queue in case of a failure: all unprocessed messages will be dropped. +// Maybe it's a good thing, maybe not. +func (n *ClusterNode) reconnect() { + var reconnTicker *time.Ticker + + // Avoid parallel reconnection threads. + n.lock.Lock() + if n.reconnecting { + n.lock.Unlock() + return + } + n.reconnecting = true + n.lock.Unlock() + + count := 0 + for { + // Attempt to reconnect right away + if conn, err := net.DialTimeout("tcp", n.address, clusterNetworkTimeout); err == nil { + if reconnTicker != nil { + reconnTicker.Stop() + } + n.lock.Lock() + n.endpoint = rpc.NewClient(conn) + n.connected = true + n.reconnecting = false + n.lock.Unlock() + statsInc("LiveClusterNodes", 1) + logs.Info.Println("cluster: connected to", n.name) + // Send this node credentials to the new node. + var unused bool + n.call("Cluster.Ping", + &ClusterPing{ + Node: globals.cluster.thisNodeName, + Fingerprint: globals.cluster.fingerprint, + }, + &unused) + return + } else if count == 0 { + reconnTicker = time.NewTicker(clusterDefaultReconnectTime) + } + + count++ + + select { + case <-reconnTicker.C: + // Wait for timer to try to reconnect again. Do nothing if the timer is inactive. + case <-n.done: + // Shutting down + logs.Info.Println("cluster: shutdown started at node", n.name) + reconnTicker.Stop() + if n.endpoint != nil { + n.endpoint.Close() + } + n.lock.Lock() + n.connected = false + n.reconnecting = false + n.lock.Unlock() + logs.Info.Println("cluster: shut down completed at node", n.name) + return + } + } +} + +func (n *ClusterNode) call(proc string, req, resp any) error { + if !n.connected { + return errors.New("cluster: node '" + n.name + "' not connected") + } + + if err := n.endpoint.Call(proc, req, resp); err != nil { + logs.Warn.Println("cluster: call failed", n.name, err) + + n.lock.Lock() + if n.connected { + n.endpoint.Close() + n.connected = false + statsInc("LiveClusterNodes", -1) + go n.reconnect() + } + n.lock.Unlock() + return err + } + + return nil +} + +func (n *ClusterNode) handleRpcResponse(call *rpc.Call) { + if call.Error != nil { + logs.Warn.Printf("cluster: %s call failed: %s", call.ServiceMethod, call.Error) + n.lock.Lock() + if n.connected { + n.endpoint.Close() + n.connected = false + statsInc("LiveClusterNodes", -1) + go n.reconnect() + } + n.lock.Unlock() + } +} + +func (n *ClusterNode) callAsync(proc string, req, resp any, done chan *rpc.Call) *rpc.Call { + if done != nil && cap(done) == 0 { + logs.Err.Panic("cluster: RPC done channel is unbuffered") + } + + if !n.connected { + call := &rpc.Call{ + ServiceMethod: proc, + Args: req, + Reply: resp, + Error: errors.New("cluster: node '" + n.name + "' not connected"), + Done: done, + } + if done != nil { + done <- call + } + return call + } + + var responseChan chan *rpc.Call + if done != nil { + // Make a separate response callback if we need to notify the caller. + myDone := make(chan *rpc.Call, 1) + go func() { + call := <-myDone + n.handleRpcResponse(call) + if done != nil { + done <- call + } + }() + responseChan = myDone + } else { + responseChan = n.rpcDone + } + + call := n.endpoint.Go(proc, req, resp, responseChan) + + return call +} + +// proxyToMaster forwards request from topic proxy to topic master. +func (n *ClusterNode) proxyToMaster(msg *ClusterReq) error { + msg.Node = globals.cluster.thisNodeName + var rejected bool + err := n.call("Cluster.TopicMaster", msg, &rejected) + if err == nil && rejected { + err = errors.New("cluster: topic master node out of sync") + } + return err +} + +// proxyToMaster forwards request from topic proxy to topic master. +func (n *ClusterNode) proxyToMasterAsync(msg *ClusterReq) error { + select { + case n.p2mSender <- msg: + return nil + default: + } + // Buffer is full. Wait briefly before giving up. + timer := time.NewTimer(clusterP2MTimeout) + defer timer.Stop() + select { + case n.p2mSender <- msg: + return nil + case <-timer.C: + return errors.New("cluster: load exceeded") + } +} + +// masterToProxyAsync forwards response from topic master to topic proxy +// in a fire-and-forget manner. +func (n *ClusterNode) masterToProxyAsync(msg *ClusterResp) error { + var unused bool + if c := n.callAsync("Cluster.TopicProxy", msg, &unused, nil); c.Error != nil { + return c.Error + } + return nil +} + +// route routes server message within the cluster. +func (n *ClusterNode) route(msg *ClusterRoute) error { + var unused bool + return n.call("Cluster.Route", msg, &unused) +} + +// Cluster is the representation of the cluster. +type Cluster struct { + // Cluster nodes with RPC endpoints (excluding current node). + nodes map[string]*ClusterNode + // Name of the local node + thisNodeName string + // Fingerprint of the local node + fingerprint int64 + + // Resolved address to listed on + listenOn string + + // Socket for inbound connections + inbound *net.TCPListener + // Ring hash for mapping topic names to nodes + ring *rh.Ring + + // Failover parameters. Could be nil if failover is not enabled + fo *clusterFailover + + // Thread pool to use for running proxy session (write) event processing logic. + // The number of proxy sessions grows as O(number of topics x number of cluster nodes). + // In large Tinode deployments (10s of thousands of topics, tens of nodes), + // running a separate event processing goroutine for each proxy session + // leads to a rather large memory usage and excessive scheduling overhead. + proxyEventQueue *concurrency.GoRoutinePool +} + +func (n *ClusterNode) stopMultiplexingSession(msess *Session) { + if msess == nil { + return + } + msess.stopSession(nil) + n.lock.Lock() + delete(n.msess, msess.sid) + n.lock.Unlock() +} + +// TopicMaster is a gRPC endpoint which receives requests sent by proxy topic to master topic. +func (c *Cluster) TopicMaster(msg *ClusterReq, rejected *bool) error { + *rejected = false + + node := c.nodes[msg.Node] + if node == nil { + logs.Warn.Println("cluster TopicMaster: request from an unknown node", msg.Node) + return nil + } + + // Master maintains one multiplexing session per proxy topic per node. + // Except channel topics: + // * one multiplexing session for channel subscriptions. + // * one multiplexing session for group subscriptions. + var msid string + if msg.CliMsg != nil && types.IsChannel(msg.CliMsg.Original) { + // If it's a channel request, use channel name. + msid = msg.CliMsg.Original + } else { + msid = msg.RcptTo + } + // Append node name. + msid += "-" + msg.Node + msess := globals.sessionStore.Get(msid) + + if msg.Gone { + // Proxy topic is gone. Tear down the local auxiliary session. + // If it was the last session, master topic will shut down as well. + node.stopMultiplexingSession(msess) + + if t := globals.hub.topicGet(msg.RcptTo); t != nil && t.isChan { + // If it's a channel topic, also stop the "chnX-" local auxiliary session. + msidChn := types.GrpToChn(t.name) + "-" + msg.Node + node.stopMultiplexingSession(globals.sessionStore.Get(msidChn)) + } + + return nil + } + + if msg.Signature != c.ring.Signature() { + logs.Warn.Println("cluster TopicMaster: session signature mismatch", msg.RcptTo) + *rejected = true + return nil + } + + // Create a new multiplexing session if needed. + if msess == nil { + // If the session is not found, create it. + var count int + msess, count = globals.sessionStore.NewSession(node, msid) + node.lock.Lock() + node.msess[msid] = struct{}{} + node.lock.Unlock() + + logs.Info.Println("cluster: multiplexing session started", msid, count) + msess.proxiedTopic = msg.RcptTo + } + + // This is a local copy of a remote session. + var sess *Session + // Sess is nil for user agent changes and deferred presence notification requests. + if msg.Sess != nil { + // We only need some session info. No need to copy everything. + sess = &Session{ + proto: PROXY, + // Multiplexing session which actually handles the communication. + multi: msess, + // Local parameters specific to this session. + sid: msg.Sess.Sid, + userAgent: msg.Sess.UserAgent, + remoteAddr: msg.Sess.RemoteAddr, + lang: msg.Sess.Lang, + countryCode: msg.Sess.CountryCode, + proxyReq: msg.ReqType, + background: msg.Sess.Background, + uid: msg.Sess.Uid, + } + } + + if msg.CliMsg != nil { + msg.CliMsg.sess = sess + msg.CliMsg.init = true + } + + switch msg.ReqType { + case ProxyReqJoin: + select { + case globals.hub.join <- msg.CliMsg: + default: + // Reply with a 500 to the user. + sess.queueOut(ErrUnknownReply(msg.CliMsg, msg.CliMsg.Timestamp)) + logs.Warn.Println("cluster: join req failed - hub.join queue full, topic ", msg.CliMsg.RcptTo, + "; orig sid ", sess.sid) + } + + case ProxyReqLeave: + if t := globals.hub.topicGet(msg.RcptTo); t != nil { + t.unreg <- msg.CliMsg + } else { + logs.Warn.Println("cluster: leave request for unknown topic", msg.RcptTo) + } + + case ProxyReqMeta: + if t := globals.hub.topicGet(msg.RcptTo); t != nil { + select { + case t.meta <- msg.CliMsg: + default: + sess.queueOut(ErrUnknownReply(msg.CliMsg, msg.CliMsg.Timestamp)) + logs.Warn.Println("cluster: meta req failed - topic.meta queue full, topic ", msg.CliMsg.RcptTo, + "; orig sid ", sess.sid) + } + } else { + logs.Warn.Println("cluster: meta request for unknown topic", msg.RcptTo) + } + + case ProxyReqBroadcast: + select { + case globals.hub.routeCli <- msg.CliMsg: + default: + logs.Err.Println("cluster: route req failed - hub.route queue full") + } + + case ProxyReqBgSession, ProxyReqMeUserAgent: + // sess could be nil + if t := globals.hub.topicGet(msg.RcptTo); t != nil { + if t.supd == nil { + logs.Err.Panicln("cluster: invalid topic category in session update", t.name, msg.ReqType) + } + su := &sessionUpdate{} + if msg.ReqType == ProxyReqBgSession { + su.sess = sess + } else { + su.userAgent = sess.userAgent + } + t.supd <- su + } else { + logs.Warn.Println("cluster: session update for unknown topic", msg.RcptTo, msg.ReqType) + } + + default: + logs.Warn.Println("cluster: unknown request type", msg.ReqType, msg.RcptTo) + *rejected = true + } + + return nil +} + +// TopicProxy is a gRPC endpoint at topic proxy which receives topic master responses. +func (Cluster) TopicProxy(msg *ClusterResp, unused *bool) error { + // This cluster member received a response from the topic master to be forwarded to the topic. + // Find appropriate topic, send the message to it. + if t := globals.hub.topicGet(msg.RcptTo); t != nil { + msg.SrvMsg.uid = types.ParseUserId(msg.SrvMsg.AsUser) + select { + case t.proxy <- msg: + default: + logs.Warn.Printf("cluster: proxy channel full, topic %s", msg.RcptTo) + } + } else { + logs.Warn.Println("cluster: master response for unknown topic", msg.RcptTo) + } + + return nil +} + +// Route endpoint receives intra-cluster messages destined for the nodes hosting the topic. +// Called by Hub.route channel consumer for messages send without attaching to topic first. +func (c *Cluster) Route(msg *ClusterRoute, rejected *bool) error { + logError := func(err string) { + sid := "" + if msg.Sess != nil { + sid = msg.Sess.Sid + } + logs.Warn.Println(err, sid) + *rejected = true + } + + *rejected = false + if msg.Signature != c.ring.Signature() { + logError("cluster Route: session signature mismatch") + return nil + } + + if msg.SrvMsg == nil { + // TODO: maybe panic here. + logError("cluster Route: nil server message") + return nil + } + + select { + case globals.hub.routeSrv <- msg.SrvMsg: + default: + logError("cluster Route: server busy") + } + return nil +} + +// User cache & push notifications management. These are calls received by the Master from Proxy. +// The Proxy expects no payload to be returned by the master. + +// UserCacheUpdate endpoint receives updates to user's cached values as well as sends push notifications. +func (c *Cluster) UserCacheUpdate(msg *UserCacheReq, rejected *bool) error { + if msg.Gone { + // User is deleted. Evict all user's sessions. + globals.sessionStore.EvictUser(msg.UserId, "") + + if globals.cluster.isRemoteTopic(msg.UserId.UserId()) { + // No need to delete user's cache if user is remote. + return nil + } + } + + usersRequestFromCluster(msg) + return nil +} + +// Ping is a gRPC endpoint which receives ping requests from peer nodes.Used to detect node restarts. +func (c *Cluster) Ping(ping *ClusterPing, unused *bool) error { + node := c.nodes[ping.Node] + if node == nil { + logs.Warn.Println("cluster Ping from unknown node", ping.Node) + return nil + } + + if node.fingerprint == 0 { + // This is the first connection to remote node. + node.fingerprint = ping.Fingerprint + } else if node.fingerprint != ping.Fingerprint { + // Remote node restarted. + node.fingerprint = ping.Fingerprint + c.invalidateProxySubs(ping.Node) + c.gcProxySessionsForNode(ping.Node) + } + + return nil +} + +// Sends user cache update to user's Master node where the cache actually resides. +// The request is extected to contain users who reside at remote nodes only. +func (c *Cluster) routeUserReq(req *UserCacheReq) error { + // Index requests by cluster node. + reqByNode := make(map[string]*UserCacheReq) + + if req.PushRcpt != nil { + // Request to send push notifications. Create separate packets for each affected cluster node. + for uid, recipient := range req.PushRcpt.To { + n := c.nodeForTopic(uid.UserId()) + if n == nil { + return errors.New("attempt to update user at a non-existent node (1)") + } + r := reqByNode[n.name] + if r == nil { + r = &UserCacheReq{ + PushRcpt: &push.Receipt{ + Payload: req.PushRcpt.Payload, + To: make(map[types.Uid]push.Recipient), + }, + Node: c.thisNodeName, + } + } + r.PushRcpt.To[uid] = recipient + reqByNode[n.name] = r + } + } else if len(req.UserIdList) > 0 { + // Request to add/remove some users from cache. + for _, uid := range req.UserIdList { + n := c.nodeForTopic(uid.UserId()) + if n == nil { + return errors.New("attempt to update user at a non-existent node (2)") + } + r := reqByNode[n.name] + if r == nil { + r = &UserCacheReq{Node: c.thisNodeName, Inc: req.Inc} + } + r.UserIdList = append(r.UserIdList, uid) + reqByNode[n.name] = r + } + } else if req.Gone { + // Message that the user is deleted is sent to all nodes. + r := &UserCacheReq{Node: c.thisNodeName, UserIdList: req.UserIdList, Gone: true} + for _, n := range c.nodes { + reqByNode[n.name] = r + } + } + + if len(reqByNode) > 0 { + for nodeName, r := range reqByNode { + n := c.nodes[nodeName] + var rejected bool + err := n.call("Cluster.UserCacheUpdate", r, &rejected) + if rejected { + err = errors.New("master node out of sync") + } + if err != nil { + return err + } + } + return nil + } + + // Update to cached values. + n := c.nodeForTopic(req.UserId.UserId()) + if n == nil { + return errors.New("attempt to update user at a non-existent node (3)") + } + req.Node = c.thisNodeName + var rejected bool + err := n.call("Cluster.UserCacheUpdate", req, &rejected) + if rejected { + err = errors.New("master node out of sync") + } + return err +} + +// Given topic name, find appropriate cluster node to route message to. +func (c *Cluster) nodeForTopic(topic string) *ClusterNode { + key := c.ring.Get(topic) + if key == c.thisNodeName { + logs.Err.Println("cluster: request to route to self") + // Do not route to self + return nil + } + + node := c.nodes[key] + if node == nil { + logs.Warn.Println("cluster: no node for topic", topic, key) + } + return node +} + +// isRemoteTopic checks if the given topic is handled by this node or a remote node. +func (c *Cluster) isRemoteTopic(topic string) bool { + if c == nil { + // Cluster not initialized, all topics are local + return false + } + return c.ring.Get(topic) != c.thisNodeName +} + +// genLocalTopicName is just like genTopicName(), but the generated name belongs to the current cluster node. +func (c *Cluster) genLocalTopicName() string { + topic := genTopicName() + if c == nil { + // Cluster not initialized, all topics are local + return topic + } + + // TODO: if cluster is large it may become too inefficient. + for c.ring.Get(topic) != c.thisNodeName { + topic = genTopicName() + } + return topic +} + +// isPartitioned checks if the cluster is partitioned due to network or other failure and if the +// current node is a part of the smaller partition. +func (c *Cluster) isPartitioned() bool { + if c == nil || c.fo == nil { + // Cluster not initialized or failover disabled therefore not partitioned. + return false + } + + c.fo.activeNodesLock.RLock() + result := (len(c.nodes)+1)/2 >= len(c.fo.activeNodes) + c.fo.activeNodesLock.RUnlock() + + return result +} + +func (c *Cluster) makeClusterReq(reqType ProxyReqType, msg *ClientComMessage, topic string, sess *Session) *ClusterReq { + req := &ClusterReq{ + Node: c.thisNodeName, + Signature: c.ring.Signature(), + Fingerprint: c.fingerprint, + ReqType: reqType, + RcptTo: topic, + } + + var uid types.Uid + + if msg != nil { + req.CliMsg = msg + uid = types.ParseUserId(req.CliMsg.AsUser) + } + + if sess != nil { + if uid.IsZero() { + uid = sess.uid + } + + req.Sess = &ClusterSess{ + Uid: uid, + AuthLvl: sess.authLvl, + RemoteAddr: sess.remoteAddr, + UserAgent: sess.userAgent, + Ver: sess.ver, + Lang: sess.lang, + CountryCode: sess.countryCode, + DeviceID: sess.deviceID, + Platform: sess.platf, + Sid: sess.sid, + Background: sess.background, + } + } + return req +} + +// Forward client request message from the Topic Proxy to the Topic Master (cluster node which owns the topic). +func (c *Cluster) routeToTopicMaster(reqType ProxyReqType, msg *ClientComMessage, topic string, sess *Session) error { + if c == nil { + // Cluster may be nil due to shutdown. + return nil + } + + if sess != nil && reqType != ProxyReqLeave { + if atomic.LoadInt32(&sess.terminating) > 0 { + // The session is terminating. + // Do not forward any requests except "leave" to the topic master. + return nil + } + } + + req := c.makeClusterReq(reqType, msg, topic, sess) + + // Find the cluster node which owns the topic, then forward to it. + n := c.nodeForTopic(topic) + if n == nil { + return errors.New("node for topic not found") + } + return n.proxyToMasterAsync(req) +} + +// Forward server response message to the node that owns topic. +func (c *Cluster) routeToTopicIntraCluster(topic string, msg *ServerComMessage, sess *Session) error { + if c == nil { + // Cluster may be nil due to shutdown. + return nil + } + + n := c.nodeForTopic(topic) + if n == nil { + return errors.New("node for topic not found (intra)") + } + + route := &ClusterRoute{ + Node: c.thisNodeName, + Signature: c.ring.Signature(), + Fingerprint: c.fingerprint, + SrvMsg: msg, + } + + if sess != nil { + route.Sess = &ClusterSess{Sid: sess.sid} + } + return n.route(route) +} + +// Topic proxy terminated. Inform remote Master node that the proxy is gone. +func (c *Cluster) topicProxyGone(topicName string) error { + if c == nil { + // Cluster may be nil due to shutdown. + return nil + } + + // Find the cluster node which owns the topic, then forward to it. + n := c.nodeForTopic(topicName) + if n == nil { + return errors.New("node for topic not found") + } + + req := c.makeClusterReq(ProxyReqLeave, nil, topicName, nil) + req.Gone = true + return n.proxyToMasterAsync(req) +} + +// Returns snowflake worker id. +func clusterInit(configString json.RawMessage, self *string) int { + if globals.cluster != nil { + logs.Err.Fatal("Cluster already initialized.") + } + + // Registering variables even if it's a standalone server. Otherwise monitoring software will + // complain about missing vars. + + // 1 if this node is cluster leader, 0 otherwise + statsRegisterInt("ClusterLeader") + // Total number of nodes configured + statsRegisterInt("TotalClusterNodes") + // Number of nodes currently believed to be up. + statsRegisterInt("LiveClusterNodes") + + // This is a standalone server, not initializing + if len(configString) == 0 { + logs.Info.Println("Cluster: running as a standalone server.") + return 1 + } + + var config clusterConfig + if err := json.Unmarshal(configString, &config); err != nil { + logs.Err.Fatal(err) + } + + thisName := *self + if thisName == "" { + thisName = config.ThisName + } + + // Name of the current node is not specified: clustering disabled. + if thisName == "" { + logs.Info.Println("Cluster: running as a standalone server.") + return 1 + } + + gob.Register([]any{}) + gob.Register(map[string]any{}) + gob.Register(map[string]int{}) + gob.Register(map[string]string{}) + gob.Register(MsgAccessMode{}) + + if config.NumProxyEventGoRoutines != 0 { + logs.Warn.Println("Cluster config: field num_proxy_event_goroutines is deprecated.") + } + + globals.cluster = &Cluster{ + thisNodeName: thisName, + fingerprint: time.Now().Unix(), + nodes: make(map[string]*ClusterNode), + proxyEventQueue: concurrency.NewGoRoutinePool(len(config.Nodes) * 5), + } + + var nodeNames []string + for _, host := range config.Nodes { + nodeNames = append(nodeNames, host.Name) + + if host.Name == thisName { + globals.cluster.listenOn = host.Addr + // Don't create a cluster member for this local instance + continue + } + + globals.cluster.nodes[host.Name] = &ClusterNode{ + address: host.Addr, + name: host.Name, + done: make(chan bool, 1), + msess: make(map[string]struct{}), + } + } + + if len(globals.cluster.nodes) == 0 { + // Cluster needs at least two nodes. + logs.Err.Fatal("Cluster: invalid cluster size: 1") + } + + if len(globals.cluster.nodes)%2 == 1 { + // Even number of cluster nodes (self + odd number). + logs.Warn.Println("Cluster: use odd number of cluster nodes") + } + + if !globals.cluster.failoverInit(config.Failover) { + globals.cluster.rehash(nil) + } + + sort.Strings(nodeNames) + workerId := sort.SearchStrings(nodeNames, thisName) + 1 + + statsSet("TotalClusterNodes", int64(len(globals.cluster.nodes)+1)) + + return workerId +} + +// Proxied session is being closed at the Master node. +func (sess *Session) closeRPC() { + if sess.isMultiplex() { + logs.Info.Println("cluster: session proxy closed", sess.sid) + } +} + +// Start accepting connections. +func (c *Cluster) start() { + addr, err := net.ResolveTCPAddr("tcp", c.listenOn) + if err != nil { + logs.Err.Fatal(err) + } + + c.inbound, err = net.ListenTCP("tcp", addr) + + if err != nil { + logs.Err.Fatal(err) + } + + var bufferSize = clusterProxyToMasterBuffer + if len(c.nodes) > 2 { + // Expand the buffer for larger (>3 node) clusters. + bufferSize += clusterProxyToMasterBufferPerNode * (len(c.nodes) - 2) + } + for _, n := range c.nodes { + go n.reconnect() + n.rpcDone = make(chan *rpc.Call, len(c.nodes)*clusterRpcCompletionBuffer) + n.p2mSender = make(chan *ClusterReq, bufferSize) + go n.asyncRpcLoop() + go n.p2mSenderLoop() + } + + if c.fo != nil { + go c.run() + } + + err = rpc.Register(c) + if err != nil { + logs.Err.Fatal(err) + } + + go rpc.Accept(c.inbound) + + logs.Info.Printf("Cluster of %d nodes initialized, node '%s' is listening on [%s]", len(globals.cluster.nodes)+1, + globals.cluster.thisNodeName, c.listenOn) +} + +func (c *Cluster) shutdown() { + if globals.cluster == nil { + return + } + for _, n := range c.nodes { + close(n.rpcDone) + close(n.p2mSender) + } + + globals.cluster.proxyEventQueue.Stop() + globals.cluster = nil + + c.inbound.Close() + + if c.fo != nil { + c.fo.done <- true + } + + for _, n := range c.nodes { + n.done <- true + } + + logs.Info.Println("Cluster shut down") +} + +// Recalculate the ring hash using provided list of nodes or only nodes in a non-failed state. +// Returns the list of nodes used for ring hash. +func (c *Cluster) rehash(nodes []string) []string { + ring := rh.New(clusterHashReplicas, nil) + + var ringKeys []string + + if nodes == nil { + for _, node := range c.nodes { + ringKeys = append(ringKeys, node.name) + } + ringKeys = append(ringKeys, c.thisNodeName) + } else { + ringKeys = append(ringKeys, nodes...) + } + ring.Add(ringKeys...) + + c.ring = ring + + return ringKeys +} + +// invalidateProxySubs iterates over sessions proxied on this node and for each session +// sends "{pres term}" informing that the topic subscription (attachment) was lost: +// - Called immediately after Cluster.rehash() for all relocated topics (forNode == ""). +// - Called for topics hosted at a specific node when a node restart is detected. +// TODO: consider resubscribing to topics instead of forcing sessions to resubscribe. +func (c *Cluster) invalidateProxySubs(forNode string) { + sessions := make(map[*Session][]string) + globals.hub.topics.Range(func(_, v any) bool { + topic := v.(*Topic) + if !topic.isProxy { + // Topic isn't a proxy. + return true + } + if forNode == "" { + if topic.masterNode == c.ring.Get(topic.name) { + // The topic hasn't moved. Continue. + return true + } + } else if topic.masterNode != forNode { + // The topic is hosted at a different node than the restarted node. + return true + } + + for s, psd := range topic.sessions { + // FIXME: 'me' topic must be the last one in the list for each topic. + sessions[s] = append(sessions[s], topicNameForUser(topic.name, psd.uid, psd.isChanSub)) + } + return true + }) + + for s, topicsToTerminate := range sessions { + s.presTermDirect(topicsToTerminate) + } +} + +// gcProxySessions terminates orphaned proxy sessions at a master node for all lost nodes (allNodes minus activeNodes). +// The session is orphaned when the origin node is gone. +func (c *Cluster) gcProxySessions(activeNodes []string) { + allNodes := []string{c.thisNodeName} + for name := range c.nodes { + allNodes = append(allNodes, name) + } + _, failedNodes, _ := stringSliceDelta(allNodes, activeNodes) + for _, node := range failedNodes { + // Iterate sessions of a failed node + c.gcProxySessionsForNode(node) + } +} + +// gcProxySessionsForNode terminates orphaned proxy sessions at a master node for the given node. +// For example, a remote node is restarted or the cluster is rehashed without the node. +func (c *Cluster) gcProxySessionsForNode(node string) { + n := c.nodes[node] + n.lock.Lock() + msess := n.msess + n.msess = make(map[string]struct{}) + n.lock.Unlock() + for sid := range msess { + if sess := globals.sessionStore.Get(sid); sess != nil { + sess.stop <- nil + } + } +} + +// clusterWriteLoop implements write loop for multiplexing (proxy) session at a node which hosts master topic. +// The session is a multiplexing session, i.e. it handles requests for multiple sessions at origin. +func (sess *Session) clusterWriteLoop(forTopic string) { + terminate := true + defer func() { + if terminate { + sess.closeRPC() + globals.sessionStore.Delete(sess) + sess.inflightReqs = nil + sess.unsubAll() + } + }() + + for { + select { + case msg, ok := <-sess.send: + if !ok || sess.clnode.endpoint == nil { + // channel closed + return + } + srvMsg := msg.(*ServerComMessage) + response := &ClusterResp{SrvMsg: srvMsg} + if srvMsg.sess == nil { + response.OrigSid = "*" + } else { + response.OrigReqType = srvMsg.sess.proxyReq + response.OrigSid = srvMsg.sess.sid + srvMsg.AsUser = srvMsg.sess.uid.UserId() + + switch srvMsg.sess.proxyReq { + case ProxyReqJoin, ProxyReqLeave, ProxyReqMeta, ProxyReqBgSession, ProxyReqMeUserAgent, ProxyReqCall: + // Do nothing + case ProxyReqBroadcast, ProxyReqNone: + if srvMsg.Data != nil || srvMsg.Pres != nil || srvMsg.Info != nil { + response.OrigSid = "*" + } else if srvMsg.Ctrl == nil { + logs.Warn.Println("cluster: request type not set in clusterWriteLoop", sess.sid, + srvMsg.describe(), "src_sid:", srvMsg.sess.sid) + } + default: + logs.Err.Panicln("cluster: unknown request type in clusterWriteLoop", srvMsg.sess.proxyReq) + } + } + + srvMsg.RcptTo = forTopic + response.RcptTo = forTopic + + if err := sess.clnode.masterToProxyAsync(response); err != nil { + logs.Warn.Printf("cluster: response to proxy failed \"%s\": %s", sess.sid, err.Error()) + return + } + case msg := <-sess.stop: + if msg == nil { + // Terminating multiplexing session. + return + } + // There are two cases of msg != nil: + // * user is being deleted + // * node shutdown + // In both cases the msg does not need to be forwarded to the proxy. + + case <-sess.detach: + return + default: + terminate = false + return + } + } +} diff --git a/server/cluster_leader.go b/server/cluster_leader.go new file mode 100644 index 0000000..e69de29 diff --git a/server/concurrency/goroutinepool.go b/server/concurrency/goroutinepool.go new file mode 100644 index 0000000..fefb541 --- /dev/null +++ b/server/concurrency/goroutinepool.go @@ -0,0 +1,56 @@ +// Package concurrency is a very simple implementation of a mutex with channels. +// Provides TryLock functionality absent in Go's regular sync.Mutex. +// See https://github.com/golang/go/issues/6123 for details. +package concurrency + +// Task represents a work task to be run on the specified thread pool. +type Task func() + +// GoRoutinePool is a pull of Go routines with associated locking mechanism. +type GoRoutinePool struct { + // Work queue. + work chan Task + // Counter to control the number of already allocated/running goroutines. + sem chan struct{} + // Exit knob. + stop chan struct{} +} + +// NewGoRoutinePool allocates a new thread pool with `numWorkers` goroutines. +func NewGoRoutinePool(numWorkers int) *GoRoutinePool { + return &GoRoutinePool{ + work: make(chan Task), + sem: make(chan struct{}, numWorkers), + stop: make(chan struct{}, numWorkers), + } +} + +// Schedule enqueus a closure to run on the GoRoutinePool's goroutines. +func (p *GoRoutinePool) Schedule(task Task) { + select { + case p.work <- task: + case p.sem <- struct{}{}: + go p.worker(task) + } +} + +// Stop sends a stop signal to all running goroutines. +func (p *GoRoutinePool) Stop() { + numWorkers := cap(p.sem) + for range numWorkers { + p.stop <- struct{}{} + } +} + +// Thread pool worker goroutine. +func (p *GoRoutinePool) worker(task Task) { + defer func() { <-p.sem }() + for { + task() + select { + case task = <-p.work: + case <-p.stop: + return + } + } +} diff --git a/server/concurrency/simplemutex.go b/server/concurrency/simplemutex.go new file mode 100644 index 0000000..9a59bf7 --- /dev/null +++ b/server/concurrency/simplemutex.go @@ -0,0 +1,30 @@ +package concurrency + +// SimpleMutex is a channel used for locking. +type SimpleMutex chan struct{} + +// NewSimpleMutex creates and returns a new SimpleMutex object. +func NewSimpleMutex() SimpleMutex { + return make(SimpleMutex, 1) +} + +// Lock acquires a lock on the mutex. +func (s SimpleMutex) Lock() { + s <- struct{}{} +} + +// TryLock attempts to acquire a lock on the mutex. +// Returns true if the lock has been acquired, false otherwise. +func (s SimpleMutex) TryLock() bool { + select { + case s <- struct{}{}: + return true + default: + return false + } +} + +// Unlock releases the mutex. +func (s SimpleMutex) Unlock() { + <-s +} diff --git a/server/datamodel.go b/server/datamodel.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/adapter.go b/server/db/adapter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/common/common.go b/server/db/common/common.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/common/common_test.go b/server/db/common/common_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/common/test_data/test_data.go b/server/db/common/test_data/test_data.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mongodb/adapter.go b/server/db/mongodb/adapter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mongodb/blank.go b/server/db/mongodb/blank.go new file mode 100644 index 0000000..f3ed9da --- /dev/null +++ b/server/db/mongodb/blank.go @@ -0,0 +1,7 @@ +//go:build !mongodb +// +build !mongodb + +// This file is needed for conditional compilation. It's used when +// the build tag 'mongodb' is not defined. Otherwise the adapter.go +// is compiled. +package mongodb diff --git a/server/db/mongodb/schema.md b/server/db/mongodb/schema.md new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mongodb/tests/mongo_test.go b/server/db/mongodb/tests/mongo_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mongodb/tests/test.conf b/server/db/mongodb/tests/test.conf new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mysql/blank.go b/server/db/mysql/blank.go new file mode 100644 index 0000000..34a79f3 --- /dev/null +++ b/server/db/mysql/blank.go @@ -0,0 +1,8 @@ +//go:build !mysql +// +build !mysql + +// This file is needed for conditional compilation. It's used when +// the build tag 'mysql' is not defined. Otherwise the adapter.go +// is compiled. + +package mysql diff --git a/server/db/mysql/schema.sql b/server/db/mysql/schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mysql/tests/mysql_test.go b/server/db/mysql/tests/mysql_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/mysql/tests/test.conf b/server/db/mysql/tests/test.conf new file mode 100644 index 0000000..e69de29 diff --git a/server/db/postgres/adapter.go b/server/db/postgres/adapter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/postgres/blank.go b/server/db/postgres/blank.go new file mode 100644 index 0000000..a805fb2 --- /dev/null +++ b/server/db/postgres/blank.go @@ -0,0 +1,8 @@ +//go:build !postgres +// +build !postgres + +// This file is needed for conditional compilation. It's used when +// the build tag 'postgres' is not defined. Otherwise the adapter.go +// is compiled. + +package postgres diff --git a/server/db/postgres/schema.sql b/server/db/postgres/schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/server/db/postgres/tests/postgres_test.go b/server/db/postgres/tests/postgres_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/postgres/tests/test.conf b/server/db/postgres/tests/test.conf new file mode 100644 index 0000000..e69de29 diff --git a/server/db/rethinkdb/adapter.go b/server/db/rethinkdb/adapter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/rethinkdb/blank.go b/server/db/rethinkdb/blank.go new file mode 100644 index 0000000..bb61343 --- /dev/null +++ b/server/db/rethinkdb/blank.go @@ -0,0 +1,8 @@ +//go:build !rethinkdb +// +build !rethinkdb + +// This file is needed for conditional compilation. It's used when +// the build tag 'rethinkdb' is not defined. Otherwise the adapter.go +// is compiled. + +package rethinkdb diff --git a/server/db/rethinkdb/schema.md b/server/db/rethinkdb/schema.md new file mode 100644 index 0000000..e69de29 diff --git a/server/db/rethinkdb/tests/rethink_test.go b/server/db/rethinkdb/tests/rethink_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/db/rethinkdb/tests/test.conf b/server/db/rethinkdb/tests/test.conf new file mode 100644 index 0000000..e69de29 diff --git a/server/drafty/drafty.go b/server/drafty/drafty.go new file mode 100644 index 0000000..e69de29 diff --git a/server/drafty/drafty_test.go b/server/drafty/drafty_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/drafty/grapheme.go b/server/drafty/grapheme.go new file mode 100644 index 0000000..66f955b --- /dev/null +++ b/server/drafty/grapheme.go @@ -0,0 +1,77 @@ +package drafty + +import ( + "github.com/rivo/uniseg" +) + +// graphemes is a container holding lengths of grapheme clusters in a string. +type graphemes struct { + // The original string. + original string + + // Sizes of grapheme clusters within the original string. + sizes []byte +} + +// prepareGraphemes returns a parsed grapheme cluster container by splitting the string into grapheme clusters +// and saving their lengths. +func prepareGraphemes(str string) *graphemes { + // Split the string into grapheme clusters and save the size of each cluster. + sizes := make([]byte, 0, len(str)) + for state, remaining, cluster := -1, str, ""; len(remaining) > 0; { + cluster, remaining, _, state = uniseg.StepString(remaining, state) + sizes = append(sizes, byte(len(cluster))) + } + + return &graphemes{ + original: str, + sizes: sizes, + } +} + +// length returns the number of grapheme clusters in the original string. +func (g *graphemes) length() int { + if g == nil { + return 0 + } + return len(g.sizes) +} + +// string returns the original string from which the grapheme cluster container was created. +func (g *graphemes) string() string { + if g == nil { + return "" + } + return g.original +} + +// slice returns a new grapheme cluster container with grapheme clusters from 'start' to 'end'. +func (g *graphemes) slice(start, end int) *graphemes { + + // Convert grapheme offsets to string offsets. + s := 0 + for i := range start { + s += int(g.sizes[i]) + } + e := s + for i := start; i < end; i++ { + e += int(g.sizes[i]) + } + + return &graphemes{ + original: g.original[s:e], + sizes: g.sizes[start:end], + } +} + +// append appends 'other' grapheme cluster container to 'g' container and returns g. +// If g is nil, the 'other' is returned. +func (g *graphemes) append(other *graphemes) *graphemes { + if g == nil { + return other + } + + g.original += other.original + g.sizes = append(g.sizes, other.sizes...) + return g +} diff --git a/server/hdl_files.go b/server/hdl_files.go new file mode 100644 index 0000000..e69de29 diff --git a/server/hdl_grpc.go b/server/hdl_grpc.go new file mode 100644 index 0000000..e69de29 diff --git a/server/hdl_longpoll.go b/server/hdl_longpoll.go new file mode 100644 index 0000000..e69de29 diff --git a/server/hdl_websock.go b/server/hdl_websock.go new file mode 100644 index 0000000..e69de29 diff --git a/server/http.go b/server/http.go new file mode 100644 index 0000000..e69de29 diff --git a/server/http_pprof.go b/server/http_pprof.go new file mode 100644 index 0000000..d12a4c8 --- /dev/null +++ b/server/http_pprof.go @@ -0,0 +1,53 @@ +// Debug tooling. Dumps named profile in response to HTTP request at +// http(s)://// +// See godoc for the list of possible profile names: https://golang.org/pkg/runtime/pprof/#Profile + +package main + +import ( + "fmt" + "net/http" + "path" + "runtime/pprof" + "strings" + + "github.com/tinode/chat/server/logs" +) + +var pprofHttpRoot string + +// Expose debug profiling at the given URL path. +func servePprof(mux *http.ServeMux, serveAt string) { + if serveAt == "" || serveAt == "-" { + return + } + + pprofHttpRoot = path.Clean("/"+serveAt) + "/" + mux.HandleFunc(pprofHttpRoot, profileHandler) + + logs.Info.Printf("pprof: profiling info exposed at '%s'", pprofHttpRoot) +} + +func profileHandler(wrt http.ResponseWriter, req *http.Request) { + wrt.Header().Set("X-Content-Type-Options", "nosniff") + wrt.Header().Set("Content-Type", "text/plain; charset=utf-8") + + profileName := strings.TrimPrefix(req.URL.Path, pprofHttpRoot) + + profile := pprof.Lookup(profileName) + if profile == nil { + servePprofError(wrt, http.StatusNotFound, "Unknown profile '"+profileName+"'") + return + } + + // Respond with the requested profile. + profile.WriteTo(wrt, 2) +} + +func servePprofError(wrt http.ResponseWriter, status int, txt string) { + wrt.Header().Set("Content-Type", "text/plain; charset=utf-8") + wrt.Header().Set("X-Go-Pprof", "1") + wrt.Header().Del("Content-Disposition") + wrt.WriteHeader(status) + fmt.Fprintln(wrt, txt) +} diff --git a/server/hub.go b/server/hub.go new file mode 100644 index 0000000..e69de29 diff --git a/server/init_topic.go b/server/init_topic.go new file mode 100644 index 0000000..e69de29 diff --git a/server/logs/logs.go b/server/logs/logs.go new file mode 100644 index 0000000..564b646 --- /dev/null +++ b/server/logs/logs.go @@ -0,0 +1,55 @@ +// Package logs exposes info, warning and error loggers. +package logs + +import ( + "io" + "log" + "strings" +) + +var ( + // Info is a logger at the 'info' logging level. + Info *log.Logger + // Warn is a logger at the 'warning' logging level. + Warn *log.Logger + // Err is a logger at the 'error' logging level. + Err *log.Logger +) + +func parseFlags(logFlags string) int { + flags := 0 + for _, v := range strings.Split(logFlags, ",") { + switch { + case v == "date": + flags |= log.Ldate + case v == "time": + flags |= log.Ltime + case v == "microseconds": + flags |= log.Lmicroseconds + case v == "longfile": + flags |= log.Llongfile + case v == "shortfile": + flags |= log.Lshortfile + case v == "UTC": + flags |= log.LUTC + case v == "msgprefix": + flags |= log.Lmsgprefix + case v == "stdFlags": + flags |= log.LstdFlags + default: + log.Fatalln("Invalid log flags string: ", logFlags) + } + } + if flags == 0 { + flags = log.LstdFlags + } + return flags +} + +// Init initializes info, warning and error loggers given the flags and the output. +func Init(output io.Writer, logFlags string) { + flags := parseFlags(logFlags) + Info = log.New(output, "I", flags) + Warn = log.New(output, "W", flags) + Err = log.New(output, "E", flags) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..e69de29 diff --git a/server/media/fs/filesys.go b/server/media/fs/filesys.go new file mode 100644 index 0000000..e69de29 diff --git a/server/media/media.go b/server/media/media.go new file mode 100644 index 0000000..e69de29 diff --git a/server/media/media_test.go b/server/media/media_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/media/s3/s3.go b/server/media/s3/s3.go new file mode 100644 index 0000000..e69de29 diff --git a/server/pbconverter.go b/server/pbconverter.go new file mode 100644 index 0000000..e69de29 diff --git a/server/plugins.go b/server/plugins.go new file mode 100644 index 0000000..e69de29 diff --git a/server/pres.go b/server/pres.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push.go b/server/push.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push/common/typedef.go b/server/push/common/typedef.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push/fcm/README.md b/server/push/fcm/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/push/fcm/payload.go b/server/push/fcm/payload.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push/fcm/push_fcm.go b/server/push/fcm/push_fcm.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push/push.go b/server/push/push.go new file mode 100644 index 0000000..e69de29 diff --git a/server/push/stdout/README.md b/server/push/stdout/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/push/stdout/push_stdout.go b/server/push/stdout/push_stdout.go new file mode 100644 index 0000000..cc11ade --- /dev/null +++ b/server/push/stdout/push_stdout.go @@ -0,0 +1,98 @@ +// Package stdout is a sample implementation of a push plugin. +// If enabled, it writes every notification to stdout. +package stdout + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/tinode/chat/server/push" +) + +var handler stdoutPush + +// How much to buffer the input channel. +const defaultBuffer = 32 + +type stdoutPush struct { + initialized bool + input chan *push.Receipt + channel chan *push.ChannelReq + stop chan bool +} + +type configType struct { + Enabled bool `json:"enabled"` + Buffer int `json:"buffer"` +} + +// Init initializes the handler +func (stdoutPush) Init(jsonconf json.RawMessage) (bool, error) { + + // Check if the handler is already initialized + if handler.initialized { + return false, errors.New("already initialized") + } + + var config configType + if err := json.Unmarshal([]byte(jsonconf), &config); err != nil { + return false, errors.New("failed to parse config: " + err.Error()) + } + + handler.initialized = true + + if !config.Enabled { + return false, nil + } + + if config.Buffer <= 0 { + config.Buffer = defaultBuffer + } + + handler.input = make(chan *push.Receipt, config.Buffer) + handler.channel = make(chan *push.ChannelReq, config.Buffer) + handler.stop = make(chan bool, 1) + + go func() { + for { + select { + case msg := <-handler.input: + fmt.Fprintln(os.Stdout, msg) + case msg := <-handler.channel: + fmt.Fprintln(os.Stdout, msg) + case <-handler.stop: + return + } + } + }() + + return true, nil +} + +// IsReady checks if the handler is initialized. +func (stdoutPush) IsReady() bool { + return handler.input != nil +} + +// Push returns a channel that the server will use to send messages to. +// If the adapter blocks, the message will be dropped. +func (stdoutPush) Push() chan<- *push.Receipt { + return handler.input +} + +// Channel returns a channel that caller can use to subscribe/unsubscribe devices to channels (FCM topics). +// If the adapter blocks, the message will be dropped. +func (stdoutPush) Channel() chan<- *push.ChannelReq { + return handler.channel +} + +// Stop terminates the handler's worker and stops sending pushes. +func (stdoutPush) Stop() { + handler.stop <- true +} + +func init() { + push.Register("stdout", &handler) +} diff --git a/server/push/tnpg/README.md b/server/push/tnpg/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/push/tnpg/push_tnpg.go b/server/push/tnpg/push_tnpg.go new file mode 100644 index 0000000..e69de29 diff --git a/server/ringhash/ringhash.go b/server/ringhash/ringhash.go new file mode 100644 index 0000000..1c2a5dc --- /dev/null +++ b/server/ringhash/ringhash.go @@ -0,0 +1,134 @@ +// Package ringhash implementats a consistent ring hash: +// https://en.wikipedia.org/wiki/Consistent_hashing +package ringhash + +import ( + "encoding/ascii85" + "hash/crc32" + "hash/fnv" + "sort" + "strconv" + + "github.com/tinode/chat/server/logs" +) + +// Hash is a signature of a hash function used by the package. +type Hash func(data []byte) uint32 + +type elem struct { + key string + hash uint32 +} + +type sortable []elem + +func (k sortable) Len() int { return len(k) } +func (k sortable) Swap(i, j int) { k[i], k[j] = k[j], k[i] } +func (k sortable) Less(i, j int) bool { + // Weak hash function may cause collisions. + if k[i].hash < k[j].hash { + return true + } + if k[i].hash == k[j].hash { + return k[i].key < k[j].key + } + return false +} + +// Ring is the definition of the ringhash. +type Ring struct { + keys []elem // Sorted list of keys. + + signature string + replicas int + hashfunc Hash +} + +// New initializes an empty ringhash with the given number of replicas and a hash function. +// If the hash function is nil, crc32.NewIEEE() is used. +func New(replicas int, fn Hash) *Ring { + ring := &Ring{ + replicas: replicas, + hashfunc: fn, + } + if ring.hashfunc == nil { + ring.hashfunc = func(data []byte) uint32 { + hash := crc32.NewIEEE() + hash.Write(data) + return hash.Sum32() + } + } + return ring +} + +// Len returns the number of keys in the ring. +func (ring *Ring) Len() int { + return len(ring.keys) +} + +// Add adds keys to the ring. +func (ring *Ring) Add(keys ...string) { + for _, key := range keys { + for i := range ring.replicas { + ring.keys = append(ring.keys, elem{ + hash: ring.hashfunc([]byte(strconv.Itoa(i) + key)), + key: key}) + } + } + sort.Sort(sortable(ring.keys)) + + // Calculate signature + hash := fnv.New128a() + b := make([]byte, 4) + for _, key := range ring.keys { + b[0] = byte(key.hash) + b[1] = byte(key.hash >> 8) + b[2] = byte(key.hash >> 16) + b[3] = byte(key.hash >> 24) + hash.Write(b) + hash.Write([]byte(key.key)) + } + + b = []byte{} + b = hash.Sum(b) + dst := make([]byte, ascii85.MaxEncodedLen(len(b))) + ascii85.Encode(dst, b) + ring.signature = string(dst) +} + +// Get returns the closest item in the ring to the provided key. +func (ring *Ring) Get(key string) string { + + if ring.Len() == 0 { + return "" + } + + hash := ring.hashfunc([]byte(key)) + + // Binary search for appropriate replica. + idx := sort.Search(len(ring.keys), func(i int) bool { + el := ring.keys[i] + return (el.hash > hash) || (el.hash == hash && el.key >= key) + }) + + // Means we have cycled back to the first replica. + if idx == len(ring.keys) { + idx = 0 + } + + return ring.keys[idx].key +} + +// Signature returns the ring's hash signature. Two identical ringhashes +// will have the same signature. Two hashes with different +// number of keys or replicas or hash functions will have different +// signatures. +func (ring *Ring) Signature() string { + return ring.signature +} + +func (ring *Ring) dump() { + for _, e := range ring.keys { + logs.Info.Printf("key: '%s', hash=%d", e.key, e.hash) + } +} diff --git a/server/ringhash/ringhash_test.go b/server/ringhash/ringhash_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/run-cluster.sh b/server/run-cluster.sh new file mode 100644 index 0000000..e69de29 diff --git a/server/sanity-test.sh b/server/sanity-test.sh new file mode 100644 index 0000000..e69de29 diff --git a/server/session.go b/server/session.go new file mode 100644 index 0000000..e69de29 diff --git a/server/session_test.go b/server/session_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/sessionstore.go b/server/sessionstore.go new file mode 100644 index 0000000..e69de29 diff --git a/server/stats.go b/server/stats.go new file mode 100644 index 0000000..aa59c7f --- /dev/null +++ b/server/stats.go @@ -0,0 +1,167 @@ +// Logic related to expvar handling: reporting live stats such as +// session and topic counts, memory usage etc. +// The stats updates happen in a separate go routine to avoid +// locking on main logic routines. + +package main + +import ( + "encoding/json" + "expvar" + "net/http" + "runtime" + "sort" + "time" + + "github.com/tinode/chat/server/logs" + "github.com/tinode/chat/server/store" +) + +// A simple implementation of histogram expvar.Var. +// `Bounds` specifies the histogram buckets as follows (length = len(bounds)): +// +// (-inf, Bounds[i]) for i = 0 +// [Bounds[i-1], Bounds[i]) for 0 < i < length +// [Bounds[i-1], +inf) for i = length +type histogram struct { + Count int64 `json:"count"` + Sum float64 `json:"sum"` + CountPerBucket []int64 `json:"count_per_bucket"` + Bounds []float64 `json:"bounds"` +} + +func (h *histogram) addSample(v float64) { + h.Count++ + h.Sum += v + idx := sort.SearchFloat64s(h.Bounds, v) + h.CountPerBucket[idx]++ +} + +func (h *histogram) String() string { + if r, err := json.Marshal(h); err == nil { + return string(r) + } + return "" +} + +type varUpdate struct { + // Name of the variable to update + varname string + // Value to publish (int, float, etc.) + value any + // Treat the count as an increment as opposite to the final value. + inc bool +} + +// Initialize stats reporting through expvar. +func statsInit(mux *http.ServeMux, path string) { + if path == "" || path == "-" { + return + } + + mux.Handle(path, expvar.Handler()) + globals.statsUpdate = make(chan *varUpdate, 1024) + + start := time.Now() + expvar.Publish("Uptime", expvar.Func(func() any { + return time.Since(start).Seconds() + })) + expvar.Publish("NumGoroutines", expvar.Func(func() any { + return runtime.NumGoroutine() + })) + + go statsUpdater() + + logs.Info.Printf("stats: variables exposed at '%s'", path) +} + +func statsRegisterDbStats() { + if f := store.Store.DbStats(); f != nil { + expvar.Publish("DbStats", expvar.Func(f)) + } +} + +// Register integer variable. Don't check for initialization. +func statsRegisterInt(name string) { + expvar.Publish(name, new(expvar.Int)) +} + +// Register histogram variable. `bounds` specifies histogram buckets/bins +// (see comment next to the `histogram` struct definition). +func statsRegisterHistogram(name string, bounds []float64) { + numBuckets := len(bounds) + 1 + expvar.Publish(name, &histogram{ + CountPerBucket: make([]int64, numBuckets), + Bounds: bounds, + }) +} + +// Async publish int variable. +func statsSet(name string, val int64) { + if globals.statsUpdate != nil { + select { + case globals.statsUpdate <- &varUpdate{name, val, false}: + default: + } + } +} + +// Async publish an increment (decrement) to int variable. +func statsInc(name string, val int) { + if globals.statsUpdate != nil { + select { + case globals.statsUpdate <- &varUpdate{name, int64(val), true}: + default: + } + } +} + +// Async publish a value (add a sample) to a histogram variable. +func statsAddHistSample(name string, val float64) { + if globals.statsUpdate != nil { + select { + case globals.statsUpdate <- &varUpdate{varname: name, value: val}: + default: + } + } +} + +// Stop publishing stats. +func statsShutdown() { + if globals.statsUpdate != nil { + globals.statsUpdate <- nil + } +} + +// The go routine which actually publishes stats updates. +func statsUpdater() { + for upd := range globals.statsUpdate { + if upd == nil { + globals.statsUpdate = nil + // Dont' care to close the channel. + break + } + + // Handle var update + if ev := expvar.Get(upd.varname); ev != nil { + switch v := ev.(type) { + case *expvar.Int: + count := upd.value.(int64) + if upd.inc { + v.Add(count) + } else { + v.Set(count) + } + case *histogram: + val := upd.value.(float64) + v.addSample(val) + default: + logs.Err.Panicf("stats: unsupported expvar type %T", ev) + } + } else { + panic("stats: update to unknown variable " + upd.varname) + } + } + + logs.Info.Println("stats: shutdown") +} diff --git a/server/store/mock_store/mock_store.go b/server/store/mock_store/mock_store.go new file mode 100644 index 0000000..e69de29 diff --git a/server/store/store.go b/server/store/store.go new file mode 100644 index 0000000..e69de29 diff --git a/server/store/types/types.go b/server/store/types/types.go new file mode 100644 index 0000000..e69de29 diff --git a/server/store/types/uidgen.go b/server/store/types/uidgen.go new file mode 100644 index 0000000..63d9204 --- /dev/null +++ b/server/store/types/uidgen.go @@ -0,0 +1,105 @@ +package types + +import ( + "encoding/base64" + "encoding/binary" + "errors" + + sf "github.com/tinode/snowflake" + "golang.org/x/crypto/xtea" +) + +// UidGenerator holds snowflake and encryption paramenets. +// RethinkDB generates UUIDs as primary keys. Using snowflake-generated uint64 instead. +type UidGenerator struct { + seq *sf.SnowFlake + cipher *xtea.Cipher +} + +var ErrUninitialized = errors.New("uninitialized") + +// Init initialises the Uid generator +func (ug *UidGenerator) Init(workerID uint, key []byte) error { + var err error + + if ug.seq == nil { + ug.seq, err = sf.NewSnowFlake(uint32(workerID)) + } + if err == nil && ug.cipher == nil { + ug.cipher, err = xtea.NewCipher(key) + } + + return err +} + +// Get generates a unique weakly-encryped random-looking ID. +// The Uid is a unit64 with the highest bit possibly set which makes it +// incompatible with go's pre-1.9 sql package. +func (ug *UidGenerator) Get() Uid { + buf, err := getIDBuffer(ug) + if err != nil { + return ZeroUid + } + return Uid(binary.LittleEndian.Uint64(buf)) +} + +// GetStr generates the same unique ID as Get then returns it as +// base64-encoded string. Slightly more efficient than calling Get() +// then base64-encoding the result. +func (ug *UidGenerator) GetStr() string { + buf, err := getIDBuffer(ug) + if err != nil { + return "" + } + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(buf) +} + +// getIdBuffer returns a byte array holding the Uid bytes +func getIDBuffer(ug *UidGenerator) ([]byte, error) { + if ug == nil || ug.seq == nil { + return nil, ErrUninitialized + } + + var id uint64 + var err error + if id, err = ug.seq.Next(); err != nil { + return nil, err + } + + src := make([]byte, 8) + dst := make([]byte, 8) + binary.LittleEndian.PutUint64(src, id) + ug.cipher.Encrypt(dst, src) + + return dst, nil +} + +// DecodeUid takes an encrypted Uid and decrypts it into a non-negative int64. +// This is needed for go/sql compatibility where uint64 with high bit +// set is unsupported and possibly for other uses such as MySQL's recommendation +// for sequential primary keys. +func (ug *UidGenerator) DecodeUid(uid Uid) int64 { + if ug == nil || ug.seq == nil { + return 0 + } + src := make([]byte, 8) + dst := make([]byte, 8) + binary.LittleEndian.PutUint64(src, uint64(uid)) + ug.cipher.Decrypt(dst, src) + return int64(binary.LittleEndian.Uint64(dst)) +} + +// EncodeInt64 takes a positive int64 and encrypts it into a Uid. +// This is needed for go/sql compatibility where uint64 with high bit +// set is unsupported and possibly for other uses such as MySQL's recommendation +// for sequential primary keys. +func (ug *UidGenerator) EncodeInt64(val int64) Uid { + if ug == nil || ug.seq == nil { + return ZeroUid + } + src := make([]byte, 8) + dst := make([]byte, 8) + binary.LittleEndian.PutUint64(src, uint64(val)) + ug.cipher.Encrypt(dst, src) + return Uid(binary.LittleEndian.Uint64(dst)) +} diff --git a/server/store/types/uidgen_test.go b/server/store/types/uidgen_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-en.templ b/server/templ/email-password-reset-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-es.templ b/server/templ/email-password-reset-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-fr.templ b/server/templ/email-password-reset-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-pt.templ b/server/templ/email-password-reset-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-ru.templ b/server/templ/email-password-reset-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-uk.templ b/server/templ/email-password-reset-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-vi.templ b/server/templ/email-password-reset-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-zh-TW.templ b/server/templ/email-password-reset-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-password-reset-zh.templ b/server/templ/email-password-reset-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-en.templ b/server/templ/email-validation-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-es.templ b/server/templ/email-validation-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-fr.templ b/server/templ/email-validation-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-pt.templ b/server/templ/email-validation-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-ru.templ b/server/templ/email-validation-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-uk.templ b/server/templ/email-validation-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-vi.templ b/server/templ/email-validation-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-zh-TW.templ b/server/templ/email-validation-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/email-validation-zh.templ b/server/templ/email-validation-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-en.templ b/server/templ/sms-universal-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-es.templ b/server/templ/sms-universal-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-fr.templ b/server/templ/sms-universal-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-pt.templ b/server/templ/sms-universal-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-ru.templ b/server/templ/sms-universal-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-uk.templ b/server/templ/sms-universal-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-vi.templ b/server/templ/sms-universal-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-zh-TW.templ b/server/templ/sms-universal-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/templ/sms-universal-zh.templ b/server/templ/sms-universal-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/server/tinode.conf b/server/tinode.conf new file mode 100644 index 0000000..e69de29 diff --git a/server/topic.go b/server/topic.go new file mode 100644 index 0000000..e69de29 diff --git a/server/topic_proxy.go b/server/topic_proxy.go new file mode 100644 index 0000000..e69de29 diff --git a/server/topic_test.go b/server/topic_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/user.go b/server/user.go new file mode 100644 index 0000000..e69de29 diff --git a/server/utils.go b/server/utils.go new file mode 100644 index 0000000..e69de29 diff --git a/server/utils_test.go b/server/utils_test.go new file mode 100644 index 0000000..e69de29 diff --git a/server/validate/email/validate.go b/server/validate/email/validate.go new file mode 100644 index 0000000..e69de29 diff --git a/server/validate/tel/twilio.go b/server/validate/tel/twilio.go new file mode 100644 index 0000000..6ad9dc1 --- /dev/null +++ b/server/validate/tel/twilio.go @@ -0,0 +1,39 @@ +package tel + +import ( + "encoding/json" + + "github.com/twilio/twilio-go" + twapi "github.com/twilio/twilio-go/rest/api/v2010" +) + +type twilioConfig struct { + AccountSid string `json:"account_sid"` + AuthToken string `json:"auth_token"` +} + +var twilioClient *twilio.RestClient + +func twilioInit(jsonconf json.RawMessage) error { + var conf twilioConfig + + if err := json.Unmarshal(jsonconf, &conf); err != nil { + return err + } + + twilioClient = twilio.NewRestClientWithParams(twilio.ClientParams{ + Username: conf.AccountSid, + Password: conf.AuthToken, + }) + + return nil +} + +func twilioSend(from, to, body string) error { + _, err := twilioClient.Api.CreateMessage(&twapi.CreateMessageParams{ + From: &from, + To: &to, + Body: &body, + }) + return err +} diff --git a/server/validate/tel/validate.go b/server/validate/tel/validate.go new file mode 100644 index 0000000..e69de29 diff --git a/server/validate/validator.go b/server/validate/validator.go new file mode 100644 index 0000000..b4d2fe9 --- /dev/null +++ b/server/validate/validator.go @@ -0,0 +1,128 @@ +// Package validate defines an interface which must be implmented by credential validators. +package validate + +import ( + "bytes" + "errors" + "fmt" + "net/url" + "os" + "path/filepath" + "text/template" + + t "github.com/tinode/chat/server/store/types" +) + +// Validator handles validation of user's credentials, like email or phone. +type Validator interface { + // Init initializes the validator. + Init(jsonconf string) error + + // IsInitialized returns true if the validator is initialized. + IsInitialized() bool + + // PreCheck pre-validates the credential without sending an actual request for validation: + // check uniqueness (if appropriate), format, etc + // Returns normalized credential prefixed with an appropriate namespace prefix. + PreCheck(cred string, params map[string]any) (string, error) + + // Request sends a request for validation to the user. Returns true if it's a new credential, + // false if it re-sent request for an existing unconfirmed credential. + // user: UID of the user making the request. + // cred: credential being validated, such as email or phone. + // lang: user's human language as repored in the session. + // resp: optional response if user already has it (i.e. captcha/recaptcha). + // tmpToken: temporary authentication token to include in the request. + Request(user t.Uid, cred, lang, resp string, tmpToken []byte) (bool, error) + + // ResetSecret sends a message with instructions for resetting an authentication secret. + // cred: address to use for the message. + // scheme: authentication scheme being reset. + // lang: human language as reported in the session. + // tmpToken: temporary authentication token + // params: authentication params. + ResetSecret(cred, scheme, lang string, tmpToken []byte, params map[string]any) error + + // Check checks validity of user's response. + // Returns the value of validated credential on success. + Check(user t.Uid, resp string) (string, error) + + // Remove deletes or deactivates user's given value. + Remove(user t.Uid, value string) error + + // Delete deletes user's record. + Delete(user t.Uid) error + + // TempAuthScheme returns a temporary authentication method used by this validator. + // It should be either "code" or "token". + TempAuthScheme() (string, error) +} + +func ValidateHostURL(origUrl string) (string, error) { + hostUrl, err := url.Parse(origUrl) + if err != nil { + return "", err + } + if !hostUrl.IsAbs() { + return "", errors.New("host_url must be absolute") + } + if hostUrl.Hostname() == "" { + return "", errors.New("invalid host_url") + } + if hostUrl.Fragment != "" { + return "", errors.New("fragment is not allowed in host_url") + } + if hostUrl.Path == "" { + hostUrl.Path = "/" + } + return hostUrl.String(), nil +} + +func ExecuteTemplate(template *template.Template, parts []string, params map[string]any) (map[string]string, error) { + content := map[string]string{} + buffer := new(bytes.Buffer) + + if parts == nil { + if err := template.Execute(buffer, params); err != nil { + return nil, err + } + content[""] = buffer.String() + } else { + for _, part := range parts { + buffer.Reset() + if templBody := template.Lookup(part); templBody != nil { + if err := templBody.Execute(buffer, params); err != nil { + return nil, err + } + } + content[part] = buffer.String() + } + } + + return content, nil +} + +func ResolveTemplatePath(path string) (string, error) { + if filepath.IsAbs(path) { + return path, nil + } + + curwd, err := os.Getwd() + if err != nil { + return "", err + } + + return filepath.Clean(filepath.Join(curwd, path)), nil +} + +func ReadTemplateFile(pathTempl *template.Template, lang string) (*template.Template, string, error) { + buffer := bytes.Buffer{} + err := pathTempl.Execute(&buffer, map[string]any{"Language": lang}) + path := buffer.String() + if err != nil { + return nil, path, fmt.Errorf("reading %s: %w", path, err) + } + + templ, err := template.ParseFiles(path) + return templ, path, err +} diff --git a/templ/email-password-reset-en.templ b/templ/email-password-reset-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-es.templ b/templ/email-password-reset-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-fr.templ b/templ/email-password-reset-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-pt.templ b/templ/email-password-reset-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-ru.templ b/templ/email-password-reset-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-uk.templ b/templ/email-password-reset-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-vi.templ b/templ/email-password-reset-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-zh-TW.templ b/templ/email-password-reset-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-password-reset-zh.templ b/templ/email-password-reset-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-en.templ b/templ/email-validation-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-es.templ b/templ/email-validation-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-fr.templ b/templ/email-validation-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-pt.templ b/templ/email-validation-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-ru.templ b/templ/email-validation-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-uk.templ b/templ/email-validation-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-vi.templ b/templ/email-validation-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-zh-TW.templ b/templ/email-validation-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/email-validation-zh.templ b/templ/email-validation-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-en.templ b/templ/sms-universal-en.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-es.templ b/templ/sms-universal-es.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-fr.templ b/templ/sms-universal-fr.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-pt.templ b/templ/sms-universal-pt.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-ru.templ b/templ/sms-universal-ru.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-uk.templ b/templ/sms-universal-uk.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-vi.templ b/templ/sms-universal-vi.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-zh-TW.templ b/templ/sms-universal-zh-TW.templ new file mode 100644 index 0000000..e69de29 diff --git a/templ/sms-universal-zh.templ b/templ/sms-universal-zh.templ new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/README.md b/tinode-db/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/abc-128.jpg b/tinode-db/abc-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/abcdef-128.jpg b/tinode-db/abcdef-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/alice-128.jpg b/tinode-db/alice-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/bf-128.jpg b/tinode-db/bf-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/bob-128.jpg b/tinode-db/bob-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/carol-128.jpg b/tinode-db/carol-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/chan-128.jpg b/tinode-db/chan-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/credentials.sh b/tinode-db/credentials.sh new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/data.json b/tinode-db/data.json new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/dave-128.jpg b/tinode-db/dave-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/eve-128.jpg b/tinode-db/eve-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/gendb.go b/tinode-db/gendb.go new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/generate_dataset.py b/tinode-db/generate_dataset.py new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/main.go b/tinode-db/main.go new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/support-128.jpg b/tinode-db/support-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/tino-128.jpg b/tinode-db/tino-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/tinode.conf b/tinode-db/tinode.conf new file mode 100644 index 0000000..e69de29 diff --git a/tinode-db/xena-128.jpg b/tinode-db/xena-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/CODE-STRUCTURE.md b/tn-cli/CODE-STRUCTURE.md new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/LICENSE b/tn-cli/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/README.md b/tn-cli/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/client.py b/tn-cli/client.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/commands.py b/tn-cli/commands.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/input_handler.py b/tn-cli/input_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/macros.py b/tn-cli/macros.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/requirements.txt b/tn-cli/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/sample-macro-script.txt b/tn-cli/sample-macro-script.txt new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/sample-script.txt b/tn-cli/sample-script.txt new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/test-128.jpg b/tn-cli/test-128.jpg new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/tn-cli.py b/tn-cli/tn-cli.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/tn_globals.py b/tn-cli/tn_globals.py new file mode 100644 index 0000000..e69de29 diff --git a/tn-cli/utils.py b/tn-cli/utils.py new file mode 100644 index 0000000..e69de29