OverviewFilesCommitsIssuesDocsReleasesPeers

keet-cli commits / b9334e0

Add calls, message ops, reactions, moderation, and log endpoint
- /rpc/log/:roomId — plain-text log with full timestamps + display names - Calls API (14 methods): authorize/deauthorize work headlessly, presence/tracks need WebRTC - Message operations: edit, delete, pin, unpin, pinned, search - Reactions: react, unreact, get reactions (using {deviceId, seq} message IDs) - Moderation: mute, unmute, block, unblock, kick, blocked - Messages now include name, seq, and id fields - System event normalization (filter out all-null generic objects) - Member name resolution via _getMemberNames() 55 RPC methods now exposed out of 176 in keet-core. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
b9334e0f22acd82d071cede6422428ba288aba53
by plankatron <fleeky@gmail.com> · 2026-02-14T13:43:47.000Z

Changed files (3) +314 -7

daemon.js +1 -0
……@@ -45,6 +45,7 @@ export async function startDaemon (opts = {}) {
4545 console.log(` GET http://localhost:${port}/rpc/rooms`)
4646 console.log(` GET http://localhost:${port}/rpc/status`)
4747 console.log(` GET http://localhost:${port}/rpc/identity`)
48 console.log(` GET http://localhost:${port}/rpc/log/:roomId?limit=50 (plain text)`)
4849 console.log(` GET http://localhost:${port}/rpc/messages/:roomId?limit=20`)
4950 console.log(` GET http://localhost:${port}/rpc/members/:roomId`)
5051 console.log(` GET http://localhost:${port}/rpc/watch/:roomId (SSE)`)
lib/http-transport.js +32 -0
……@@ -56,6 +56,25 @@ export class HttpTransport {
5656 return this._json(res, await this.router.handle('identity'))
5757 }
5858
59 if (req.method === 'GET' && path.startsWith('/rpc/log/')) {
60 const roomId = decodeURIComponent(path.slice('/rpc/log/'.length))
61 const query = parseQuery(queryStr)
62 const limit = parseInt(query.limit) || 50
63 const result = await this.router.handle('read', { roomId, limit })
64 if (!result.ok) return this._json(res, result)
65 const lines = (result.data || []).map(m => {
66 const ts = m.timestamp ? new Date(m.timestamp).toISOString().replace('T', ' ').slice(0, 19) : '????-??-?? ??:??:??'
67 const who = m.name || m.memberId?.slice(0, 8) || '?'
68 if (m.system) return `[${ts}] * ${formatSystemEvent(m.system)}`
69 const del = m.deleted ? ' [DELETED]' : ''
70 return `[${ts}] ${who}: ${m.text}${del}`
71 })
72 const body = lines.join('\n') + '\n'
73 res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(body) })
74 res.end(body)
75 return
76 }
77
5978 if (req.method === 'GET' && path.startsWith('/rpc/messages/')) {
6079 const roomId = decodeURIComponent(path.slice('/rpc/messages/'.length))
6180 const query = parseQuery(queryStr)
……@@ -176,6 +195,19 @@ function readBody (req) {
176195 })
177196}
178197
198function formatSystemEvent (generic) {
199 if (!generic) return 'SYSTEM_EVENT'
200 if (typeof generic === 'number') {
201 const names = { 1: 'MEMBER_JOINED', 2: 'CALL_STARTED', 3: 'AVATAR_CHANGED', 4: 'INVITATION_CREATED', 5: 'MEMBER_LEFT', 6: 'TITLE_CHANGED', 7: 'MEMBER_REMOVED', 8: 'CALL_HANGUP', 9: 'CALL_MISSED' }
202 return names[generic] || `EVENT_${generic}`
203 }
204 // Object form — pick first truthy key
205 for (const [k, v] of Object.entries(generic)) {
206 if (v !== null && v !== false) return k.replace(/([A-Z])/g, '_$1').toUpperCase()
207 }
208 return 'SYSTEM_EVENT'
209}
210
179211function parseQuery (str) {
180212 const params = {}
181213 if (!str) return params
lib/router.js +281 -7
……@@ -32,6 +32,35 @@ export class Router {
3232 case 'members.role': return this._changeMemberRole(params)
3333 case 'dm.request': return this._dmRequest(params)
3434 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 case 'call.raw': return this._callRaw(params)
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)
3564 case 'raw': return this._raw(params)
3665 case 'status': return this._status()
3766 case 'shutdown': return this._shutdown()
……@@ -140,13 +169,48 @@ export class Router {
140169 reverse: true
141170 })
142171 const messages = Array.isArray(result) ? result : (result?.messages || result?.data || result?.events || [])
143 return [...(messages || [])].reverse().map(msg => ({
144 memberId: msg.memberId || 'unknown',
145 text: msg.chat?.text || msg.text || msg.message?.text || '',
146 timestamp: msg.timestamp || null,
147 deleted: msg.deleted || false,
148 system: msg.message?.generic || null
149 })).filter(m => m.text || m.system)
172
173 // Resolve member display names
174 const nameMap = await this._getMemberNames(params.roomId)
175
176 return [...(messages || [])].reverse().map(msg => {
177 const memberId = msg.memberId || 'unknown'
178 const generic = msg.message?.generic || null
179 // Normalize system: only set if there's an actual event (non-null/non-false value)
180 let system = null
181 if (generic && typeof generic === 'object') {
182 for (const v of Object.values(generic)) {
183 if (v !== null && v !== false) { system = generic; break }
184 }
185 } else if (generic) {
186 system = generic
187 }
188 return {
189 memberId,
190 name: nameMap[memberId] || memberId.slice(0, 8),
191 text: msg.chat?.text || msg.text || msg.message?.text || '',
192 timestamp: msg.timestamp || null,
193 deleted: msg.deleted || false,
194 system,
195 seq: msg.seq ?? msg.clock ?? null,
196 id: msg.id || null
197 }
198 }).filter(m => m.text || m.system)
199 }
200
201 async _getMemberNames (roomId) {
202 try {
203 const result = await this.backend.api.core.getMembers(roomId)
204 const members = Array.isArray(result) ? result : (result?.members || result?.data || [])
205 const map = {}
206 for (const m of (members || [])) {
207 const id = m.memberId || m.id
208 if (id) map[id] = m.displayName || m.name || id.slice(0, 8)
209 }
210 return map
211 } catch {
212 return {}
213 }
150214 }
151215
152216 async _watch () {
……@@ -202,6 +266,216 @@ export class Router {
202266 return result || { ok: true }
203267 }
204268
269 // --- Message Operations ---
270
271 async _editMessage (params) {
272 if (!params.roomId) throw new Error('Missing required param: roomId')
273 if (!params.messageId) throw new Error('Missing required param: messageId')
274 if (!params.text) throw new Error('Missing required param: text')
275 await this.backend.api.core.updateChatMessage(params.roomId, params.messageId, params.text)
276 return { ok: true }
277 }
278
279 async _deleteMessage (params) {
280 if (!params.roomId) throw new Error('Missing required param: roomId')
281 if (!params.messageId) throw new Error('Missing required param: messageId')
282 await this.backend.api.core.removeChatMessage(params.roomId, params.messageId)
283 return { ok: true }
284 }
285
286 async _pinMessage (params) {
287 if (!params.roomId) throw new Error('Missing required param: roomId')
288 if (!params.messageId) throw new Error('Missing required param: messageId')
289 await this.backend.api.core.pinChatMessage(params.roomId, params.messageId)
290 return { ok: true }
291 }
292
293 async _unpinMessage (params) {
294 if (!params.roomId) throw new Error('Missing required param: roomId')
295 if (!params.messageId) throw new Error('Missing required param: messageId')
296 await this.backend.api.core.unpinChatMessage(params.roomId, params.messageId)
297 return { ok: true }
298 }
299
300 async _getPinned (params) {
301 if (!params.roomId) throw new Error('Missing required param: roomId')
302 return this.backend.api.core.getPinnedChatMessages(params.roomId)
303 }
304
305 async _searchMessages (params) {
306 if (!params.roomId) throw new Error('Missing required param: roomId')
307 if (!params.query) throw new Error('Missing required param: query')
308 return this.backend.api.core.searchChatMessages(params.roomId, { query: params.query })
309 }
310
311 // --- Reactions ---
312
313 async _addReaction (params) {
314 if (!params.roomId) throw new Error('Missing required param: roomId')
315 if (!params.messageId) throw new Error('Missing required param: messageId')
316 if (!params.emoji) throw new Error('Missing required param: emoji')
317 await this.backend.api.core.addReaction(params.roomId, params.messageId, params.emoji)
318 return { ok: true }
319 }
320
321 async _removeReaction (params) {
322 if (!params.roomId) throw new Error('Missing required param: roomId')
323 if (!params.messageId) throw new Error('Missing required param: messageId')
324 if (!params.emoji) throw new Error('Missing required param: emoji')
325 await this.backend.api.core.removeReaction(params.roomId, params.messageId, params.emoji)
326 return { ok: true }
327 }
328
329 async _getReactions (params) {
330 if (!params.roomId) throw new Error('Missing required param: roomId')
331 if (!params.messageId) throw new Error('Missing required param: messageId')
332 return this.backend.api.core.getReactions(params.roomId, params.messageId)
333 }
334
335 // --- Moderation ---
336
337 async _muteMember (params) {
338 if (!params.roomId) throw new Error('Missing required param: roomId')
339 if (!params.memberId) throw new Error('Missing required param: memberId')
340 await this.backend.api.core.muteMember(params.roomId, params.memberId)
341 return { ok: true }
342 }
343
344 async _unmuteMember (params) {
345 if (!params.roomId) throw new Error('Missing required param: roomId')
346 if (!params.memberId) throw new Error('Missing required param: memberId')
347 await this.backend.api.core.unmuteMember(params.roomId, params.memberId)
348 return { ok: true }
349 }
350
351 async _blockMember (params) {
352 if (!params.memberId) throw new Error('Missing required param: memberId')
353 await this.backend.api.core.blockMember(params.memberId)
354 return { ok: true }
355 }
356
357 async _unblockMember (params) {
358 if (!params.memberId) throw new Error('Missing required param: memberId')
359 await this.backend.api.core.unblockMember(params.memberId)
360 return { ok: true }
361 }
362
363 async _removeMember (params) {
364 if (!params.roomId) throw new Error('Missing required param: roomId')
365 if (!params.memberId) throw new Error('Missing required param: memberId')
366 await this.backend.api.core.removeMember(params.roomId, params.memberId)
367 return { ok: true }
368 }
369
370 async _getBlocked (params) {
371 return this.backend.api.core.getBlockedMembers()
372 }
373
374 // --- Calls ---
375
376 _callApi () {
377 const api = this.backend.api.call
378 if (!api) throw new Error('Call API not available')
379 return api
380 }
381
382 async _callAuthorize (params) {
383 if (!params.roomId) throw new Error('Missing required param: roomId')
384 await this._callApi().authorize(params.roomId)
385 return { ok: true }
386 }
387
388 async _callDeauthorize (params) {
389 if (!params.roomId) throw new Error('Missing required param: roomId')
390 await this._callApi().deauthorize(params.roomId)
391 return { ok: true }
392 }
393
394 async _callJoinPeer (params) {
395 if (!params.roomId) throw new Error('Missing required param: roomId')
396 if (!params.peerId) throw new Error('Missing required param: peerId')
397 await this._callApi().joinPeer(params.roomId, params.peerId)
398 return { ok: true }
399 }
400
401 async _callLeavePeer (params) {
402 if (!params.roomId) throw new Error('Missing required param: roomId')
403 if (!params.peerId) throw new Error('Missing required param: peerId')
404 await this._callApi().leavePeer(params.roomId, params.peerId)
405 return { ok: true }
406 }
407
408 async _callPresence (params) {
409 if (!params.roomId) throw new Error('Missing required param: roomId')
410 return this._callApi().getPresence(params.roomId)
411 }
412
413 async _callSetPresent (params) {
414 if (!params.roomId) throw new Error('Missing required param: roomId')
415 await this._callApi().setPresent(params.roomId)
416 return { ok: true }
417 }
418
419 async _callSetNotPresent (params) {
420 if (!params.roomId) throw new Error('Missing required param: roomId')
421 await this._callApi().setNotPresent(params.roomId)
422 return { ok: true }
423 }
424
425 async _callGetTracks (params) {
426 if (!params.roomId) throw new Error('Missing required param: roomId')
427 return this._callApi().getAvailableTracks(params.roomId)
428 }
429
430 async _callRequestTrack (params) {
431 if (!params.roomId) throw new Error('Missing required param: roomId')
432 if (!params.peerId) throw new Error('Missing required param: peerId')
433 if (!params.trackKind) throw new Error('Missing required param: trackKind (audio|video|screen)')
434 await this._callApi().requestTrack(params.roomId, params.peerId, params.trackKind)
435 return { ok: true }
436 }
437
438 async _callUnrequestTrack (params) {
439 if (!params.roomId) throw new Error('Missing required param: roomId')
440 if (!params.peerId) throw new Error('Missing required param: peerId')
441 if (!params.trackKind) throw new Error('Missing required param: trackKind')
442 await this._callApi().unrequestTrack(params.roomId, params.peerId, params.trackKind)
443 return { ok: true }
444 }
445
446 async _callShareTrack (params) {
447 if (!params.roomId) throw new Error('Missing required param: roomId')
448 if (!params.peerId) throw new Error('Missing required param: peerId')
449 if (!params.trackKind) throw new Error('Missing required param: trackKind')
450 await this._callApi().shareTrack(params.roomId, params.peerId, params.trackKind, params.trackData || null)
451 return { ok: true }
452 }
453
454 async _callUnshareTrack (params) {
455 if (!params.roomId) throw new Error('Missing required param: roomId')
456 if (!params.peerId) throw new Error('Missing required param: peerId')
457 if (!params.trackKind) throw new Error('Missing required param: trackKind')
458 await this._callApi().unshareTrack(params.roomId, params.peerId, params.trackKind)
459 return { ok: true }
460 }
461
462 async _callSignalMedia (params) {
463 if (!params.roomId) throw new Error('Missing required param: roomId')
464 if (!params.peerId) throw new Error('Missing required param: peerId')
465 if (!params.signal) throw new Error('Missing required param: signal')
466 await this._callApi().signalMedia(params.roomId, params.peerId, params.signal)
467 return { ok: true }
468 }
469
470 async _callRaw (params) {
471 if (!params.fn) throw new Error('Missing required param: fn')
472 const api = this._callApi()
473 const fn = api[params.fn]
474 if (typeof fn !== 'function') throw new Error(`Not a function: api.call.${params.fn}`)
475 const args = params.args || []
476 return fn.apply(api, args)
477 }
478
205479 // --- Raw API passthrough ---
206480
207481 async _raw (params) {