diff --git a/docker/tinode/Dockerfile b/docker/tinode/Dockerfile index c7c2473..20a6686 100644 --- a/docker/tinode/Dockerfile +++ b/docker/tinode/Dockerfile @@ -41,7 +41,7 @@ ENV RESET_DB=false ENV UPGRADE_DB=false # Option to skip DB initialization when it's missing. -ENV NO_DB_INIT=false +ENV NO_DB_INIT=true # Load sample data to database from data.json. ARG SAMPLE_DATA=data.json @@ -130,7 +130,7 @@ ENV TNPG_ORG= # Video calls configuration. ENV WEBRTC_ENABLED=false -ENV ICE_SERVERS_FILE= +ENV ICE_SERVERS_FILE=/opt/tinode/ice-servers.json # Use the target db by default. # When TARGET_DB is "alldbs", it is the user's responsibility @@ -153,6 +153,7 @@ WORKDIR /opt/tinode # Copy config template to the container. COPY config.template . COPY entrypoint.sh . +COPY ice-servers.json . # Get the desired Tinode build. ADD https://github.com/tinode/chat/releases/download/v$BINVERS/tinode-$TARGET_DB.linux-amd64.tar.gz . diff --git a/docker/tinode/Dockerfile.build b/docker/tinode/Dockerfile.build index 2138643..3310a99 100644 --- a/docker/tinode/Dockerfile.build +++ b/docker/tinode/Dockerfile.build @@ -27,7 +27,7 @@ LABEL name="ImServer" ENV WAIT_FOR= ENV RESET_DB=false ENV UPGRADE_DB=false -ENV NO_DB_INIT=false +ENV NO_DB_INIT=true # Empty by default: do not load Tinode's canned demo users into a real deployment. # Override explicitly if sample data is ever wanted. ENV SAMPLE_DATA= @@ -51,6 +51,7 @@ ENV FCM_PUSH_ENABLED=false ENV FCM_INCLUDE_ANDROID_NOTIFICATION=true ENV TNPG_PUSH_ENABLED=false ENV WEBRTC_ENABLED=false +ENV ICE_SERVERS_FILE=/opt/tinode/ice-servers.json ENV SERVER_STATUS_PATH='' ENV ACC_GC_ENABLED=false ENV PLUGIN_PYTHON_CHAT_BOT_ENABLED=false @@ -68,6 +69,7 @@ WORKDIR /opt/tinode COPY docker/tinode/config.template . COPY docker/tinode/entrypoint.sh . +COPY docker/tinode/ice-servers.json . COPY tinode-db/credentials.sh . COPY server/templ ./templ COPY --from=build /out/tinode . diff --git a/docker/tinode/entrypoint.sh b/docker/tinode/entrypoint.sh index 00856f5..dd51a09 100644 --- a/docker/tinode/entrypoint.sh +++ b/docker/tinode/entrypoint.sh @@ -113,27 +113,59 @@ if [ ! -z "$WAIT_FOR" ] ; then until nc -z -v -w5 ${DB[0]} ${DB[1]}; do echo "waiting for ${WAIT_FOR}..."; sleep 3; done fi -# Initialize the database if it has not been initialized yet or if data reset/upgrade has been requested. -init_stdout=./init-db-stdout.txt -./init-db \ - --reset=${RESET_DB} \ - --upgrade=${UPGRADE_DB} \ - --config=${CONFIG} \ - --data=${SAMPLE_DATA} \ - --no_init=${NO_DB_INIT} \ - 1>${init_stdout} -if [ $? -ne 0 ]; then - echo "./init-db failed. Quitting." - exit 1 -fi +run_with_retries() { + local name="$1" + shift + local attempts="${STARTUP_RETRY_ATTEMPTS:-10}" + local delay="${STARTUP_RETRY_DELAY_SECONDS:-3}" + local i=1 + while [ "$i" -le "$attempts" ]; do + "$@" + local status=$? + if [ "$status" -eq 0 ]; then + return 0 + fi + echo "$name failed with status $status (attempt $i/$attempts)." + if [ "$i" -eq "$attempts" ]; then + return "$status" + fi + sleep "$delay" + i=$((i + 1)) + done +} -# Create/upgrade the hotgo tenant registry and the workbench/org/meetings/ -# reports/mailbox/etc. tables that init-db above does not manage. Idempotent -# (IF NOT EXISTS / ON DUPLICATE KEY UPDATE throughout), safe to run every start. -IM_MYSQL_DSN="${MYSQL_DSN}" ./apply-hotgo-tenant-db -if [ $? -ne 0 ]; then - echo "./apply-hotgo-tenant-db failed. Quitting." - exit 1 +init_db_once() { + ./init-db \ + --reset=${RESET_DB} \ + --upgrade=${UPGRADE_DB} \ + --config=${CONFIG} \ + --data=${SAMPLE_DATA} \ + --no_init=${NO_DB_INIT} \ + 1>${init_stdout} +} + +apply_hotgo_tenant_db_once() { + IM_MYSQL_DSN="${MYSQL_DSN}" ./apply-hotgo-tenant-db +} + +init_stdout=./init-db-stdout.txt +if [ "$NO_DB_INIT" = "true" ] ; then + echo "NO_DB_INIT=true: skipping init-db and apply-hotgo-tenant-db." +else + # Initialize the database if it has not been initialized yet or if data reset/upgrade has been requested. + run_with_retries "init-db" init_db_once + if [ $? -ne 0 ]; then + echo "./init-db failed. Quitting." + exit 1 + fi + + # Create/upgrade the hotgo tenant registry and the workbench/org/meetings/ + # reports/mailbox/etc. tables that init-db above does not manage. + run_with_retries "apply-hotgo-tenant-db" apply_hotgo_tenant_db_once + if [ $? -ne 0 ]; then + echo "./apply-hotgo-tenant-db failed. Quitting." + exit 1 + fi fi # If sample data was provided, try to find Tino password. diff --git a/docker/tinode/ice-servers.json b/docker/tinode/ice-servers.json new file mode 100644 index 0000000..07f1d7f --- /dev/null +++ b/docker/tinode/ice-servers.json @@ -0,0 +1,5 @@ +[ + { + "urls": ["stun:stun.l.google.com:19302"] + } +] diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index d11233e..d540781 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -996,6 +996,10 @@ func (a *adapter) UpgradeDb() error { return err } + if err := a.ensureMessageTopicSeqIndex(); err != nil { + return err + } + if a.version == adpVersion { return nil } @@ -1004,6 +1008,22 @@ func (a *adapter) UpgradeDb() error { strconv.Itoa(a.version) + " to " + strconv.Itoa(adpVersion) + " is not supported") } +func (a *adapter) ensureMessageTopicSeqIndex() error { + const indexName = "messages_topic_seqid" + var count int + if err := a.db.QueryRow( + `SELECT COUNT(*) FROM information_schema.statistics + WHERE table_schema=DATABASE() AND table_name='im_messages' AND index_name=?`, + indexName).Scan(&count); err != nil { + return err + } + if count > 0 { + return nil + } + _, err := a.db.Exec("ALTER TABLE im_messages ADD UNIQUE INDEX messages_topic_seqid(topic, seqid)") + return err +} + func createIMNotificationTables(tx *sql.Tx) error { _, err := tx.Exec( `CREATE TABLE im_system_notifications( diff --git a/server/store/types/default_access_test.go b/server/store/types/default_access_test.go new file mode 100644 index 0000000..025bdb9 --- /dev/null +++ b/server/store/types/default_access_test.go @@ -0,0 +1,30 @@ +package types + +import "testing" + +func TestDefaultAccessScanAllowsNil(t *testing.T) { + var da DefaultAccess + if err := da.Scan(nil); err != nil { + t.Fatalf("Scan(nil) returned error: %v", err) + } + if da.Auth != ModeNone || da.Anon != ModeNone { + t.Fatalf("Scan(nil) changed default access: %#v", da) + } +} + +func TestDefaultAccessScanJSON(t *testing.T) { + for name, input := range map[string]any{ + "bytes": []byte(`{"Auth":"JRWP","Anon":"N"}`), + "string": `{"Auth":"JRWP","Anon":"N"}`, + } { + t.Run(name, func(t *testing.T) { + var da DefaultAccess + if err := da.Scan(input); err != nil { + t.Fatalf("Scan returned error: %v", err) + } + if da.Auth.String() != "JRWP" || da.Anon.String() != "N" { + t.Fatalf("unexpected default access: %#v", da) + } + }) + } +} diff --git a/server/store/types/types.go b/server/store/types/types.go index 6dc0d63..c8c4b92 100644 --- a/server/store/types/types.go +++ b/server/store/types/types.go @@ -863,7 +863,17 @@ type DefaultAccess struct { // Scan is an implementation of Scanner interface so the value can be read from SQL DBs // It assumes the value is serialized and stored as JSON func (da *DefaultAccess) Scan(val any) error { - return json.Unmarshal(val.([]byte), da) + if val == nil { + return nil + } + switch v := val.(type) { + case []byte: + return json.Unmarshal(v, da) + case string: + return json.Unmarshal([]byte(v), da) + default: + return errors.New("scan failed: data is not a byte slice or string") + } } // Value implements sql's driver.Valuer interface. diff --git a/server/tinode.conf b/server/tinode.conf index 4d0455b..52e2e7e 100644 --- a/server/tinode.conf +++ b/server/tinode.conf @@ -142,12 +142,6 @@ } ], - "webrtc": { - "enabled": false, - "call_establishment_timeout": 30, - "ice_servers": [] - }, - "cluster_config": { "self": "", "nodes": [],