| … | … | @@ -1,920 +0,0 @@ |
| 1 | | export class Router { |
| 2 | | constructor (backend, core) { |
| 3 | | this.backend = backend |
| 4 | | this.core = core |
| 5 | | this._startTime = Date.now() |
| 6 | | } |
| 7 | | |
| 8 | | async handle (method, params = {}) { |
| 9 | | try { |
| 10 | | const data = await this._dispatch(method, params) |
| 11 | | return { ok: true, data } |
| 12 | | } catch (err) { |
| 13 | | return { ok: false, error: err.message || String(err) } |
| 14 | | } |
| 15 | | } |
| 16 | | |
| 17 | | async _dispatch (method, params) { |
| 18 | | switch (method) { |
| 19 | | case 'identity': return this._identity() |
| 20 | | case 'identity.recovery': return this._identityRecovery() |
| 21 | | case 'identity.restore': return this._identityRestore(params) |
| 22 | | case 'identity.setName': return this._identitySetName(params) |
| 23 | | case 'rooms': return this._rooms() |
| 24 | | case 'room': return this._room(params) |
| 25 | | case 'room.create': return this._roomCreate(params) |
| 26 | | case 'room.join': return this._roomJoin(params) |
| 27 | | case 'room.invite': return this._roomInvite(params) |
| 28 | | case 'room.leave': return this._roomLeave(params) |
| 29 | | case 'send': return this._send(params) |
| 30 | | case 'read': return this._read(params) |
| 31 | | case 'members': return this._members(params) |
| 32 | | case 'members.role': return this._changeMemberRole(params) |
| 33 | | case 'dm.request': return this._dmRequest(params) |
| 34 | | case 'watch': return this._watch(params) |
| 35 | | case 'call.authorize': return this._callAuthorize(params) |
| 36 | | case 'call.deauthorize': return this._callDeauthorize(params) |
| 37 | | case 'call.join': return this._callJoinPeer(params) |
| 38 | | case 'call.leave': return this._callLeavePeer(params) |
| 39 | | case 'call.presence': return this._callPresence(params) |
| 40 | | case 'call.setPresent': return this._callSetPresent(params) |
| 41 | | case 'call.setNotPresent': return this._callSetNotPresent(params) |
| 42 | | case 'call.tracks': return this._callGetTracks(params) |
| 43 | | case 'call.requestTrack': return this._callRequestTrack(params) |
| 44 | | case 'call.unrequestTrack': return this._callUnrequestTrack(params) |
| 45 | | case 'call.shareTrack': return this._callShareTrack(params) |
| 46 | | case 'call.unshareTrack': return this._callUnshareTrack(params) |
| 47 | | case 'call.signal': return this._callSignalMedia(params) |
| 48 | | // call.raw removed — arbitrary function execution (SECURITY_AUDIT_V2 D-F3) |
| 49 | | case 'msg.edit': return this._editMessage(params) |
| 50 | | case 'msg.delete': return this._deleteMessage(params) |
| 51 | | case 'msg.pin': return this._pinMessage(params) |
| 52 | | case 'msg.unpin': return this._unpinMessage(params) |
| 53 | | case 'msg.pinned': return this._getPinned(params) |
| 54 | | case 'msg.search': return this._searchMessages(params) |
| 55 | | case 'react': return this._addReaction(params) |
| 56 | | case 'unreact': return this._removeReaction(params) |
| 57 | | case 'reactions': return this._getReactions(params) |
| 58 | | case 'mute': return this._muteMember(params) |
| 59 | | case 'unmute': return this._unmuteMember(params) |
| 60 | | case 'block': return this._blockMember(params) |
| 61 | | case 'unblock': return this._unblockMember(params) |
| 62 | | case 'kick': return this._removeMember(params) |
| 63 | | case 'blocked': return this._getBlocked(params) |
| 64 | | // raw removed — arbitrary function execution (SECURITY_AUDIT_V2 D-F3) |
| 65 | | case 'status': return this._status() |
| 66 | | case 'shutdown': return this._shutdown() |
| 67 | | |
| 68 | | // --- Introspection --- |
| 69 | | case 'introspect': return this._introspect(params) |
| 70 | | |
| 71 | | // --- Identity & Username --- |
| 72 | | case 'identity.confirm': return this._identityConfirm() |
| 73 | | case 'identity.store': return this._identityStore(params) |
| 74 | | case 'identity.register': return this._identityRegister(params) |
| 75 | | case 'identity.checkName': return this._identityCheckName(params) |
| 76 | | case 'identity.lookup': return this._identityLookup(params) |
| 77 | | case 'identity.lookupMember': return this._identityLookupMember(params) |
| 78 | | |
| 79 | | // --- Room Config & Management --- |
| 80 | | case 'room.config': return this._roomConfig(params) |
| 81 | | case 'room.config.set': return this._roomConfigSet(params) |
| 82 | | case 'room.bookmark': return this._roomBookmark(params) |
| 83 | | case 'room.unbookmark': return this._roomUnbookmark(params) |
| 84 | | case 'room.preferences': return this._roomPreferences(params) |
| 85 | | case 'room.preferences.set': return this._roomPreferencesSet(params) |
| 86 | | case 'room.search': return this._roomSearch(params) |
| 87 | | case 'room.unread': return this._roomUnread(params) |
| 88 | | |
| 89 | | // --- DM Management --- |
| 90 | | case 'dm.list': return this._dmList() |
| 91 | | case 'dm.requests': return this._dmRequests(params) |
| 92 | | case 'dm.accept': return this._dmAccept(params) |
| 93 | | case 'dm.reject': return this._dmReject(params) |
| 94 | | |
| 95 | | // --- File Management --- |
| 96 | | case 'file.send': return this._fileSend(params) |
| 97 | | case 'file.list': return this._fileList(params) |
| 98 | | case 'file.info': return this._fileInfo(params) |
| 99 | | case 'file.download': return this._fileDownload(params) |
| 100 | | case 'file.cancel': return this._fileCancel(params) |
| 101 | | case 'file.remove': return this._fileRemove(params) |
| 102 | | case 'file.clear': return this._fileClear(params) |
| 103 | | case 'file.fetch': return this._fileFetch(params) |
| 104 | | case 'file.preview': return this._filePreview(params) |
| 105 | | case 'file.subscribe': return { ok: true } |
| 106 | | case 'file.unsubscribe': return { ok: true } |
| 107 | | case 'file.path': return this._filePath(params) |
| 108 | | |
| 109 | | // --- Advanced Members --- |
| 110 | | case 'members.active': return this._membersActive(params) |
| 111 | | case 'members.mods': return this._membersMods(params) |
| 112 | | case 'members.count': return this._membersCount(params) |
| 113 | | case 'members.connected': return this._membersConnected(params) |
| 114 | | case 'members.search': return this._membersSearch(params) |
| 115 | | |
| 116 | | // --- Contacts --- |
| 117 | | case 'contact.info': return this._contactInfo(params) |
| 118 | | case 'contact.self': return this._contactSelf() |
| 119 | | case 'dm.fromContact': return this._dmFromContact(params) |
| 120 | | |
| 121 | | // --- Sub-APIs --- |
| 122 | | case 'network': return this._networkQuery() |
| 123 | | case 'preferences': return this._preferencesQuery() |
| 124 | | case 'preferences.set': return this._preferencesSet(params) |
| 125 | | case 'debug.stats': return this._debugStats(params) |
| 126 | | |
| 127 | | // --- System --- |
| 128 | | case 'version': return this._getVersion() |
| 129 | | case 'stats': return this._getStats() |
| 130 | | |
| 131 | | default: throw new Error(`Unknown method: ${method}`) |
| 132 | | } |
| 133 | | } |
| 134 | | |
| 135 | | // --- Identity --- |
| 136 | | |
| 137 | | async _identity () { |
| 138 | | const identity = await this.backend.api.core.getIdentity() |
| 139 | | return identity || null |
| 140 | | } |
| 141 | | |
| 142 | | async _identityRecovery () { |
| 143 | | const phrase = await this.backend.api.core.getRecoveryPhrase() |
| 144 | | return { phrase } |
| 145 | | } |
| 146 | | |
| 147 | | async _identityRestore (params) { |
| 148 | | if (!params.phrase) throw new Error('Missing required param: phrase') |
| 149 | | await this.backend.api.core.restoreIdentity(params.phrase) |
| 150 | | return { ok: true } |
| 151 | | } |
| 152 | | |
| 153 | | async _identitySetName (params) { |
| 154 | | if (!params.name) throw new Error('Missing required param: name') |
| 155 | | await this.backend.api.core.updateIdentityProfile({ displayName: params.name }) |
| 156 | | return { ok: true, name: params.name } |
| 157 | | } |
| 158 | | |
| 159 | | // --- Rooms --- |
| 160 | | |
| 161 | | async _rooms () { |
| 162 | | const result = await this.backend.api.core.getRecentRooms() |
| 163 | | const rooms = Array.isArray(result) ? result : (result?.rooms || result?.data || []) |
| 164 | | return rooms.map(room => ({ |
| 165 | | roomId: room.roomId || room.id || room.key || '?', |
| 166 | | title: room.config?.title || room.title || '(untitled)' |
| 167 | | })) |
| 168 | | } |
| 169 | | |
| 170 | | async _room (params) { |
| 171 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 172 | | return this.backend.api.core.getRoomInfo(params.roomId) |
| 173 | | } |
| 174 | | |
| 175 | | async _roomCreate (params) { |
| 176 | | if (!params.title) throw new Error('Missing required param: title') |
| 177 | | return this.backend.api.core.createRoom({ title: params.title }) |
| 178 | | } |
| 179 | | |
| 180 | | async _roomJoin (params) { |
| 181 | | if (!params.invite) throw new Error('Missing required param: invite') |
| 182 | | let invite = params.invite.trim() |
| 183 | | |
| 184 | | // Strip pear://keet/ or punch:// prefixes — keet-core expects raw invite token |
| 185 | | if (invite.startsWith('pear://keet/')) invite = invite.slice('pear://keet/'.length) |
| 186 | | else if (invite.startsWith('punch://')) invite = invite.slice('punch://'.length) |
| 187 | | |
| 188 | | try { |
| 189 | | const result = await this.backend.api.core.startPairingRoom(invite) |
| 190 | | return { status: 'pairing', ...(result || {}) } |
| 191 | | } catch (err) { |
| 192 | | // Fallback to pairRoom if startPairingRoom fails |
| 193 | | try { |
| 194 | | const result = await this.backend.api.core.pairRoom(invite) |
| 195 | | return { status: 'joined', ...(result || {}) } |
| 196 | | } catch { |
| 197 | | throw err |
| 198 | | } |
| 199 | | } |
| 200 | | } |
| 201 | | |
| 202 | | async _roomInvite (params) { |
| 203 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 204 | | const opts = {} |
| 205 | | if (params.role !== undefined) opts.role = params.role |
| 206 | | const result = await this.backend.api.core.createInvitation(params.roomId, opts) |
| 207 | | if (typeof result === 'string') { |
| 208 | | return { token: result } |
| 209 | | } |
| 210 | | return result |
| 211 | | } |
| 212 | | |
| 213 | | async _roomLeave (params) { |
| 214 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 215 | | await this.backend.api.core.leaveRoom(params.roomId) |
| 216 | | return { ok: true } |
| 217 | | } |
| 218 | | |
| 219 | | // --- Messages --- |
| 220 | | |
| 221 | | async _send (params) { |
| 222 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 223 | | if (!params.text) throw new Error('Missing required param: text') |
| 224 | | const result = await this.backend.api.core.addChatMessage(params.roomId, params.text, {}) |
| 225 | | return result || { ok: true } |
| 226 | | } |
| 227 | | |
| 228 | | async _read (params) { |
| 229 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 230 | | const limit = params.limit || 20 |
| 231 | | const result = await this.backend.api.core.getChatMessages(params.roomId, { |
| 232 | | limit, |
| 233 | | reverse: true |
| 234 | | }) |
| 235 | | const messages = Array.isArray(result) ? result : (result?.messages || result?.data || result?.events || []) |
| 236 | | |
| 237 | | // Resolve member display names |
| 238 | | const nameMap = await this._getMemberNames(params.roomId) |
| 239 | | |
| 240 | | return [...(messages || [])].reverse().map(msg => { |
| 241 | | const memberId = msg.memberId || 'unknown' |
| 242 | | const generic = msg.message?.generic || null |
| 243 | | // Normalize system: only set if there's an actual event (non-null/non-false value) |
| 244 | | let system = null |
| 245 | | if (generic && typeof generic === 'object') { |
| 246 | | for (const v of Object.values(generic)) { |
| 247 | | if (v !== null && v !== false) { system = generic; break } |
| 248 | | } |
| 249 | | } else if (generic) { |
| 250 | | system = generic |
| 251 | | } |
| 252 | | // Extract file attachments (encoded file structure from keet-core API) |
| 253 | | // Encoded files have: metadata.{name,mimetype,size,dimensions}, entry.{httpLink, preview.small.httpLink}, |
| 254 | | // pointer.externalBlob.id (z32 key). Raw files have: name, mimeType, size at top level. |
| 255 | | const rawFiles = msg.message?.files || msg.files || [] |
| 256 | | const files = Array.isArray(rawFiles) ? rawFiles.map(f => { |
| 257 | | const meta = f.metadata || {} |
| 258 | | const entry = f.entry || {} |
| 259 | | const name = meta.name || f.name || f.fileName || 'file' |
| 260 | | return { |
| 261 | | id: name, // Use filename as canonical ID (most reliable across encoded/raw formats) |
| 262 | | name, |
| 263 | | type: meta.mimetype || f.mimeType || '', |
| 264 | | size: meta.size || f.size || 0, |
| 265 | | dimensions: meta.dimensions || f.dimensions || null, |
| 266 | | httpLink: entry.httpLink || f.httpLink || '', |
| 267 | | preview: entry.preview ? { |
| 268 | | small: entry.preview.small?.httpLink || '', |
| 269 | | medium: entry.preview.medium?.httpLink || '' |
| 270 | | } : null |
| 271 | | } |
| 272 | | }) : [] |
| 273 | | |
| 274 | | return { |
| 275 | | memberId, |
| 276 | | name: nameMap[memberId] || memberId.slice(0, 8), |
| 277 | | text: msg.chat?.text || msg.text || msg.message?.text || '', |
| 278 | | timestamp: msg.timestamp || null, |
| 279 | | deleted: msg.deleted || false, |
| 280 | | system, |
| 281 | | files: files.length > 0 ? files : null, |
| 282 | | seq: msg.seq ?? msg.clock ?? null, |
| 283 | | id: msg.id || null |
| 284 | | } |
| 285 | | }).filter(m => m.text || m.system || m.files) |
| 286 | | } |
| 287 | | |
| 288 | | async _getMemberNames (roomId) { |
| 289 | | try { |
| 290 | | const result = await this.backend.api.core.getMembers(roomId) |
| 291 | | const members = Array.isArray(result) ? result : (result?.members || result?.data || []) |
| 292 | | const map = {} |
| 293 | | for (const m of (members || [])) { |
| 294 | | const id = m.memberId || m.id |
| 295 | | if (id) map[id] = m.displayName || m.name || id.slice(0, 8) |
| 296 | | } |
| 297 | | return map |
| 298 | | } catch { |
| 299 | | return {} |
| 300 | | } |
| 301 | | } |
| 302 | | |
| 303 | | async _watch () { |
| 304 | | // watch is handled at the transport level (SSE / socket push) |
| 305 | | throw new Error('watch must be handled by transport layer') |
| 306 | | } |
| 307 | | |
| 308 | | // --- Members --- |
| 309 | | |
| 310 | | async _changeMemberRole (params) { |
| 311 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 312 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 313 | | if (params.role === undefined) throw new Error('Missing required param: role (1=admin, 2=mod, 3=peer)') |
| 314 | | await this.backend.api.core.changeMemberRole(params.roomId, { |
| 315 | | memberId: params.memberId, |
| 316 | | role: params.role |
| 317 | | }) |
| 318 | | return { ok: true } |
| 319 | | } |
| 320 | | |
| 321 | | async _members (params) { |
| 322 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 323 | | const result = await this.backend.api.core.getMembers(params.roomId) |
| 324 | | const members = Array.isArray(result) ? result : (result?.members || result?.data || []) |
| 325 | | return (members || []).map(m => { |
| 326 | | let role = 'peer' |
| 327 | | if (m.status?.isAdmin) role = 'admin' |
| 328 | | else if (m.status?.isModerator) role = 'mod' |
| 329 | | else if (m.role === 1) role = 'admin' |
| 330 | | else if (m.role === 2) role = 'mod' |
| 331 | | else if (m.role === 4) role = 'read-only' |
| 332 | | return { |
| 333 | | memberId: m.memberId || m.id || '?', |
| 334 | | name: m.displayName || m.name || m.memberId || '?', |
| 335 | | role, |
| 336 | | online: m.status?.isActive || m.online || false, |
| 337 | | canDM: m.canDM || false |
| 338 | | } |
| 339 | | }) |
| 340 | | } |
| 341 | | |
| 342 | | // --- DMs --- |
| 343 | | |
| 344 | | async _dmRequest (params) { |
| 345 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 346 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 347 | | const message = params.message || '' |
| 348 | | const result = await this.backend.api.core.sendDMRequest( |
| 349 | | params.roomId, |
| 350 | | params.memberId, |
| 351 | | { message, receivers: [params.memberId] } |
| 352 | | ) |
| 353 | | return result || { ok: true } |
| 354 | | } |
| 355 | | |
| 356 | | // --- Message Operations --- |
| 357 | | |
| 358 | | async _editMessage (params) { |
| 359 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 360 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 361 | | if (!params.text) throw new Error('Missing required param: text') |
| 362 | | await this.backend.api.core.updateChatMessage(params.roomId, params.messageId, params.text) |
| 363 | | return { ok: true } |
| 364 | | } |
| 365 | | |
| 366 | | async _deleteMessage (params) { |
| 367 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 368 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 369 | | await this.backend.api.core.removeChatMessage(params.roomId, params.messageId) |
| 370 | | return { ok: true } |
| 371 | | } |
| 372 | | |
| 373 | | async _pinMessage (params) { |
| 374 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 375 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 376 | | await this.backend.api.core.pinChatMessage(params.roomId, params.messageId) |
| 377 | | return { ok: true } |
| 378 | | } |
| 379 | | |
| 380 | | async _unpinMessage (params) { |
| 381 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 382 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 383 | | await this.backend.api.core.unpinChatMessage(params.roomId, params.messageId) |
| 384 | | return { ok: true } |
| 385 | | } |
| 386 | | |
| 387 | | async _getPinned (params) { |
| 388 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 389 | | return this.backend.api.core.getPinnedChatMessages(params.roomId) |
| 390 | | } |
| 391 | | |
| 392 | | async _searchMessages (params) { |
| 393 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 394 | | if (!params.query) throw new Error('Missing required param: query') |
| 395 | | return this.backend.api.core.searchChatMessages(params.roomId, { query: params.query }) |
| 396 | | } |
| 397 | | |
| 398 | | // --- Reactions --- |
| 399 | | |
| 400 | | async _addReaction (params) { |
| 401 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 402 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 403 | | if (!params.emoji) throw new Error('Missing required param: emoji') |
| 404 | | await this.backend.api.core.addReaction(params.roomId, params.messageId, params.emoji) |
| 405 | | return { ok: true } |
| 406 | | } |
| 407 | | |
| 408 | | async _removeReaction (params) { |
| 409 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 410 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 411 | | if (!params.emoji) throw new Error('Missing required param: emoji') |
| 412 | | await this.backend.api.core.removeReaction(params.roomId, params.messageId, params.emoji) |
| 413 | | return { ok: true } |
| 414 | | } |
| 415 | | |
| 416 | | async _getReactions (params) { |
| 417 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 418 | | if (!params.messageId) throw new Error('Missing required param: messageId') |
| 419 | | return this.backend.api.core.getReactions(params.roomId, params.messageId) |
| 420 | | } |
| 421 | | |
| 422 | | // --- Moderation --- |
| 423 | | |
| 424 | | async _muteMember (params) { |
| 425 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 426 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 427 | | await this.backend.api.core.muteMember(params.roomId, params.memberId) |
| 428 | | return { ok: true } |
| 429 | | } |
| 430 | | |
| 431 | | async _unmuteMember (params) { |
| 432 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 433 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 434 | | await this.backend.api.core.unmuteMember(params.roomId, params.memberId) |
| 435 | | return { ok: true } |
| 436 | | } |
| 437 | | |
| 438 | | async _blockMember (params) { |
| 439 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 440 | | await this.backend.api.core.blockMember(params.memberId) |
| 441 | | return { ok: true } |
| 442 | | } |
| 443 | | |
| 444 | | async _unblockMember (params) { |
| 445 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 446 | | await this.backend.api.core.unblockMember(params.memberId) |
| 447 | | return { ok: true } |
| 448 | | } |
| 449 | | |
| 450 | | async _removeMember (params) { |
| 451 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 452 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 453 | | await this.backend.api.core.removeMember(params.roomId, params.memberId) |
| 454 | | return { ok: true } |
| 455 | | } |
| 456 | | |
| 457 | | async _getBlocked (params) { |
| 458 | | return this.backend.api.core.getBlockedMembers() |
| 459 | | } |
| 460 | | |
| 461 | | // --- Calls --- |
| 462 | | |
| 463 | | _callApi () { |
| 464 | | const api = this.backend.api.call |
| 465 | | if (!api) throw new Error('Call API not available') |
| 466 | | return api |
| 467 | | } |
| 468 | | |
| 469 | | async _callAuthorize (params) { |
| 470 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 471 | | await this._callApi().authorize(params.roomId) |
| 472 | | return { ok: true } |
| 473 | | } |
| 474 | | |
| 475 | | async _callDeauthorize (params) { |
| 476 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 477 | | await this._callApi().deauthorize(params.roomId) |
| 478 | | return { ok: true } |
| 479 | | } |
| 480 | | |
| 481 | | async _callJoinPeer (params) { |
| 482 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 483 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 484 | | await this._callApi().joinPeer(params.roomId, params.peerId) |
| 485 | | return { ok: true } |
| 486 | | } |
| 487 | | |
| 488 | | async _callLeavePeer (params) { |
| 489 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 490 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 491 | | await this._callApi().leavePeer(params.roomId, params.peerId) |
| 492 | | return { ok: true } |
| 493 | | } |
| 494 | | |
| 495 | | async _callPresence (params) { |
| 496 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 497 | | return this._callApi().getPresence(params.roomId) |
| 498 | | } |
| 499 | | |
| 500 | | async _callSetPresent (params) { |
| 501 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 502 | | await this._callApi().setPresent(params.roomId) |
| 503 | | return { ok: true } |
| 504 | | } |
| 505 | | |
| 506 | | async _callSetNotPresent (params) { |
| 507 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 508 | | await this._callApi().setNotPresent(params.roomId) |
| 509 | | return { ok: true } |
| 510 | | } |
| 511 | | |
| 512 | | async _callGetTracks (params) { |
| 513 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 514 | | return this._callApi().getAvailableTracks(params.roomId) |
| 515 | | } |
| 516 | | |
| 517 | | async _callRequestTrack (params) { |
| 518 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 519 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 520 | | if (!params.trackKind) throw new Error('Missing required param: trackKind (audio|video|screen)') |
| 521 | | await this._callApi().requestTrack(params.roomId, params.peerId, params.trackKind) |
| 522 | | return { ok: true } |
| 523 | | } |
| 524 | | |
| 525 | | async _callUnrequestTrack (params) { |
| 526 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 527 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 528 | | if (!params.trackKind) throw new Error('Missing required param: trackKind') |
| 529 | | await this._callApi().unrequestTrack(params.roomId, params.peerId, params.trackKind) |
| 530 | | return { ok: true } |
| 531 | | } |
| 532 | | |
| 533 | | async _callShareTrack (params) { |
| 534 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 535 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 536 | | if (!params.trackKind) throw new Error('Missing required param: trackKind') |
| 537 | | await this._callApi().shareTrack(params.roomId, params.peerId, params.trackKind, params.trackData || null) |
| 538 | | return { ok: true } |
| 539 | | } |
| 540 | | |
| 541 | | async _callUnshareTrack (params) { |
| 542 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 543 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 544 | | if (!params.trackKind) throw new Error('Missing required param: trackKind') |
| 545 | | await this._callApi().unshareTrack(params.roomId, params.peerId, params.trackKind) |
| 546 | | return { ok: true } |
| 547 | | } |
| 548 | | |
| 549 | | async _callSignalMedia (params) { |
| 550 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 551 | | if (!params.peerId) throw new Error('Missing required param: peerId') |
| 552 | | if (!params.signal) throw new Error('Missing required param: signal') |
| 553 | | await this._callApi().signalMedia(params.roomId, params.peerId, params.signal) |
| 554 | | return { ok: true } |
| 555 | | } |
| 556 | | |
| 557 | | // call.raw and raw removed — arbitrary function execution vectors (SECURITY_AUDIT_V2 D-F3) |
| 558 | | |
| 559 | | // --- Streaming --- |
| 560 | | |
| 561 | | subscribe (roomId) { |
| 562 | | return this.backend.api.core.subscribeChatMessages(roomId) |
| 563 | | } |
| 564 | | |
| 565 | | // --- System --- |
| 566 | | |
| 567 | | async _status () { |
| 568 | | const identity = await this.backend.api.core.getIdentity() |
| 569 | | const rooms = await this._rooms() |
| 570 | | return { |
| 571 | | uptime: Math.floor((Date.now() - this._startTime) / 1000), |
| 572 | | rooms: rooms.length, |
| 573 | | identity: identity ? { memberId: identity.memberId, name: identity.displayName } : null |
| 574 | | } |
| 575 | | } |
| 576 | | |
| 577 | | async _shutdown () { |
| 578 | | setTimeout(async () => { |
| 579 | | await this.core.close() |
| 580 | | Bare.exit(0) |
| 581 | | }, 500) |
| 582 | | return { ok: true, message: 'shutting down' } |
| 583 | | } |
| 584 | | |
| 585 | | // --- Introspection --- |
| 586 | | |
| 587 | | async _introspect (params) { |
| 588 | | const target = params.api || 'core' |
| 589 | | const api = target === 'core' ? this.backend.api.core |
| 590 | | : target === 'call' ? this.backend.api.call |
| 591 | | : target === 'preferences' ? this.backend.api.preferences |
| 592 | | : target === 'network' ? this.backend.api.network |
| 593 | | : target === 'debug' ? this.backend.api.debug |
| 594 | | : null |
| 595 | | if (!api) throw new Error(`Unknown API: ${target}`) |
| 596 | | const methods = [] |
| 597 | | for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(api))) { |
| 598 | | if (key === 'constructor') continue |
| 599 | | if (typeof api[key] === 'function') methods.push(key) |
| 600 | | } |
| 601 | | for (const key of Object.keys(api)) { |
| 602 | | if (typeof api[key] === 'function' && !methods.includes(key)) methods.push(key) |
| 603 | | } |
| 604 | | return { api: target, methods: methods.sort(), count: methods.length } |
| 605 | | } |
| 606 | | |
| 607 | | // --- Identity & Username --- |
| 608 | | |
| 609 | | async _identityConfirm () { |
| 610 | | await this.backend.api.core.confirmRecoveryPhrase() |
| 611 | | return { ok: true } |
| 612 | | } |
| 613 | | |
| 614 | | async _identityStore (params) { |
| 615 | | if (!params.phrase) throw new Error('Missing required param: phrase') |
| 616 | | await this.backend.api.core.storeRecoveryPhrase(params.phrase) |
| 617 | | return { ok: true } |
| 618 | | } |
| 619 | | |
| 620 | | async _identityRegister (params) { |
| 621 | | if (!params.username) throw new Error('Missing required param: username') |
| 622 | | const result = await this.backend.api.core.registerUsername(params.username) |
| 623 | | return { ok: true, result } |
| 624 | | } |
| 625 | | |
| 626 | | async _identityCheckName (params) { |
| 627 | | if (!params.username) throw new Error('Missing required param: username') |
| 628 | | const available = await this.backend.api.core.checkUsername(params.username) |
| 629 | | return { username: params.username, available } |
| 630 | | } |
| 631 | | |
| 632 | | async _identityLookup (params) { |
| 633 | | if (!params.username) throw new Error('Missing required param: username') |
| 634 | | return this.backend.api.core.lookupUsername(params.username) |
| 635 | | } |
| 636 | | |
| 637 | | async _identityLookupMember (params) { |
| 638 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 639 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 640 | | return this.backend.api.core.lookupUserByMemberId(params.roomId, params.memberId) |
| 641 | | } |
| 642 | | |
| 643 | | // --- Room Config & Management --- |
| 644 | | |
| 645 | | async _roomConfig (params) { |
| 646 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 647 | | return this.backend.api.core.getConfig(params.roomId) |
| 648 | | } |
| 649 | | |
| 650 | | async _roomConfigSet (params) { |
| 651 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 652 | | if (!params.key) throw new Error('Missing required param: key') |
| 653 | | if (params.value === undefined) throw new Error('Missing required param: value') |
| 654 | | await this.backend.api.core.updateConfig(params.roomId, params.key, params.value) |
| 655 | | return { ok: true } |
| 656 | | } |
| 657 | | |
| 658 | | async _roomBookmark (params) { |
| 659 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 660 | | await this.backend.api.core.bookmark(params.roomId) |
| 661 | | return { ok: true } |
| 662 | | } |
| 663 | | |
| 664 | | async _roomUnbookmark (params) { |
| 665 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 666 | | await this.backend.api.core.unbookmark(params.roomId) |
| 667 | | return { ok: true } |
| 668 | | } |
| 669 | | |
| 670 | | async _roomPreferences (params) { |
| 671 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 672 | | return this.backend.api.core.getRoomPreferences(params.roomId) |
| 673 | | } |
| 674 | | |
| 675 | | async _roomPreferencesSet (params) { |
| 676 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 677 | | const { roomId, ...prefs } = params |
| 678 | | await this.backend.api.core.updateRoomPreferences(roomId, prefs) |
| 679 | | return { ok: true } |
| 680 | | } |
| 681 | | |
| 682 | | async _roomSearch (params) { |
| 683 | | if (!params.query) throw new Error('Missing required param: query') |
| 684 | | return this.backend.api.core.searchRooms(params.query) |
| 685 | | } |
| 686 | | |
| 687 | | async _roomUnread (params) { |
| 688 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 689 | | if (params.unread === undefined) throw new Error('Missing required param: unread') |
| 690 | | await this.backend.api.core.setRoomUnread(params.roomId, params.unread) |
| 691 | | return { ok: true } |
| 692 | | } |
| 693 | | |
| 694 | | // --- DM Management --- |
| 695 | | |
| 696 | | async _dmList () { |
| 697 | | // No getDmList — use getDmRequestsByStatus(1) for accepted DMs |
| 698 | | return this.backend.api.core.getDmRequestsByStatus(1) |
| 699 | | } |
| 700 | | |
| 701 | | async _dmRequests (params) { |
| 702 | | const status = params.status || 3 // default: pending |
| 703 | | return this.backend.api.core.getDmRequestsByStatus(status) |
| 704 | | } |
| 705 | | |
| 706 | | async _dmAccept (params) { |
| 707 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 708 | | await this.backend.api.core.acceptDmRequest(params.roomId) |
| 709 | | return { ok: true } |
| 710 | | } |
| 711 | | |
| 712 | | async _dmReject (params) { |
| 713 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 714 | | await this.backend.api.core.rejectDmRequest(params.roomId) |
| 715 | | return { ok: true } |
| 716 | | } |
| 717 | | |
| 718 | | // --- File Management --- |
| 719 | | |
| 720 | | async _fileSend (params) { |
| 721 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 722 | | if (!params.file) throw new Error('Missing required param: file') |
| 723 | | return this.backend.api.core.sendFile(params.roomId, params.file) |
| 724 | | } |
| 725 | | |
| 726 | | async _fileList (params) { |
| 727 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 728 | | return this.backend.api.core.getLocalFiles(params.roomId) |
| 729 | | } |
| 730 | | |
| 731 | | async _fileInfo (params) { |
| 732 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 733 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 734 | | return this.backend.api.core.getFileEntry(params.roomId, params.fileId) |
| 735 | | } |
| 736 | | |
| 737 | | async _fileDownload (params) { |
| 738 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 739 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 740 | | await this.backend.api.core.startDownload(params.roomId, params.fileId) |
| 741 | | return { ok: true } |
| 742 | | } |
| 743 | | |
| 744 | | async _fileCancel (params) { |
| 745 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 746 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 747 | | await this.backend.api.core.cancelDownload(params.roomId, params.fileId) |
| 748 | | return { ok: true } |
| 749 | | } |
| 750 | | |
| 751 | | async _fileRemove (params) { |
| 752 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 753 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 754 | | await this.backend.api.core.removeFile(params.roomId, params.fileId) |
| 755 | | return { ok: true } |
| 756 | | } |
| 757 | | |
| 758 | | async _fileClear (params) { |
| 759 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 760 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 761 | | await this.backend.api.core.clearFile(params.roomId, params.fileId) |
| 762 | | return { ok: true } |
| 763 | | } |
| 764 | | |
| 765 | | async _fileFetch (params) { |
| 766 | | if (!params.url) throw new Error('Missing required param: url (httpLink URL from file preview)') |
| 767 | | const { default: fetch } = await import('bare-fetch') |
| 768 | | const resp = await fetch(params.url) |
| 769 | | if (!resp.ok) throw new Error(`Fetch failed: ${resp.status} ${resp.statusText}`) |
| 770 | | const buf = Buffer.from(await resp.arrayBuffer()) |
| 771 | | const contentType = resp.headers.get('content-type') || 'application/octet-stream' |
| 772 | | return { base64: buf.toString('base64'), contentType, size: buf.length } |
| 773 | | } |
| 774 | | |
| 775 | | async _filePath (params) { |
| 776 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 777 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 778 | | const result = await this.backend.api.core.getFileEntry(params.roomId, params.fileId) |
| 779 | | return result || {} |
| 780 | | } |
| 781 | | |
| 782 | | async _filePreview (params) { |
| 783 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 784 | | if (!params.fileId) throw new Error('Missing required param: fileId') |
| 785 | | |
| 786 | | // getLocalFiles returns raw DB entries (without httpLinks) |
| 787 | | const files = await this.backend.api.core.getLocalFiles(params.roomId) |
| 788 | | const entries = Array.isArray(files) ? files : (files?.data || files?.entries || []) |
| 789 | | |
| 790 | | console.log('[keet-daemon] file.preview: looking for "%s" among %d local files', params.fileId, entries.length) |
| 791 | | if (entries.length > 0) { |
| 792 | | const sample = entries[0] |
| 793 | | const sObj = sample.file || sample |
| 794 | | console.log('[keet-daemon] sample entry keys:', Object.keys(sObj).join(','), |
| 795 | | 'metadata.name:', sObj?.metadata?.name, |
| 796 | | 'pointer.externalBlob:', sObj?.pointer?.externalBlob ? 'yes' : 'no') |
| 797 | | } |
| 798 | | |
| 799 | | // Match file by FILENAME (primary) or blob ID (fallback) |
| 800 | | const targetId = String(params.fileId) |
| 801 | | const entry = entries.find(f => { |
| 802 | | const fileObj = f.file || f |
| 803 | | // Try filename match first (most reliable — Godot sends filename as fileId) |
| 804 | | const name = fileObj?.metadata?.name || '' |
| 805 | | if (name && name === targetId) return true |
| 806 | | // Fallback: match by blob ID (z32 or hex) |
| 807 | | const blobId = fileObj?.pointer?.externalBlob?.id |
| 808 | | if (!blobId) return false |
| 809 | | const idStr = Buffer.isBuffer(blobId) ? blobId.toString('hex') : String(blobId) |
| 810 | | return idStr === targetId |
| 811 | | }) |
| 812 | | |
| 813 | | if (!entry) { |
| 814 | | const names = entries.slice(0, 5).map(f => (f.file || f)?.metadata?.name || '?').join(', ') |
| 815 | | throw new Error('File not found: ' + params.fileId + ' (have: ' + names + ')') |
| 816 | | } |
| 817 | | |
| 818 | | // Generate httpLink using FileManager's blob server |
| 819 | | const fileObj = entry.file || entry |
| 820 | | let httpLink = '' |
| 821 | | try { |
| 822 | | httpLink = String(await this.core.files.getFileLink(entry.roomKey, fileObj) || '') |
| 823 | | } catch (err) { |
| 824 | | console.error('[keet-daemon] getFileLink error:', err.message) |
| 825 | | } |
| 826 | | |
| 827 | | console.log('[keet-daemon] file.preview: found "%s" -> httpLink=%s', params.fileId, (httpLink || '').slice(0, 80)) |
| 828 | | |
| 829 | | const meta = fileObj?.metadata || {} |
| 830 | | return { |
| 831 | | name: meta.name || 'file', |
| 832 | | type: meta.mimetype || '', |
| 833 | | size: meta.size || 0, |
| 834 | | httpLink, |
| 835 | | dimensions: meta.dimensions || null |
| 836 | | } |
| 837 | | } |
| 838 | | |
| 839 | | // --- Advanced Members --- |
| 840 | | |
| 841 | | async _membersActive (params) { |
| 842 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 843 | | return this.backend.api.core.getActiveMembers(params.roomId) |
| 844 | | } |
| 845 | | |
| 846 | | async _membersMods (params) { |
| 847 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 848 | | return this.backend.api.core.getModerators(params.roomId) |
| 849 | | } |
| 850 | | |
| 851 | | async _membersCount (params) { |
| 852 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 853 | | return this.backend.api.core.getMemberCount(params.roomId) |
| 854 | | } |
| 855 | | |
| 856 | | async _membersConnected (params) { |
| 857 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 858 | | return this.backend.api.core.getConnectedMembers(params.roomId) |
| 859 | | } |
| 860 | | |
| 861 | | async _membersSearch (params) { |
| 862 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 863 | | if (!params.query) throw new Error('Missing required param: query') |
| 864 | | return this.backend.api.core.getSearchMembers(params.roomId, params.query) |
| 865 | | } |
| 866 | | |
| 867 | | // --- Contacts --- |
| 868 | | |
| 869 | | async _contactInfo (params) { |
| 870 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 871 | | return this.backend.api.core.getContactInfo(params.memberId) |
| 872 | | } |
| 873 | | |
| 874 | | async _contactSelf () { |
| 875 | | return this.backend.api.core.getSelfContactInfo() |
| 876 | | } |
| 877 | | |
| 878 | | async _dmFromContact (params) { |
| 879 | | if (!params.memberId) throw new Error('Missing required param: memberId') |
| 880 | | return this.backend.api.core.sendDMFromContact(params.memberId) |
| 881 | | } |
| 882 | | |
| 883 | | // --- Sub-APIs --- |
| 884 | | |
| 885 | | async _networkQuery () { |
| 886 | | const api = this.backend.api.network |
| 887 | | if (!api) throw new Error('Network API not available') |
| 888 | | return api.query() |
| 889 | | } |
| 890 | | |
| 891 | | async _preferencesQuery () { |
| 892 | | const api = this.backend.api.preferences |
| 893 | | if (!api) throw new Error('Preferences API not available') |
| 894 | | return api.query() |
| 895 | | } |
| 896 | | |
| 897 | | async _preferencesSet (params) { |
| 898 | | const api = this.backend.api.preferences |
| 899 | | if (!api) throw new Error('Preferences API not available') |
| 900 | | await api.update(params) |
| 901 | | return { ok: true } |
| 902 | | } |
| 903 | | |
| 904 | | async _debugStats (params) { |
| 905 | | const api = this.backend.api.debug |
| 906 | | if (!api) throw new Error('Debug API not available') |
| 907 | | if (!params.roomId) throw new Error('Missing required param: roomId') |
| 908 | | return api.getCurrentRoomStats(params.roomId) |
| 909 | | } |
| 910 | | |
| 911 | | // --- System (extended) --- |
| 912 | | |
| 913 | | async _getVersion () { |
| 914 | | return this.backend.api.core.getVersion() |
| 915 | | } |
| 916 | | |
| 917 | | async _getStats () { |
| 918 | | return this.backend.api.core.getStats() |
| 919 | | } |
| 920 | | } |