most-box 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -9
- package/electron/deepLink.js +29 -0
- package/electron/main.js +87 -20
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/AppShell-BOtfY12t.js +1 -0
- package/out/assets/FilePreviewOverlay-C5qK9HAE.js +1 -0
- package/out/assets/LanguageToggle-Di5b88mK.js +1 -0
- package/out/assets/LogoIcon-B2fFe0l1.js +1 -0
- package/out/assets/MarketingHeader-BOytKcCc.js +1 -0
- package/out/assets/MarketingLayout-B8m1Q7Pa.js +1 -0
- package/out/assets/{MarketingThemeToggle-DBaC9bjz.js → MarketingThemeToggle-C6fggkl7.js} +1 -1
- package/out/assets/{MilkdownEditor-BqJzntYE.js → MilkdownEditor-Cfze75zl.js} +1 -1
- package/out/assets/OpenSidebarButton-DDuALgJ2.js +1 -0
- package/out/assets/SidebarAccount-bo1ypzrJ.js +1 -0
- package/out/assets/arrow-right-RldN7Rwi.js +1 -0
- package/out/assets/circle-alert-BQoSDxUe.js +1 -0
- package/out/assets/cloud-DHoTMeZY.js +1 -0
- package/out/assets/copy-BtJHJnqH.js +1 -0
- package/out/assets/download-Bg-OdoxM.js +1 -0
- package/out/assets/downloadValidation-CUvbvj9f.js +1 -0
- package/out/assets/external-link-m8ZIQe4p.js +1 -0
- package/out/assets/file-text-DG1orIkZ.js +1 -0
- package/out/assets/game-s6irY8hS.js +1 -0
- package/out/assets/hard-drive-BB-sllXA.js +1 -0
- package/out/assets/index-3OD3Chi9.css +1 -0
- package/out/assets/index-BSOvFG3o.css +1 -0
- package/out/assets/index-C0xqKeu-.css +1 -0
- package/out/assets/index-CrAXrmfP.js +31 -0
- package/out/assets/index.lazy-0Njp0U6I.js +1 -0
- package/out/assets/index.lazy-BTndBeBF.js +1 -0
- package/out/assets/index.lazy-Bq39jYTl.js +1 -0
- package/out/assets/index.lazy-Bu93oGzJ.js +2 -0
- package/out/assets/index.lazy-BvdBwgHx.js +1 -0
- package/out/assets/index.lazy-CIx8ist6.js +3 -0
- package/out/assets/index.lazy-DK7R297q.js +1 -0
- package/out/assets/index.lazy-DVOcrcEB.js +2 -0
- package/out/assets/index.lazy-DejOniAm.js +1 -0
- package/out/assets/index.lazy-DfsgyiMN.js +1 -0
- package/out/assets/index.lazy-DpNIiSXF.js +1 -0
- package/out/assets/index.lazy-Vnvz-t7T.js +1 -0
- package/out/assets/index.lazy-v7nBJ-sF.js +1 -0
- package/out/assets/key-round-DIQ3Xt5F.js +1 -0
- package/out/assets/lock-tf1t2yuy.js +1 -0
- package/out/assets/message-square-DaQH7q-P.js +1 -0
- package/out/assets/mp-DvFTsIL9.js +1 -0
- package/out/assets/music-DaFvU2DL.js +1 -0
- package/out/assets/notebook-pen-CqEFOHKx.js +1 -0
- package/out/assets/play-B7q6F75-.js +1 -0
- package/out/assets/plus-H2i2mspM.js +1 -0
- package/out/assets/refresh-cw-roxAhABl.js +1 -0
- package/out/assets/save-DR0O9ReR.js +1 -0
- package/out/assets/search-ncblG-zw.js +1 -0
- package/out/assets/send-m1XCcuPn.js +1 -0
- package/out/assets/shield-check-BwcvTU4U.js +1 -0
- package/out/assets/trash-2-CNpsqYc1.js +1 -0
- package/out/assets/triangle-alert-DP9EP7IM.js +1 -0
- package/out/assets/upload-V--8p13l.js +1 -0
- package/out/assets/useChannelMessages-46C52EyL.js +3 -0
- package/out/assets/useGameRoom-BtxPpfck.js +1 -0
- package/out/assets/useNavigate-DWlBD_-b.js +1 -0
- package/out/assets/userStore-C4vdYsQp.js +4 -0
- package/out/assets/wallet-CozFU6yK.js +1 -0
- package/out/assets/wifi-DIR3g_8A.js +1 -0
- package/out/avatars/default/LICENSE.md +7 -0
- package/out/avatars/default/dolphin.svg +25 -0
- package/out/avatars/default/owl.svg +60 -0
- package/out/avatars/default/panda.svg +29 -0
- package/out/avatars/default/snow-mountain.svg +100 -0
- package/out/avatars/default/tiger.svg +55 -0
- package/out/avatars/default/turtle.svg +34 -0
- package/out/chat/index.html +0 -0
- package/out/chat/join/index.html +0 -0
- package/out/download/index.html +4 -3
- package/out/game/gandengyan/index.html +0 -0
- package/out/game/index.html +0 -0
- package/out/game/zhajinhua/index.html +0 -0
- package/out/index.html +4 -3
- package/out/note/index.html +0 -0
- package/out/ping/index.html +4 -3
- package/out/{demo → profile}/index.html +0 -0
- package/out/web3/index.html +0 -0
- package/package.json +9 -1
- package/public/avatars/default/LICENSE.md +7 -0
- package/public/avatars/default/dolphin.svg +25 -0
- package/public/avatars/default/owl.svg +60 -0
- package/public/avatars/default/panda.svg +29 -0
- package/public/avatars/default/snow-mountain.svg +100 -0
- package/public/avatars/default/tiger.svg +55 -0
- package/public/avatars/default/turtle.svg +34 -0
- package/server/index.js +36 -7
- package/server/src/core/channelIdentity.js +6 -11
- package/server/src/core/gameRoom.js +15 -5
- package/server/src/core/mostLink.js +8 -7
- package/server/src/core/zhajinhua.js +6 -0
- package/server/src/games/gandengyan.js +10 -0
- package/server/src/http/access.js +63 -12
- package/server/src/http/app.js +34 -840
- package/server/src/http/nodeStatus.js +101 -9
- package/server/src/http/routePolicy.js +2 -0
- package/server/src/http/routes/channelRoutes.js +163 -0
- package/server/src/http/routes/fileRoutes.js +345 -0
- package/server/src/http/routes/nodeRoutes.js +323 -0
- package/server/src/http/routes/seedRoutes.js +58 -0
- package/server/src/index.js +483 -265
- package/server/src/node/config.js +2 -6
- package/server/src/utils/avatar.js +59 -3
- package/server/src/utils/downloadMessages.js +0 -2
- package/out/assets/AppShell-CQhg6DJU.js +0 -1
- package/out/assets/ChatUi-BepWs-ZU.js +0 -1
- package/out/assets/LanguageToggle-CtzCCAYv.js +0 -1
- package/out/assets/LogoIcon-Dxto3Sb4.js +0 -1
- package/out/assets/MarketingLayout-BQw0IS2i.js +0 -1
- package/out/assets/MoveModal-4D9n11Kw.js +0 -1
- package/out/assets/Nav-9MDdvgNs.js +0 -1
- package/out/assets/NoteSidebar-C-rIt32H.js +0 -1
- package/out/assets/OpenSidebarButton-Dd0JmKuE.js +0 -1
- package/out/assets/PemBlock-C8dEIzu-.js +0 -1
- package/out/assets/SidebarAccount-ClS-N0lq.js +0 -1
- package/out/assets/arrow-right-urE9Rd7j.js +0 -1
- package/out/assets/channelApi-BwQU0-h1.js +0 -1
- package/out/assets/check-DUNsD2t6.js +0 -1
- package/out/assets/chevron-down-D6mpsfv4.js +0 -1
- package/out/assets/circle-alert-W0iyN4sC.js +0 -1
- package/out/assets/cloud-BMyOoC2x.js +0 -1
- package/out/assets/code-B1Cb_Icm.js +0 -1
- package/out/assets/copy-C1MttOli.js +0 -1
- package/out/assets/download-y7SZXu6E.js +0 -1
- package/out/assets/downloadValidation-B0p9Ai_9.js +0 -1
- package/out/assets/filePreview-UI9NH34f.js +0 -1
- package/out/assets/game-CdU3xnZo.js +0 -1
- package/out/assets/hard-drive-D13Qbobu.js +0 -1
- package/out/assets/image-DJCA16l_.js +0 -1
- package/out/assets/index-8eWJAjpY.css +0 -1
- package/out/assets/index-BZc4blbW.css +0 -1
- package/out/assets/index-BdaFEQG-.css +0 -1
- package/out/assets/index-QxXZzOUL.js +0 -33
- package/out/assets/index.lazy-BBTTFanX.js +0 -1
- package/out/assets/index.lazy-BG4ZylHD.js +0 -2
- package/out/assets/index.lazy-Bi-6ZXZX.js +0 -1
- package/out/assets/index.lazy-BixWVr0B.js +0 -1
- package/out/assets/index.lazy-BjFwNYy5.js +0 -3
- package/out/assets/index.lazy-C8EIQsXY.js +0 -2
- package/out/assets/index.lazy-CarNe2uu.js +0 -1
- package/out/assets/index.lazy-DEuGu3H3.js +0 -1
- package/out/assets/index.lazy-GPyILCA7.js +0 -3
- package/out/assets/index.lazy-I8ofndXl.js +0 -1
- package/out/assets/index.lazy-TxhWsA7y.js +0 -1
- package/out/assets/index.lazy-azfky8k7.js +0 -1
- package/out/assets/key-round-CZniN9lv.js +0 -1
- package/out/assets/lock-D5OSNhep.js +0 -1
- package/out/assets/log-out-B6phyZ5z.js +0 -1
- package/out/assets/music-CbUskKgg.js +0 -1
- package/out/assets/notebook-pen-DqKDQ6MJ.js +0 -1
- package/out/assets/play-BIl8q9eU.js +0 -1
- package/out/assets/plus-BxxbpH6Q.js +0 -1
- package/out/assets/save-DkH1n_Ov.js +0 -1
- package/out/assets/search-BQi5Z0E-.js +0 -1
- package/out/assets/send-Cl6NtD2T.js +0 -1
- package/out/assets/trash-2-BBjpgK_f.js +0 -1
- package/out/assets/triangle-alert-l98G8u9O.js +0 -1
- package/out/assets/upload-ByP6Ydde.js +0 -1
- package/out/assets/useChannelMessages-BgbYfF2c.js +0 -3
- package/out/assets/useGameRoom-DPmweWwe.js +0 -1
- package/out/assets/wallet-c7zIhNSM.js +0 -1
- package/out/assets/wifi-Bm4biAjc.js +0 -1
package/server/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import b4a from 'b4a'
|
|
|
16
16
|
import crypto from 'node:crypto'
|
|
17
17
|
import fs from 'node:fs'
|
|
18
18
|
import path from 'node:path'
|
|
19
|
+
import { Duplex } from 'node:stream'
|
|
19
20
|
|
|
20
21
|
import { calculateCid, parseMostLink, buildMostLink } from './core/cid.js'
|
|
21
22
|
import { normalizeChannelAttachment } from './core/channelAttachment.js'
|
|
@@ -28,11 +29,10 @@ import {
|
|
|
28
29
|
normalizeChannelDisplayName,
|
|
29
30
|
normalizeChannelAvatar,
|
|
30
31
|
normalizeChannelId,
|
|
31
|
-
createChannelFingerprint,
|
|
32
32
|
createChannelWriterId,
|
|
33
33
|
buildChannelKey,
|
|
34
34
|
normalizeChannelKey,
|
|
35
|
-
|
|
35
|
+
isSpecialChannel,
|
|
36
36
|
uniqueStrings,
|
|
37
37
|
} from './core/channelIdentity.js'
|
|
38
38
|
import { getPathBaseName, getDisplayPathFolder } from './core/displayPath.js'
|
|
@@ -99,6 +99,44 @@ import {
|
|
|
99
99
|
|
|
100
100
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
101
101
|
|
|
102
|
+
function createMemoryDuplexPair() {
|
|
103
|
+
let left
|
|
104
|
+
let right
|
|
105
|
+
|
|
106
|
+
left = new Duplex({
|
|
107
|
+
read() {},
|
|
108
|
+
write(chunk, _encoding, callback) {
|
|
109
|
+
if (!right.destroyed) right.push(chunk)
|
|
110
|
+
callback()
|
|
111
|
+
},
|
|
112
|
+
final(callback) {
|
|
113
|
+
if (!right.destroyed) right.push(null)
|
|
114
|
+
callback()
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
right = new Duplex({
|
|
119
|
+
read() {},
|
|
120
|
+
write(chunk, _encoding, callback) {
|
|
121
|
+
if (!left.destroyed) left.push(chunk)
|
|
122
|
+
callback()
|
|
123
|
+
},
|
|
124
|
+
final(callback) {
|
|
125
|
+
if (!left.destroyed) left.push(null)
|
|
126
|
+
callback()
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
left.on('close', () => {
|
|
131
|
+
if (!right.destroyed) right.destroy()
|
|
132
|
+
})
|
|
133
|
+
right.on('close', () => {
|
|
134
|
+
if (!left.destroyed) left.destroy()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return [left, right]
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
export class MostBoxEngine extends EventEmitter {
|
|
103
141
|
#store = null
|
|
104
142
|
#swarm = null
|
|
@@ -123,12 +161,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
123
161
|
#channelIdDiscoveries = new Map()
|
|
124
162
|
#channelPeers = new Map()
|
|
125
163
|
#channelCandidateCache = new Map()
|
|
164
|
+
#channelStreams = new Set()
|
|
126
165
|
|
|
127
166
|
#userSyncSessions = new Map()
|
|
128
167
|
#userSyncCores = new Map()
|
|
129
168
|
#userSyncCoreOffsets = new Map()
|
|
130
169
|
#userSyncDiscoveries = new Map()
|
|
131
|
-
#userSyncMetadata = { sessions: {}, clocks: {} }
|
|
170
|
+
#userSyncMetadata = { sessions: {}, clocks: {}, profiles: {} }
|
|
132
171
|
|
|
133
172
|
#chatSwarm = null
|
|
134
173
|
|
|
@@ -387,6 +426,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
387
426
|
this.#channelIdDiscoveries.clear()
|
|
388
427
|
this.#channelPeers.clear()
|
|
389
428
|
this.#channelCandidateCache.clear()
|
|
429
|
+
this.#channelStreams.clear()
|
|
390
430
|
this.#channels = []
|
|
391
431
|
|
|
392
432
|
for (const [, coresMap] of this.#userSyncCores) {
|
|
@@ -1739,10 +1779,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1739
1779
|
|
|
1740
1780
|
const left = this.#store.replicate(true, { live: true })
|
|
1741
1781
|
const right = peerEngine.#store.replicate(false, { live: true })
|
|
1782
|
+
const [leftChat, rightChat] = createMemoryDuplexPair()
|
|
1742
1783
|
|
|
1743
1784
|
left.on('error', () => {})
|
|
1744
1785
|
right.on('error', () => {})
|
|
1786
|
+
leftChat.on('error', () => {})
|
|
1787
|
+
rightChat.on('error', () => {})
|
|
1745
1788
|
left.pipe(right).pipe(left)
|
|
1789
|
+
this.#handleChannelConnection(leftChat).catch(() => {})
|
|
1790
|
+
peerEngine.#handleChannelConnection(rightChat).catch(() => {})
|
|
1746
1791
|
this.#exchangeUserSyncSessions(peerEngine).catch(() => {})
|
|
1747
1792
|
peerEngine.#exchangeUserSyncSessions(this).catch(() => {})
|
|
1748
1793
|
|
|
@@ -1750,6 +1795,8 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1750
1795
|
close: () => {
|
|
1751
1796
|
left.destroy()
|
|
1752
1797
|
right.destroy()
|
|
1798
|
+
leftChat.destroy()
|
|
1799
|
+
rightChat.destroy()
|
|
1753
1800
|
},
|
|
1754
1801
|
}
|
|
1755
1802
|
}
|
|
@@ -1837,6 +1884,58 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
1837
1884
|
}
|
|
1838
1885
|
}
|
|
1839
1886
|
|
|
1887
|
+
getUserProfile(ownerAddressInput) {
|
|
1888
|
+
this.#ensureInitialized()
|
|
1889
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1890
|
+
if (!ownerAddress) {
|
|
1891
|
+
throw new ValidationError('valid owner address is required')
|
|
1892
|
+
}
|
|
1893
|
+
const profile = this.#userSyncMetadata.profiles?.[ownerAddress]
|
|
1894
|
+
return profile ? { ...profile } : null
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
saveUserProfile(ownerAddressInput, profileInput = {}) {
|
|
1898
|
+
this.#ensureInitialized()
|
|
1899
|
+
const ownerAddress = normalizeOwnerAddress(ownerAddressInput)
|
|
1900
|
+
if (!ownerAddress) {
|
|
1901
|
+
throw new ValidationError('valid owner address is required')
|
|
1902
|
+
}
|
|
1903
|
+
const existing = this.getUserProfile(ownerAddress)
|
|
1904
|
+
const profile = this.#normalizeUserProfileRecord(
|
|
1905
|
+
ownerAddress,
|
|
1906
|
+
profileInput,
|
|
1907
|
+
getNextSyncTimestamp(existing?.syncUpdatedAt)
|
|
1908
|
+
)
|
|
1909
|
+
if (!profile) {
|
|
1910
|
+
throw new ValidationError('valid profile is required')
|
|
1911
|
+
}
|
|
1912
|
+
if (existing && profile.syncUpdatedAt <= existing.syncUpdatedAt) {
|
|
1913
|
+
return { ...existing }
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
this.#userSyncMetadata.profiles = this.#userSyncMetadata.profiles || {}
|
|
1917
|
+
this.#userSyncMetadata.profiles[ownerAddress] = profile
|
|
1918
|
+
this.#setUserSyncClock(ownerAddress, 'profile', profile.syncUpdatedAt)
|
|
1919
|
+
this.#appendUserSyncOpSoon(ownerAddress, 'profile:upsert', { profile })
|
|
1920
|
+
|
|
1921
|
+
const changedChannels = this.#applyUserProfileToJoinedChannels(
|
|
1922
|
+
ownerAddress,
|
|
1923
|
+
profile
|
|
1924
|
+
)
|
|
1925
|
+
if (changedChannels) {
|
|
1926
|
+
this.#saveChannelsMetadata()
|
|
1927
|
+
this.emit('user:metadata:updated', {
|
|
1928
|
+
ownerAddress,
|
|
1929
|
+
scope: 'channels',
|
|
1930
|
+
})
|
|
1931
|
+
}
|
|
1932
|
+
this.emit('user:metadata:updated', {
|
|
1933
|
+
ownerAddress,
|
|
1934
|
+
scope: 'profile',
|
|
1935
|
+
})
|
|
1936
|
+
return { ...profile }
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1840
1939
|
async cacheFile(cid, options = {}) {
|
|
1841
1940
|
this.#ensureInitialized()
|
|
1842
1941
|
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
@@ -2050,7 +2149,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2050
2149
|
// --- 频道管理 ---
|
|
2051
2150
|
|
|
2052
2151
|
/**
|
|
2053
|
-
* 创建或加入频道。channelId 是用户输入的短 ID,channelKey
|
|
2152
|
+
* 创建或加入频道。channelId 是用户输入的短 ID,channelKey 与频道名一致。
|
|
2054
2153
|
* @param {string} channelIdInput - 用户可见短频道 ID
|
|
2055
2154
|
* @param {string} [type='personal'] - 频道类型
|
|
2056
2155
|
* @returns {Promise<object>}
|
|
@@ -2060,8 +2159,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2060
2159
|
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
2061
2160
|
const channelId = normalizeChannelId(channelIdInput)
|
|
2062
2161
|
const channelType = String(type || 'personal').trim() || 'personal'
|
|
2063
|
-
const selectedChannelKey = normalizeChannelKey(options.channelKey)
|
|
2064
|
-
const selectedFingerprint = String(options.fingerprint || '').trim()
|
|
2065
2162
|
|
|
2066
2163
|
if (channelId.includes('.') && channelType !== 'game') {
|
|
2067
2164
|
throw new Error('点号为系统保留,不能用于手动频道 ID')
|
|
@@ -2079,30 +2176,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2079
2176
|
throw new Error(`频道名最多 ${CHANNEL_NAME_MAX_LENGTH} 个字符`)
|
|
2080
2177
|
}
|
|
2081
2178
|
|
|
2082
|
-
if (selectedChannelKey || selectedFingerprint) {
|
|
2083
|
-
const channelKey =
|
|
2084
|
-
selectedChannelKey || buildChannelKey(channelId, selectedFingerprint)
|
|
2085
|
-
const existing = this.#channels.find(c => c.channelKey === channelKey)
|
|
2086
|
-
if (existing) {
|
|
2087
|
-
await this.#mergeChannelWriterCoreKeys(
|
|
2088
|
-
existing,
|
|
2089
|
-
options.writerCoreKeys
|
|
2090
|
-
)
|
|
2091
|
-
if (this.#upsertChannelMember(existing, options)) {
|
|
2092
|
-
existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
|
|
2093
|
-
this.#saveChannelsMetadata()
|
|
2094
|
-
this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
|
|
2095
|
-
}
|
|
2096
|
-
return this.#formatChannelForResponse(existing, ownerAddress)
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
const candidate = this.#getCachedChannelCandidate(channelId, channelKey)
|
|
2100
|
-
if (!candidate) {
|
|
2101
|
-
throw new Error('未发现该频道候选,请重新搜索频道')
|
|
2102
|
-
}
|
|
2103
|
-
return this.#joinChannelFromCandidate(candidate, channelType, options)
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
2179
|
const localCandidates = this.#getLocalChannelCandidates(channelId)
|
|
2107
2180
|
const remoteCandidates = options.discover
|
|
2108
2181
|
? await this.#discoverChannelCandidates(channelId, {
|
|
@@ -2114,28 +2187,26 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2114
2187
|
...remoteCandidates,
|
|
2115
2188
|
])
|
|
2116
2189
|
|
|
2117
|
-
if (candidates.length >
|
|
2118
|
-
return {
|
|
2119
|
-
conflict: true,
|
|
2120
|
-
channelId,
|
|
2121
|
-
candidates: candidates.map(candidate =>
|
|
2122
|
-
this.#formatChannelCandidateForResponse(candidate, ownerAddress)
|
|
2123
|
-
),
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
if (candidates.length === 1) {
|
|
2190
|
+
if (candidates.length > 0) {
|
|
2128
2191
|
const candidate = candidates[0]
|
|
2129
2192
|
if (candidate.local) {
|
|
2130
2193
|
const existing = this.#channels.find(
|
|
2131
2194
|
channel => channel.channelKey === candidate.channelKey
|
|
2132
2195
|
)
|
|
2133
|
-
if (existing
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2196
|
+
if (existing) {
|
|
2197
|
+
const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
|
|
2198
|
+
existing,
|
|
2199
|
+
candidate.writerCoreKeys
|
|
2200
|
+
)
|
|
2201
|
+
const memberChanged = this.#upsertChannelMember(existing, options)
|
|
2202
|
+
if (writerKeysChanged || memberChanged) {
|
|
2203
|
+
existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
|
|
2204
|
+
this.#saveChannelsMetadata()
|
|
2205
|
+
this.#appendUserSyncChannelUpsertSoon(existing, ownerAddress)
|
|
2206
|
+
this.#broadcastChannelHello()
|
|
2207
|
+
}
|
|
2208
|
+
return this.#formatChannelForResponse(existing, ownerAddress)
|
|
2137
2209
|
}
|
|
2138
|
-
if (existing) return this.#formatChannelForResponse(existing, ownerAddress)
|
|
2139
2210
|
const joined = await this.#joinChannelFromCandidate(
|
|
2140
2211
|
candidate,
|
|
2141
2212
|
channelType,
|
|
@@ -2188,25 +2259,32 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2188
2259
|
? { channelKey: String(candidateInput), channelId }
|
|
2189
2260
|
: null
|
|
2190
2261
|
|
|
2191
|
-
if (!candidate?.channelKey
|
|
2262
|
+
if (!candidate?.channelKey) {
|
|
2192
2263
|
return this.createChannel(channelId, options.type || 'group', options)
|
|
2193
2264
|
}
|
|
2194
2265
|
|
|
2195
|
-
const channelKey =
|
|
2196
|
-
normalizeChannelKey(candidate.channelKey) ||
|
|
2197
|
-
buildChannelKey(channelId, String(candidate.fingerprint || '').trim())
|
|
2266
|
+
const channelKey = buildChannelKey(channelId)
|
|
2198
2267
|
const existing = this.#channels.find(c => c.channelKey === channelKey)
|
|
2199
2268
|
if (existing) {
|
|
2200
|
-
await this.#mergeChannelWriterCoreKeys(
|
|
2201
|
-
|
|
2269
|
+
const writerKeysChanged = await this.#mergeChannelWriterCoreKeys(
|
|
2270
|
+
existing,
|
|
2271
|
+
candidate.writerCoreKeys
|
|
2272
|
+
)
|
|
2273
|
+
const memberChanged = this.#upsertChannelMember(existing, options)
|
|
2274
|
+
if (writerKeysChanged || memberChanged) {
|
|
2202
2275
|
existing.syncUpdatedAt = getNextSyncTimestamp(existing.syncUpdatedAt)
|
|
2203
2276
|
this.#saveChannelsMetadata()
|
|
2204
2277
|
this.#appendUserSyncChannelUpsertSoon(existing, options.ownerAddress)
|
|
2278
|
+
this.#broadcastChannelHello()
|
|
2205
2279
|
}
|
|
2206
2280
|
return this.#formatChannelForResponse(existing, options.ownerAddress)
|
|
2207
2281
|
}
|
|
2208
2282
|
|
|
2209
|
-
const cached =
|
|
2283
|
+
const cached =
|
|
2284
|
+
this.#getCachedChannelCandidate(
|
|
2285
|
+
channelId,
|
|
2286
|
+
normalizeChannelKey(candidate.channelKey)
|
|
2287
|
+
) || this.#getCachedChannelCandidate(channelId, channelKey)
|
|
2210
2288
|
const joined = await this.#joinChannelFromCandidate(cached || candidate, 'group', {
|
|
2211
2289
|
...options,
|
|
2212
2290
|
channelKey,
|
|
@@ -2379,14 +2457,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2379
2457
|
}
|
|
2380
2458
|
|
|
2381
2459
|
/**
|
|
2382
|
-
*
|
|
2460
|
+
* 列出频道;默认排除带点号的系统频道。
|
|
2383
2461
|
* @returns {Array<{ channelId: string, channelKey: string, name: string, createdAt: string, lastMessageAt: string, type: string, peerCount: number, remark: string, pinned: boolean }>}
|
|
2384
2462
|
*/
|
|
2385
2463
|
listChannels(options = {}) {
|
|
2386
2464
|
this.#ensureInitialized()
|
|
2387
2465
|
const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
|
|
2388
2466
|
const type = String(options.type || '').trim()
|
|
2389
|
-
const excludeType = String(options.excludeType || '').trim()
|
|
2390
2467
|
|
|
2391
2468
|
return this.#channels
|
|
2392
2469
|
.filter(c => {
|
|
@@ -2395,8 +2472,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2395
2472
|
})
|
|
2396
2473
|
.filter(c => {
|
|
2397
2474
|
if (type) return c.type === type
|
|
2398
|
-
|
|
2399
|
-
return true
|
|
2475
|
+
return !isSpecialChannel(c)
|
|
2400
2476
|
})
|
|
2401
2477
|
.map(c => this.#formatChannelForResponse(c, ownerAddress))
|
|
2402
2478
|
}
|
|
@@ -2506,7 +2582,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2506
2582
|
this.#upsertChannelMember(channel, {
|
|
2507
2583
|
ownerAddress: options.ownerAddress,
|
|
2508
2584
|
displayName: authorName,
|
|
2509
|
-
|
|
2585
|
+
...(Object.prototype.hasOwnProperty.call(options, 'avatar')
|
|
2586
|
+
? { avatar: options.avatar }
|
|
2587
|
+
: {}),
|
|
2510
2588
|
})
|
|
2511
2589
|
) {
|
|
2512
2590
|
this.#saveChannelsMetadata()
|
|
@@ -2517,7 +2595,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2517
2595
|
author,
|
|
2518
2596
|
authorName,
|
|
2519
2597
|
content: trimmed,
|
|
2520
|
-
timestamp:
|
|
2598
|
+
timestamp: await this.#getNextChannelMessageTimestamp(
|
|
2599
|
+
channel.channelKey
|
|
2600
|
+
),
|
|
2521
2601
|
}
|
|
2522
2602
|
if (attachment) {
|
|
2523
2603
|
message.attachment = attachment
|
|
@@ -2597,21 +2677,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2597
2677
|
let channel = this.#channels.find(c => c.channelKey === value)
|
|
2598
2678
|
if (channel) return channel
|
|
2599
2679
|
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
: matches
|
|
2604
|
-
if (visibleMatches.length === 1) return visibleMatches[0]
|
|
2605
|
-
if (visibleMatches.length > 1) {
|
|
2606
|
-
throw new Error('频道 ID 存在多个候选,请使用 channelKey')
|
|
2680
|
+
channel = this.#channels.find(c => c.channelId === value)
|
|
2681
|
+
if (channel && (!owner || this.#channelHasMember(channel, owner))) {
|
|
2682
|
+
return channel
|
|
2607
2683
|
}
|
|
2608
2684
|
throw new Error('频道不存在')
|
|
2609
2685
|
}
|
|
2610
2686
|
|
|
2611
2687
|
async #createLocalChannel(channelId, type = 'personal', options = {}) {
|
|
2612
|
-
const
|
|
2613
|
-
String(options.fingerprint || '').trim() || createChannelFingerprint()
|
|
2614
|
-
const channelKey = buildChannelKey(channelId, fingerprint)
|
|
2688
|
+
const channelKey = buildChannelKey(channelId)
|
|
2615
2689
|
const writerId = String(options.writerId || '').trim() || createChannelWriterId()
|
|
2616
2690
|
const ns = this.#store.namespace(`channel-${channelKey}`)
|
|
2617
2691
|
const localCore = ns.get({
|
|
@@ -2626,7 +2700,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2626
2700
|
])
|
|
2627
2701
|
const channelInfo = {
|
|
2628
2702
|
channelId,
|
|
2629
|
-
fingerprint,
|
|
2630
2703
|
channelKey,
|
|
2631
2704
|
name: channelId,
|
|
2632
2705
|
type: String(type || 'personal').trim() || 'personal',
|
|
@@ -2651,6 +2724,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2651
2724
|
await this.#joinChannelDiscoveryTopics(channelInfo)
|
|
2652
2725
|
this.#cacheChannelCandidate(this.#channelToCandidate(channelInfo, true))
|
|
2653
2726
|
this.#saveChannelsMetadata()
|
|
2727
|
+
this.#broadcastChannelHello()
|
|
2654
2728
|
return channelInfo
|
|
2655
2729
|
}
|
|
2656
2730
|
|
|
@@ -2658,24 +2732,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2658
2732
|
const channelId = normalizeChannelId(
|
|
2659
2733
|
candidateInput.channelId || options.channelId
|
|
2660
2734
|
)
|
|
2661
|
-
const channelKey =
|
|
2662
|
-
|
|
2663
|
-
String(candidateInput.fingerprint || '').trim() ||
|
|
2664
|
-
getChannelFingerprintFromKey(channelId, channelKey)
|
|
2665
|
-
if (!channelId || !fingerprint) {
|
|
2735
|
+
const channelKey = buildChannelKey(channelId)
|
|
2736
|
+
if (!channelId || !channelKey) {
|
|
2666
2737
|
throw new Error('频道候选缺少身份信息')
|
|
2667
2738
|
}
|
|
2668
|
-
const expectedChannelKey = buildChannelKey(channelId, fingerprint)
|
|
2669
|
-
if (channelKey && channelKey !== expectedChannelKey) {
|
|
2670
|
-
throw new Error('频道候选身份格式不匹配')
|
|
2671
|
-
}
|
|
2672
2739
|
|
|
2673
2740
|
const existing = this.#channels.find(
|
|
2674
|
-
channel => channel.channelKey ===
|
|
2741
|
+
channel => channel.channelKey === channelKey
|
|
2675
2742
|
)
|
|
2676
2743
|
if (existing) {
|
|
2677
2744
|
if (this.#upsertChannelMember(existing, options)) {
|
|
2678
2745
|
this.#saveChannelsMetadata()
|
|
2746
|
+
this.#broadcastChannelHello()
|
|
2679
2747
|
}
|
|
2680
2748
|
return this.#formatChannelForResponse(existing, options.ownerAddress)
|
|
2681
2749
|
}
|
|
@@ -2691,7 +2759,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2691
2759
|
const channelInfo = await this.#createLocalChannel(channelId, candidateInput.type || type, {
|
|
2692
2760
|
...options,
|
|
2693
2761
|
ownerAddress,
|
|
2694
|
-
fingerprint,
|
|
2695
2762
|
createdAt: candidateInput.createdAt,
|
|
2696
2763
|
lastMessageAt: candidateInput.lastMessageAt,
|
|
2697
2764
|
writerCoreKeys: candidateInput.writerCoreKeys,
|
|
@@ -2805,7 +2872,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2805
2872
|
}
|
|
2806
2873
|
|
|
2807
2874
|
async #discoverChannelCandidates(channelId, options = {}) {
|
|
2808
|
-
|
|
2875
|
+
const getCachedCandidates = () => {
|
|
2876
|
+
const now = Date.now()
|
|
2877
|
+
return [
|
|
2878
|
+
...(this.#channelCandidateCache.get(channelId)?.values() || []),
|
|
2879
|
+
].filter(
|
|
2880
|
+
candidate =>
|
|
2881
|
+
candidate.local ||
|
|
2882
|
+
!candidate.lastSeen ||
|
|
2883
|
+
now - candidate.lastSeen <= CHANNEL_CANDIDATE_TTL
|
|
2884
|
+
)
|
|
2885
|
+
}
|
|
2886
|
+
if (this.#options.disableNetwork) return getCachedCandidates()
|
|
2809
2887
|
const timeout =
|
|
2810
2888
|
Number(options.timeout) >= 0
|
|
2811
2889
|
? Number(options.timeout)
|
|
@@ -2819,15 +2897,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2819
2897
|
this.#channelIdDiscoveries.set(channelId, discovery)
|
|
2820
2898
|
}
|
|
2821
2899
|
await sleep(timeout)
|
|
2822
|
-
const
|
|
2823
|
-
const candidates = [
|
|
2824
|
-
...(this.#channelCandidateCache.get(channelId)?.values() || []),
|
|
2825
|
-
].filter(
|
|
2826
|
-
candidate =>
|
|
2827
|
-
candidate.local ||
|
|
2828
|
-
!candidate.lastSeen ||
|
|
2829
|
-
now - candidate.lastSeen <= CHANNEL_CANDIDATE_TTL
|
|
2830
|
-
)
|
|
2900
|
+
const candidates = getCachedCandidates()
|
|
2831
2901
|
if (!hadDiscovery && !this.#channels.some(c => c.channelId === channelId)) {
|
|
2832
2902
|
this.#channelIdDiscoveries.delete(channelId)
|
|
2833
2903
|
this.#chatSwarm
|
|
@@ -2846,7 +2916,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2846
2916
|
byKey.set(candidate.channelKey, {
|
|
2847
2917
|
...candidate,
|
|
2848
2918
|
writerCoreKeys: uniqueStrings(candidate.writerCoreKeys),
|
|
2849
|
-
onlineCount: Number(candidate.onlineCount) || (candidate.local ? 0 : 1),
|
|
2850
2919
|
})
|
|
2851
2920
|
continue
|
|
2852
2921
|
}
|
|
@@ -2858,9 +2927,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2858
2927
|
...existing.writerCoreKeys,
|
|
2859
2928
|
...(candidate.writerCoreKeys || []),
|
|
2860
2929
|
]),
|
|
2861
|
-
onlineCount:
|
|
2862
|
-
Math.max(Number(existing.onlineCount) || 0, 0) +
|
|
2863
|
-
(candidate.local ? 0 : 1),
|
|
2864
2930
|
})
|
|
2865
2931
|
}
|
|
2866
2932
|
return [...byKey.values()]
|
|
@@ -2869,66 +2935,47 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2869
2935
|
#channelToCandidate(channel, local = false) {
|
|
2870
2936
|
return {
|
|
2871
2937
|
channelId: channel.channelId,
|
|
2872
|
-
fingerprint: channel.fingerprint,
|
|
2873
2938
|
channelKey: channel.channelKey,
|
|
2874
2939
|
type: channel.type,
|
|
2875
2940
|
createdAt: channel.createdAt,
|
|
2876
2941
|
lastMessageAt: channel.lastMessageAt || '',
|
|
2877
2942
|
writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
|
|
2878
2943
|
local,
|
|
2879
|
-
onlineCount: local ? 0 : 1,
|
|
2880
2944
|
}
|
|
2881
2945
|
}
|
|
2882
2946
|
|
|
2883
2947
|
#cacheChannelCandidate(candidate) {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
cache.
|
|
2948
|
+
const channelId = normalizeChannelId(candidate?.channelId)
|
|
2949
|
+
const channelKey = buildChannelKey(channelId)
|
|
2950
|
+
if (!channelId || !channelKey) return
|
|
2951
|
+
if (!this.#channelCandidateCache.has(channelId)) {
|
|
2952
|
+
this.#channelCandidateCache.set(channelId, new Map())
|
|
2953
|
+
}
|
|
2954
|
+
const cache = this.#channelCandidateCache.get(channelId)
|
|
2955
|
+
const existing = cache.get(channelKey)
|
|
2956
|
+
cache.set(channelKey, {
|
|
2891
2957
|
...existing,
|
|
2892
2958
|
...candidate,
|
|
2959
|
+
channelId,
|
|
2960
|
+
channelKey,
|
|
2893
2961
|
writerCoreKeys: uniqueStrings([
|
|
2894
2962
|
...(existing?.writerCoreKeys || []),
|
|
2895
2963
|
...(candidate.writerCoreKeys || []),
|
|
2896
2964
|
]),
|
|
2897
|
-
onlineCount: Math.max(Number(existing?.onlineCount) || 0, 0) + 1,
|
|
2898
2965
|
lastSeen: Date.now(),
|
|
2899
2966
|
})
|
|
2900
2967
|
}
|
|
2901
2968
|
|
|
2902
2969
|
#getCachedChannelCandidate(channelId, channelKey) {
|
|
2903
|
-
const
|
|
2970
|
+
const normalizedChannelId = normalizeChannelId(channelId)
|
|
2971
|
+
const normalizedChannelKey = buildChannelKey(normalizedChannelId)
|
|
2972
|
+
const cache = this.#channelCandidateCache.get(normalizedChannelId)
|
|
2973
|
+
const candidate = cache?.get(channelKey) || cache?.get(normalizedChannelKey)
|
|
2904
2974
|
if (candidate) return candidate
|
|
2905
|
-
const local = this.#channels.find(
|
|
2906
|
-
|
|
2907
|
-
}
|
|
2908
|
-
|
|
2909
|
-
#formatChannelCandidateForResponse(candidate, ownerAddress = '') {
|
|
2910
|
-
const owner = normalizeOwnerAddress(ownerAddress)
|
|
2911
|
-
const localChannel = this.#channels.find(
|
|
2912
|
-
channel => channel.channelKey === candidate.channelKey
|
|
2975
|
+
const local = this.#channels.find(
|
|
2976
|
+
channel => channel.channelKey === normalizedChannelKey
|
|
2913
2977
|
)
|
|
2914
|
-
|
|
2915
|
-
localChannel && owner
|
|
2916
|
-
? localChannel.remarks?.[owner] || ''
|
|
2917
|
-
: candidate.local
|
|
2918
|
-
? ''
|
|
2919
|
-
: `${candidate.channelId}-网络`
|
|
2920
|
-
return {
|
|
2921
|
-
channelId: candidate.channelId,
|
|
2922
|
-
fingerprint: candidate.fingerprint,
|
|
2923
|
-
channelKey: candidate.channelKey,
|
|
2924
|
-
name: candidate.channelId,
|
|
2925
|
-
type: candidate.type || 'public',
|
|
2926
|
-
createdAt: candidate.createdAt || '',
|
|
2927
|
-
lastMessageAt: candidate.lastMessageAt || '',
|
|
2928
|
-
remark,
|
|
2929
|
-
local: Boolean(candidate.local),
|
|
2930
|
-
onlineCount: Number(candidate.onlineCount) || 0,
|
|
2931
|
-
}
|
|
2978
|
+
return local ? this.#channelToCandidate(local, true) : null
|
|
2932
2979
|
}
|
|
2933
2980
|
|
|
2934
2981
|
#formatChannelForResponse(channel, ownerAddress = '') {
|
|
@@ -2936,7 +2983,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2936
2983
|
return {
|
|
2937
2984
|
name: channel.channelId,
|
|
2938
2985
|
channelId: channel.channelId,
|
|
2939
|
-
fingerprint: channel.fingerprint,
|
|
2940
2986
|
channelKey: channel.channelKey,
|
|
2941
2987
|
key: channel.channelKey,
|
|
2942
2988
|
coreKey: channel.localWriterCoreKey,
|
|
@@ -2999,9 +3045,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2999
3045
|
existing.displayName = displayName
|
|
3000
3046
|
changed = true
|
|
3001
3047
|
}
|
|
3002
|
-
if (
|
|
3003
|
-
|
|
3004
|
-
|
|
3048
|
+
if (Object.prototype.hasOwnProperty.call(options, 'avatar')) {
|
|
3049
|
+
const currentAvatar = normalizeChannelAvatar(existing.avatar)
|
|
3050
|
+
if (currentAvatar !== avatar) {
|
|
3051
|
+
if (avatar) {
|
|
3052
|
+
existing.avatar = avatar
|
|
3053
|
+
} else {
|
|
3054
|
+
delete existing.avatar
|
|
3055
|
+
}
|
|
3056
|
+
changed = true
|
|
3057
|
+
}
|
|
3005
3058
|
}
|
|
3006
3059
|
if (!existing.joinedAt) {
|
|
3007
3060
|
existing.joinedAt = new Date().toISOString()
|
|
@@ -3046,6 +3099,31 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3046
3099
|
)
|
|
3047
3100
|
}
|
|
3048
3101
|
|
|
3102
|
+
async #getNextChannelMessageTimestamp(channelKey) {
|
|
3103
|
+
const coresMap = this.#channelCores.get(channelKey)
|
|
3104
|
+
let maxTimestamp = 0
|
|
3105
|
+
|
|
3106
|
+
if (coresMap) {
|
|
3107
|
+
for (const [, core] of coresMap) {
|
|
3108
|
+
for (let i = 0; i < core.length; i++) {
|
|
3109
|
+
try {
|
|
3110
|
+
const entry = await core.get(i)
|
|
3111
|
+
if (entry?.type === 'message') {
|
|
3112
|
+
maxTimestamp = Math.max(
|
|
3113
|
+
maxTimestamp,
|
|
3114
|
+
Number(entry.timestamp) || 0
|
|
3115
|
+
)
|
|
3116
|
+
}
|
|
3117
|
+
} catch {
|
|
3118
|
+
break
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
return Math.max(Date.now(), maxTimestamp + 1)
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3049
3127
|
#normalizeChannelMessageForResponse(channelKey, message) {
|
|
3050
3128
|
const channel = this.#channels.find(item => item.channelKey === channelKey)
|
|
3051
3129
|
const authorAddress = normalizeOwnerAddress(message?.author)
|
|
@@ -3054,10 +3132,23 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3054
3132
|
item => normalizeOwnerAddress(item?.address) === authorAddress
|
|
3055
3133
|
)
|
|
3056
3134
|
: null
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3135
|
+
let baseMessage = message
|
|
3136
|
+
if (member) {
|
|
3137
|
+
const displayName = normalizeChannelDisplayName(
|
|
3138
|
+
member.displayName,
|
|
3139
|
+
authorAddress
|
|
3140
|
+
)
|
|
3141
|
+
const avatar = normalizeChannelAvatar(member.avatar)
|
|
3142
|
+
if (displayName && baseMessage?.authorName !== displayName) {
|
|
3143
|
+
baseMessage = { ...baseMessage, authorName: displayName }
|
|
3144
|
+
}
|
|
3145
|
+
if (avatar && baseMessage?.avatar !== avatar) {
|
|
3146
|
+
baseMessage = { ...baseMessage, avatar }
|
|
3147
|
+
} else if (!avatar && baseMessage?.avatar) {
|
|
3148
|
+
baseMessage = { ...baseMessage }
|
|
3149
|
+
delete baseMessage.avatar
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3061
3152
|
const attachment = baseMessage?.attachment
|
|
3062
3153
|
if (!attachment?.cid || !attachment.fileName) {
|
|
3063
3154
|
return baseMessage
|
|
@@ -3344,8 +3435,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3344
3435
|
.filter(channel => this.#channelHasMember(channel, ownerAddress))
|
|
3345
3436
|
.map(channel => this.#formatChannelForSync(channel, ownerAddress))
|
|
3346
3437
|
.filter(Boolean)
|
|
3438
|
+
const profile = this.#formatUserProfileForSync(ownerAddress)
|
|
3347
3439
|
return this.#appendUserSyncOp(ownerAddress, 'snapshot', {
|
|
3348
3440
|
reason,
|
|
3441
|
+
profile,
|
|
3349
3442
|
files,
|
|
3350
3443
|
trashFiles,
|
|
3351
3444
|
channels,
|
|
@@ -3364,8 +3457,16 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3364
3457
|
async #applyUserSyncOp(session, op) {
|
|
3365
3458
|
let changedFiles = false
|
|
3366
3459
|
let changedChannels = false
|
|
3460
|
+
let changedProfile = false
|
|
3367
3461
|
if (op.kind === 'snapshot') {
|
|
3368
3462
|
const payload = op.payload || {}
|
|
3463
|
+
const profileResult = this.#applyUserSyncProfileRecord(
|
|
3464
|
+
session.ownerAddress,
|
|
3465
|
+
payload.profile,
|
|
3466
|
+
getSyncTimestamp(payload.profile?.syncUpdatedAt, op.timestamp)
|
|
3467
|
+
)
|
|
3468
|
+
changedProfile = profileResult.changedProfile || changedProfile
|
|
3469
|
+
changedChannels = profileResult.changedChannels || changedChannels
|
|
3369
3470
|
for (const file of Array.isArray(payload.files) ? payload.files : []) {
|
|
3370
3471
|
changedFiles =
|
|
3371
3472
|
this.#applyUserSyncFileRecord(
|
|
@@ -3424,8 +3525,22 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3424
3525
|
op.payload?.channelKey,
|
|
3425
3526
|
getSyncTimestamp(op.payload?.syncUpdatedAt, op.timestamp)
|
|
3426
3527
|
)
|
|
3528
|
+
} else if (op.kind === 'profile:upsert') {
|
|
3529
|
+
const profileResult = this.#applyUserSyncProfileRecord(
|
|
3530
|
+
session.ownerAddress,
|
|
3531
|
+
op.payload?.profile,
|
|
3532
|
+
getSyncTimestamp(op.payload?.profile?.syncUpdatedAt, op.timestamp)
|
|
3533
|
+
)
|
|
3534
|
+
changedProfile = profileResult.changedProfile
|
|
3535
|
+
changedChannels = changedChannels || profileResult.changedChannels
|
|
3427
3536
|
}
|
|
3428
3537
|
|
|
3538
|
+
if (changedProfile) {
|
|
3539
|
+
this.emit('user:metadata:updated', {
|
|
3540
|
+
ownerAddress: session.ownerAddress,
|
|
3541
|
+
scope: 'profile',
|
|
3542
|
+
})
|
|
3543
|
+
}
|
|
3429
3544
|
if (changedFiles) {
|
|
3430
3545
|
this.#savePublishedMetadata()
|
|
3431
3546
|
this.#saveTrashMetadata()
|
|
@@ -3444,7 +3559,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3444
3559
|
if (changedFiles || changedChannels) {
|
|
3445
3560
|
this.#touchUserSyncSession(session)
|
|
3446
3561
|
}
|
|
3447
|
-
|
|
3562
|
+
if (changedProfile) {
|
|
3563
|
+
this.#touchUserSyncSession(session)
|
|
3564
|
+
}
|
|
3565
|
+
return changedFiles || changedChannels || changedProfile
|
|
3448
3566
|
}
|
|
3449
3567
|
|
|
3450
3568
|
async #mergeUserSyncWriterCoreKeys(session, writerCoreKeys = []) {
|
|
@@ -3523,7 +3641,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3523
3641
|
}
|
|
3524
3642
|
return {
|
|
3525
3643
|
channelId: channel.channelId,
|
|
3526
|
-
fingerprint: channel.fingerprint,
|
|
3527
3644
|
channelKey: channel.channelKey,
|
|
3528
3645
|
type: channel.type,
|
|
3529
3646
|
createdAt: channel.createdAt,
|
|
@@ -3540,6 +3657,70 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3540
3657
|
}
|
|
3541
3658
|
}
|
|
3542
3659
|
|
|
3660
|
+
#formatUserProfileForSync(ownerAddress) {
|
|
3661
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3662
|
+
if (!owner) return null
|
|
3663
|
+
const profile = this.#userSyncMetadata.profiles?.[owner]
|
|
3664
|
+
return profile ? { ...profile } : null
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
#normalizeUserProfileRecord(ownerAddress, record, timestamp = Date.now()) {
|
|
3668
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3669
|
+
if (!owner || !record || typeof record !== 'object') return null
|
|
3670
|
+
const displayName = normalizeChannelDisplayName(record.displayName, owner)
|
|
3671
|
+
const avatar = normalizeChannelAvatar(record.avatar)
|
|
3672
|
+
const syncUpdatedAt = getSyncTimestamp(record.syncUpdatedAt, timestamp)
|
|
3673
|
+
return {
|
|
3674
|
+
displayName,
|
|
3675
|
+
avatar,
|
|
3676
|
+
syncUpdatedAt,
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
#applyUserSyncProfileRecord(ownerAddress, record, timestamp) {
|
|
3681
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3682
|
+
const profile = this.#normalizeUserProfileRecord(owner, record, timestamp)
|
|
3683
|
+
if (!profile) {
|
|
3684
|
+
return { changedProfile: false, changedChannels: false }
|
|
3685
|
+
}
|
|
3686
|
+
if (!this.#shouldApplyUserSyncEntity(owner, 'profile', profile.syncUpdatedAt)) {
|
|
3687
|
+
return { changedProfile: false, changedChannels: false }
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
const existing = this.#userSyncMetadata.profiles?.[owner]
|
|
3691
|
+
this.#userSyncMetadata.profiles = this.#userSyncMetadata.profiles || {}
|
|
3692
|
+
this.#userSyncMetadata.profiles[owner] = profile
|
|
3693
|
+
this.#setUserSyncClock(owner, 'profile', profile.syncUpdatedAt)
|
|
3694
|
+
const changedChannels = this.#applyUserProfileToJoinedChannels(
|
|
3695
|
+
owner,
|
|
3696
|
+
profile
|
|
3697
|
+
)
|
|
3698
|
+
return {
|
|
3699
|
+
changedProfile:
|
|
3700
|
+
!existing ||
|
|
3701
|
+
existing.displayName !== profile.displayName ||
|
|
3702
|
+
existing.avatar !== profile.avatar ||
|
|
3703
|
+
Number(existing.syncUpdatedAt) !== profile.syncUpdatedAt,
|
|
3704
|
+
changedChannels,
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
#applyUserProfileToJoinedChannels(ownerAddress, profile) {
|
|
3709
|
+
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3710
|
+
if (!owner || !profile) return false
|
|
3711
|
+
let changed = false
|
|
3712
|
+
for (const channel of this.#channels) {
|
|
3713
|
+
if (!this.#channelHasMember(channel, owner)) continue
|
|
3714
|
+
changed =
|
|
3715
|
+
this.#upsertChannelMember(channel, {
|
|
3716
|
+
ownerAddress: owner,
|
|
3717
|
+
displayName: profile.displayName,
|
|
3718
|
+
avatar: profile.avatar,
|
|
3719
|
+
}) || changed
|
|
3720
|
+
}
|
|
3721
|
+
return changed
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3543
3724
|
#appendUserSyncChannelUpsertSoon(channel, ownerAddress) {
|
|
3544
3725
|
const owner = normalizeOwnerAddress(ownerAddress)
|
|
3545
3726
|
const record = this.#formatChannelForSync(channel, owner)
|
|
@@ -3701,15 +3882,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3701
3882
|
async #applyUserSyncChannelRecord(ownerAddress, record, timestamp) {
|
|
3702
3883
|
if (!record || typeof record !== 'object') return false
|
|
3703
3884
|
const channelId = normalizeChannelId(record.channelId)
|
|
3704
|
-
const
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
const recordChannelKey = normalizeChannelKey(record.channelKey)
|
|
3708
|
-
if (recordChannelKey && recordChannelKey !== expectedChannelKey) {
|
|
3709
|
-
return false
|
|
3710
|
-
}
|
|
3711
|
-
const channelKey = recordChannelKey || expectedChannelKey
|
|
3712
|
-
if (!channelId || !fingerprint || !channelKey) return false
|
|
3885
|
+
const expectedChannelKey = buildChannelKey(channelId)
|
|
3886
|
+
const channelKey = expectedChannelKey
|
|
3887
|
+
if (!channelId || !channelKey) return false
|
|
3713
3888
|
const syncUpdatedAt = getSyncTimestamp(record.syncUpdatedAt, timestamp)
|
|
3714
3889
|
const entityKey = `channel:${channelKey}`
|
|
3715
3890
|
if (!this.#shouldApplyUserSyncEntity(ownerAddress, entityKey, syncUpdatedAt)) {
|
|
@@ -3721,7 +3896,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3721
3896
|
if (!channel) {
|
|
3722
3897
|
channel = {
|
|
3723
3898
|
channelId,
|
|
3724
|
-
fingerprint,
|
|
3725
3899
|
channelKey,
|
|
3726
3900
|
name: channelId,
|
|
3727
3901
|
createdAt:
|
|
@@ -3886,6 +4060,12 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3886
4060
|
if (channelKey) {
|
|
3887
4061
|
this.#setUserSyncClock(ownerAddress, `channel:${channelKey}`, timestamp)
|
|
3888
4062
|
}
|
|
4063
|
+
} else if (op.kind === 'profile:upsert') {
|
|
4064
|
+
const timestamp = getSyncTimestamp(
|
|
4065
|
+
op.payload?.profile?.syncUpdatedAt,
|
|
4066
|
+
op.timestamp
|
|
4067
|
+
)
|
|
4068
|
+
this.#setUserSyncClock(ownerAddress, 'profile', timestamp)
|
|
3889
4069
|
}
|
|
3890
4070
|
}
|
|
3891
4071
|
|
|
@@ -4626,7 +4806,18 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4626
4806
|
}
|
|
4627
4807
|
}
|
|
4628
4808
|
|
|
4629
|
-
|
|
4809
|
+
const profiles = {}
|
|
4810
|
+
for (const [owner, profile] of Object.entries(parsed.profiles || {})) {
|
|
4811
|
+
const ownerAddress = normalizeOwnerAddress(owner)
|
|
4812
|
+
const normalized = this.#normalizeUserProfileRecord(
|
|
4813
|
+
ownerAddress,
|
|
4814
|
+
profile,
|
|
4815
|
+
profile?.syncUpdatedAt
|
|
4816
|
+
)
|
|
4817
|
+
if (normalized) profiles[ownerAddress] = normalized
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
return { sessions, clocks, profiles }
|
|
4630
4821
|
}
|
|
4631
4822
|
} catch (err) {
|
|
4632
4823
|
console.warn(
|
|
@@ -4634,7 +4825,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4634
4825
|
err.message
|
|
4635
4826
|
)
|
|
4636
4827
|
}
|
|
4637
|
-
return { sessions: {}, clocks: {} }
|
|
4828
|
+
return { sessions: {}, clocks: {}, profiles: {} }
|
|
4638
4829
|
}
|
|
4639
4830
|
|
|
4640
4831
|
#saveUserSyncMetadata() {
|
|
@@ -4668,16 +4859,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4668
4859
|
.filter(channel => channel && typeof channel === 'object')
|
|
4669
4860
|
.map(channel => {
|
|
4670
4861
|
const channelId = normalizeChannelId(channel.channelId)
|
|
4671
|
-
const fingerprint = String(channel.fingerprint || '').trim()
|
|
4672
|
-
const expectedChannelKey =
|
|
4673
|
-
channelId && fingerprint
|
|
4674
|
-
? buildChannelKey(channelId, fingerprint)
|
|
4675
|
-
: ''
|
|
4676
4862
|
const channelKey = normalizeChannelKey(channel.channelKey)
|
|
4863
|
+
const expectedChannelKey = buildChannelKey(channelId)
|
|
4677
4864
|
return {
|
|
4678
4865
|
...channel,
|
|
4679
4866
|
channelId,
|
|
4680
|
-
fingerprint,
|
|
4681
4867
|
channelKey,
|
|
4682
4868
|
expectedChannelKey,
|
|
4683
4869
|
name: channelId,
|
|
@@ -4687,7 +4873,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4687
4873
|
.filter(
|
|
4688
4874
|
channel =>
|
|
4689
4875
|
CHANNEL_NAME_REGEX.test(channel.channelId) &&
|
|
4690
|
-
channel.fingerprint &&
|
|
4691
4876
|
channel.channelKey === channel.expectedChannelKey &&
|
|
4692
4877
|
channel.writerId &&
|
|
4693
4878
|
channel.localWriterCoreKey
|
|
@@ -4710,7 +4895,6 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4710
4895
|
.filter(channel => !TRANSIENT_CHANNEL_TYPES.has(channel?.type))
|
|
4711
4896
|
.map(channel => ({
|
|
4712
4897
|
channelId: channel.channelId,
|
|
4713
|
-
fingerprint: channel.fingerprint,
|
|
4714
4898
|
channelKey: channel.channelKey,
|
|
4715
4899
|
name: channel.channelId,
|
|
4716
4900
|
type: channel.type,
|
|
@@ -4840,13 +5024,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4840
5024
|
}
|
|
4841
5025
|
}
|
|
4842
5026
|
|
|
4843
|
-
|
|
4844
|
-
const stream = conn
|
|
4845
|
-
let connectedPeerId = null
|
|
4846
|
-
|
|
5027
|
+
#buildChannelHelloMessage() {
|
|
4847
5028
|
const channels = this.#channels.map(channel => ({
|
|
4848
5029
|
channelId: channel.channelId,
|
|
4849
|
-
fingerprint: channel.fingerprint,
|
|
4850
5030
|
channelKey: channel.channelKey,
|
|
4851
5031
|
type: channel.type,
|
|
4852
5032
|
createdAt: channel.createdAt,
|
|
@@ -4865,122 +5045,157 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4865
5045
|
})
|
|
4866
5046
|
)
|
|
4867
5047
|
|
|
4868
|
-
|
|
5048
|
+
return {
|
|
4869
5049
|
type: 'channel-hello',
|
|
4870
5050
|
peerId: this.getNodeId(),
|
|
4871
5051
|
authorName: this.getNodeId().slice(0, 4),
|
|
4872
5052
|
channels,
|
|
4873
5053
|
userSyncSessions,
|
|
4874
|
-
}
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
4875
5056
|
|
|
5057
|
+
#sendChannelHello(stream) {
|
|
5058
|
+
if (!stream || stream.destroyed || stream.writableEnded) {
|
|
5059
|
+
this.#channelStreams.delete(stream)
|
|
5060
|
+
return false
|
|
5061
|
+
}
|
|
4876
5062
|
try {
|
|
4877
|
-
stream.write(
|
|
5063
|
+
stream.write(`${JSON.stringify(this.#buildChannelHelloMessage())}\n`)
|
|
5064
|
+
return true
|
|
4878
5065
|
} catch {
|
|
4879
|
-
|
|
5066
|
+
this.#channelStreams.delete(stream)
|
|
5067
|
+
return false
|
|
4880
5068
|
}
|
|
5069
|
+
}
|
|
4881
5070
|
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
const remoteChannels = Array.isArray(msg.channels)
|
|
4889
|
-
? msg.channels
|
|
4890
|
-
.filter(channel => channel && typeof channel === 'object')
|
|
4891
|
-
.map(channel => ({
|
|
4892
|
-
channelId: normalizeChannelId(channel.channelId),
|
|
4893
|
-
fingerprint: String(channel.fingerprint || '').trim(),
|
|
4894
|
-
channelKey: normalizeChannelKey(channel.channelKey),
|
|
4895
|
-
type: String(channel.type || 'public').trim() || 'public',
|
|
4896
|
-
createdAt:
|
|
4897
|
-
typeof channel.createdAt === 'string'
|
|
4898
|
-
? channel.createdAt
|
|
4899
|
-
: '',
|
|
4900
|
-
lastMessageAt:
|
|
4901
|
-
typeof channel.lastMessageAt === 'string'
|
|
4902
|
-
? channel.lastMessageAt
|
|
4903
|
-
: '',
|
|
4904
|
-
writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
|
|
4905
|
-
}))
|
|
4906
|
-
.filter(
|
|
4907
|
-
channel =>
|
|
4908
|
-
channel.channelId &&
|
|
4909
|
-
channel.fingerprint &&
|
|
4910
|
-
channel.channelKey
|
|
4911
|
-
)
|
|
4912
|
-
: []
|
|
4913
|
-
|
|
4914
|
-
for (const remoteChannel of remoteChannels) {
|
|
4915
|
-
this.#cacheChannelCandidate({
|
|
4916
|
-
...remoteChannel,
|
|
4917
|
-
local: false,
|
|
4918
|
-
peerId: msg.peerId,
|
|
4919
|
-
onlineCount: 1,
|
|
4920
|
-
})
|
|
5071
|
+
#broadcastChannelHello() {
|
|
5072
|
+
for (const stream of [...this.#channelStreams]) {
|
|
5073
|
+
this.#sendChannelHello(stream)
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
4921
5076
|
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
)
|
|
4925
|
-
if (!localChannel) continue
|
|
4926
|
-
|
|
4927
|
-
const peers = this.#channelPeers.get(localChannel.channelKey)
|
|
4928
|
-
if (peers) {
|
|
4929
|
-
peers.set(msg.peerId, {
|
|
4930
|
-
peerId: msg.peerId,
|
|
4931
|
-
authorName: msg.authorName,
|
|
4932
|
-
lastSeen: Date.now(),
|
|
4933
|
-
})
|
|
4934
|
-
}
|
|
5077
|
+
async #processChannelHelloMessage(msg) {
|
|
5078
|
+
if (msg.type !== 'channel-hello') return null
|
|
4935
5079
|
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
5080
|
+
const remoteChannels = Array.isArray(msg.channels)
|
|
5081
|
+
? msg.channels
|
|
5082
|
+
.filter(channel => channel && typeof channel === 'object')
|
|
5083
|
+
.map(channel => {
|
|
5084
|
+
const channelId = normalizeChannelId(channel.channelId)
|
|
5085
|
+
return {
|
|
5086
|
+
channelId,
|
|
5087
|
+
channelKey: buildChannelKey(channelId),
|
|
5088
|
+
type: String(channel.type || 'public').trim() || 'public',
|
|
5089
|
+
createdAt:
|
|
5090
|
+
typeof channel.createdAt === 'string' ? channel.createdAt : '',
|
|
5091
|
+
lastMessageAt:
|
|
5092
|
+
typeof channel.lastMessageAt === 'string'
|
|
5093
|
+
? channel.lastMessageAt
|
|
5094
|
+
: '',
|
|
5095
|
+
writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
|
|
4946
5096
|
}
|
|
4947
|
-
}
|
|
5097
|
+
})
|
|
5098
|
+
.filter(channel => channel.channelId && channel.channelKey)
|
|
5099
|
+
: []
|
|
4948
5100
|
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
4956
|
-
}))
|
|
4957
|
-
.filter(session => session.ownerAddress && session.syncId)
|
|
4958
|
-
: []
|
|
4959
|
-
|
|
4960
|
-
for (const remoteSession of remoteUserSyncSessions) {
|
|
4961
|
-
const localSession = this.#userSyncSessions.get(
|
|
4962
|
-
remoteSession.ownerAddress
|
|
4963
|
-
)
|
|
4964
|
-
if (!localSession || localSession.syncId !== remoteSession.syncId) {
|
|
4965
|
-
continue
|
|
4966
|
-
}
|
|
4967
|
-
await this.#mergeUserSyncWriterCoreKeys(
|
|
4968
|
-
localSession,
|
|
4969
|
-
remoteSession.writerCoreKeys
|
|
4970
|
-
)
|
|
4971
|
-
}
|
|
5101
|
+
for (const remoteChannel of remoteChannels) {
|
|
5102
|
+
this.#cacheChannelCandidate({
|
|
5103
|
+
...remoteChannel,
|
|
5104
|
+
local: false,
|
|
5105
|
+
peerId: msg.peerId,
|
|
5106
|
+
})
|
|
4972
5107
|
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5108
|
+
const localChannel = this.#channels.find(
|
|
5109
|
+
channel => channel.channelKey === remoteChannel.channelKey
|
|
5110
|
+
)
|
|
5111
|
+
if (!localChannel) continue
|
|
5112
|
+
|
|
5113
|
+
const peers = this.#channelPeers.get(localChannel.channelKey)
|
|
5114
|
+
if (peers) {
|
|
5115
|
+
peers.set(msg.peerId, {
|
|
5116
|
+
peerId: msg.peerId,
|
|
5117
|
+
authorName: msg.authorName,
|
|
5118
|
+
lastSeen: Date.now(),
|
|
5119
|
+
})
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
for (const writerCoreKey of remoteChannel.writerCoreKeys) {
|
|
5123
|
+
if (
|
|
5124
|
+
writerCoreKey &&
|
|
5125
|
+
writerCoreKey !== this.#channelLocalCoreKey.get(localChannel.channelKey)
|
|
5126
|
+
) {
|
|
5127
|
+
await this.#openRemoteChannelCore(
|
|
5128
|
+
localChannel.channelKey,
|
|
5129
|
+
writerCoreKey
|
|
5130
|
+
)
|
|
4977
5131
|
}
|
|
4978
|
-
} catch (err) {
|
|
4979
|
-
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|
|
4980
5132
|
}
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
const remoteUserSyncSessions = Array.isArray(msg.userSyncSessions)
|
|
5136
|
+
? msg.userSyncSessions
|
|
5137
|
+
.filter(session => session && typeof session === 'object')
|
|
5138
|
+
.map(session => ({
|
|
5139
|
+
ownerAddress: normalizeOwnerAddress(session.ownerAddress),
|
|
5140
|
+
syncId: String(session.syncId || '').trim(),
|
|
5141
|
+
writerCoreKeys: uniqueStrings(session.writerCoreKeys),
|
|
5142
|
+
}))
|
|
5143
|
+
.filter(session => session.ownerAddress && session.syncId)
|
|
5144
|
+
: []
|
|
5145
|
+
|
|
5146
|
+
for (const remoteSession of remoteUserSyncSessions) {
|
|
5147
|
+
const localSession = this.#userSyncSessions.get(
|
|
5148
|
+
remoteSession.ownerAddress
|
|
5149
|
+
)
|
|
5150
|
+
if (!localSession || localSession.syncId !== remoteSession.syncId) {
|
|
5151
|
+
continue
|
|
5152
|
+
}
|
|
5153
|
+
await this.#mergeUserSyncWriterCoreKeys(
|
|
5154
|
+
localSession,
|
|
5155
|
+
remoteSession.writerCoreKeys
|
|
5156
|
+
)
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
this.emit('channel:peer:online', {
|
|
5160
|
+
peerId: msg.peerId,
|
|
5161
|
+
authorName: msg.authorName,
|
|
4981
5162
|
})
|
|
4982
5163
|
|
|
4983
|
-
|
|
5164
|
+
return msg.peerId
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
async #handleChannelConnection(conn) {
|
|
5168
|
+
const stream = conn
|
|
5169
|
+
let connectedPeerId = null
|
|
5170
|
+
let readBuffer = ''
|
|
5171
|
+
let closed = false
|
|
5172
|
+
|
|
5173
|
+
this.#channelStreams.add(stream)
|
|
5174
|
+
if (!this.#sendChannelHello(stream)) return
|
|
5175
|
+
|
|
5176
|
+
stream.on('data', async data => {
|
|
5177
|
+
readBuffer += data.toString()
|
|
5178
|
+
let newlineIndex = readBuffer.indexOf('\n')
|
|
5179
|
+
while (newlineIndex !== -1) {
|
|
5180
|
+
const line = readBuffer.slice(0, newlineIndex).trim()
|
|
5181
|
+
readBuffer = readBuffer.slice(newlineIndex + 1)
|
|
5182
|
+
newlineIndex = readBuffer.indexOf('\n')
|
|
5183
|
+
if (!line) continue
|
|
5184
|
+
try {
|
|
5185
|
+
const peerId = await this.#processChannelHelloMessage(
|
|
5186
|
+
JSON.parse(line)
|
|
5187
|
+
)
|
|
5188
|
+
if (peerId) connectedPeerId = peerId
|
|
5189
|
+
} catch (err) {
|
|
5190
|
+
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
})
|
|
5194
|
+
|
|
5195
|
+
const cleanup = () => {
|
|
5196
|
+
if (closed) return
|
|
5197
|
+
closed = true
|
|
5198
|
+
this.#channelStreams.delete(stream)
|
|
4984
5199
|
if (connectedPeerId) {
|
|
4985
5200
|
for (const [, peers] of this.#channelPeers) {
|
|
4986
5201
|
if (peers.has(connectedPeerId)) {
|
|
@@ -4993,7 +5208,10 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4993
5208
|
}
|
|
4994
5209
|
}
|
|
4995
5210
|
}
|
|
4996
|
-
}
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
stream.on('close', cleanup)
|
|
5214
|
+
stream.on('error', cleanup)
|
|
4997
5215
|
}
|
|
4998
5216
|
|
|
4999
5217
|
/**
|