yakmesh 2.8.2 → 3.0.0
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/CHANGELOG.md +637 -0
- package/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +334 -74
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +28 -13
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
package/mesh/katha.js
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KATHA - Kommunication And Threading Handler Architecture
|
|
3
|
+
*
|
|
4
|
+
* Chat layer for Yakmesh providing rich messaging features:
|
|
5
|
+
* - Text messages with formatting
|
|
6
|
+
* - Reactions (emoji responses to messages)
|
|
7
|
+
* - Typing indicators (ephemeral presence)
|
|
8
|
+
* - Reply/threading (message relationships)
|
|
9
|
+
* - Read receipts (optional acknowledgment)
|
|
10
|
+
* - Media embeds (images, GIFs as base64)
|
|
11
|
+
*
|
|
12
|
+
* Etymology: कथा (katha) = story, talk, narrative in Sanskrit
|
|
13
|
+
*
|
|
14
|
+
* SECURITY POLICY (2026-02-11):
|
|
15
|
+
* KATHA REQUIRES ANNEX encryption for ALL message transport.
|
|
16
|
+
* - Direct channels: ANNEX session must be established
|
|
17
|
+
* - GUMBA bundles: Bundle encryption via ANNEX
|
|
18
|
+
* - No plaintext messages permitted on wire
|
|
19
|
+
*
|
|
20
|
+
* @module mesh/katha
|
|
21
|
+
* @license MIT
|
|
22
|
+
* @copyright 2026 YAKMESH™ Contributors
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomBytes } from 'crypto';
|
|
26
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
27
|
+
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// CONFIGURATION
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
export const KATHA_CONFIG = Object.freeze({
|
|
34
|
+
// Message constraints
|
|
35
|
+
maxMessageLength: 4000, // Max text length (like Discord)
|
|
36
|
+
maxReactionEmojis: 20, // Max unique reactions per message
|
|
37
|
+
maxReactionsPerUser: 1, // One reaction per user per message
|
|
38
|
+
maxMediaSize: 10 * 1024 * 1024, // 10MB max media
|
|
39
|
+
|
|
40
|
+
// Typing indicator
|
|
41
|
+
typingTimeout: 5000, // Typing expires after 5 seconds
|
|
42
|
+
typingThrottle: 3000, // Don't send more than once per 3s
|
|
43
|
+
|
|
44
|
+
// Threading
|
|
45
|
+
maxThreadDepth: 1, // Direct replies only (no nested threads)
|
|
46
|
+
maxThreadReplies: 1000, // Max replies to a single message
|
|
47
|
+
|
|
48
|
+
// Read receipts
|
|
49
|
+
receiptBatchInterval: 1000, // Batch receipts every 1s
|
|
50
|
+
maxReceiptBatch: 50, // Max messages in one receipt batch
|
|
51
|
+
|
|
52
|
+
// Message types
|
|
53
|
+
messageTypes: {
|
|
54
|
+
TEXT: 'katha:text',
|
|
55
|
+
REACTION_ADD: 'katha:reaction:add',
|
|
56
|
+
REACTION_REMOVE: 'katha:reaction:remove',
|
|
57
|
+
TYPING_START: 'katha:typing:start',
|
|
58
|
+
TYPING_STOP: 'katha:typing:stop',
|
|
59
|
+
READ_RECEIPT: 'katha:read',
|
|
60
|
+
EDIT: 'katha:edit',
|
|
61
|
+
DELETE: 'katha:delete',
|
|
62
|
+
MEDIA: 'katha:media',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Media types
|
|
66
|
+
mediaTypes: {
|
|
67
|
+
IMAGE: 'image',
|
|
68
|
+
GIF: 'gif',
|
|
69
|
+
FILE: 'file',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
// KATHA MESSAGE - Base chat message
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* KathaMessage - A chat message in the KATHA protocol
|
|
79
|
+
*
|
|
80
|
+
* Immutable once created. Edits create new messages with editOf reference.
|
|
81
|
+
*/
|
|
82
|
+
export class KathaMessage {
|
|
83
|
+
/**
|
|
84
|
+
* @param {Object} options
|
|
85
|
+
* @param {string} options.id - Unique message ID
|
|
86
|
+
* @param {string} options.channelId - Channel/room this message belongs to
|
|
87
|
+
* @param {string} options.senderId - Sender's node ID
|
|
88
|
+
* @param {string} options.content - Message text content
|
|
89
|
+
* @param {string} [options.replyTo] - ID of message being replied to
|
|
90
|
+
* @param {number} [options.timestamp] - Unix timestamp
|
|
91
|
+
*/
|
|
92
|
+
constructor(options = {}) {
|
|
93
|
+
this.id = options.id || KathaMessage.generateId();
|
|
94
|
+
this.type = KATHA_CONFIG.messageTypes.TEXT;
|
|
95
|
+
this.channelId = options.channelId;
|
|
96
|
+
this.senderId = options.senderId;
|
|
97
|
+
this.content = options.content || '';
|
|
98
|
+
this.replyTo = options.replyTo || null;
|
|
99
|
+
this.timestamp = options.timestamp !== undefined ? options.timestamp : Date.now();
|
|
100
|
+
this.editedAt = options.editedAt || null;
|
|
101
|
+
this.reactions = new Map(); // emoji -> Set of userIds
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate a unique message ID
|
|
106
|
+
*/
|
|
107
|
+
static generateId() {
|
|
108
|
+
const timestamp = Date.now().toString(36);
|
|
109
|
+
const random = bytesToHex(randomBytes(8));
|
|
110
|
+
return `${timestamp}-${random}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate message content
|
|
115
|
+
*/
|
|
116
|
+
validate() {
|
|
117
|
+
const errors = [];
|
|
118
|
+
|
|
119
|
+
if (!this.channelId) {
|
|
120
|
+
errors.push('channelId is required');
|
|
121
|
+
}
|
|
122
|
+
if (!this.senderId) {
|
|
123
|
+
errors.push('senderId is required');
|
|
124
|
+
}
|
|
125
|
+
if (typeof this.content !== 'string') {
|
|
126
|
+
errors.push('content must be a string');
|
|
127
|
+
}
|
|
128
|
+
if (this.content.length > KATHA_CONFIG.maxMessageLength) {
|
|
129
|
+
errors.push(`content exceeds max length of ${KATHA_CONFIG.maxMessageLength}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
valid: errors.length === 0,
|
|
134
|
+
errors,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Add a reaction
|
|
140
|
+
* @returns {boolean} true if reaction was added
|
|
141
|
+
*/
|
|
142
|
+
addReaction(emoji, userId) {
|
|
143
|
+
if (!emoji || !userId) return false;
|
|
144
|
+
|
|
145
|
+
// Check limits
|
|
146
|
+
if (this.reactions.size >= KATHA_CONFIG.maxReactionEmojis && !this.reactions.has(emoji)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!this.reactions.has(emoji)) {
|
|
151
|
+
this.reactions.set(emoji, new Set());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const users = this.reactions.get(emoji);
|
|
155
|
+
if (users.has(userId)) return false;
|
|
156
|
+
|
|
157
|
+
users.add(userId);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Remove a reaction
|
|
163
|
+
* @returns {boolean} true if reaction was removed
|
|
164
|
+
*/
|
|
165
|
+
removeReaction(emoji, userId) {
|
|
166
|
+
if (!this.reactions.has(emoji)) return false;
|
|
167
|
+
|
|
168
|
+
const users = this.reactions.get(emoji);
|
|
169
|
+
const removed = users.delete(userId);
|
|
170
|
+
|
|
171
|
+
// Clean up empty reaction sets
|
|
172
|
+
if (users.size === 0) {
|
|
173
|
+
this.reactions.delete(emoji);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return removed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get reaction counts
|
|
181
|
+
*/
|
|
182
|
+
getReactionCounts() {
|
|
183
|
+
const counts = {};
|
|
184
|
+
for (const [emoji, users] of this.reactions) {
|
|
185
|
+
counts[emoji] = users.size;
|
|
186
|
+
}
|
|
187
|
+
return counts;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if user reacted with emoji
|
|
192
|
+
*/
|
|
193
|
+
hasUserReaction(emoji, userId) {
|
|
194
|
+
return this.reactions.has(emoji) && this.reactions.get(emoji).has(userId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Serialize for transmission
|
|
199
|
+
*/
|
|
200
|
+
toJSON() {
|
|
201
|
+
// Convert reactions Map to plain object
|
|
202
|
+
const reactions = {};
|
|
203
|
+
for (const [emoji, users] of this.reactions) {
|
|
204
|
+
reactions[emoji] = Array.from(users);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
id: this.id,
|
|
209
|
+
type: this.type,
|
|
210
|
+
channelId: this.channelId,
|
|
211
|
+
senderId: this.senderId,
|
|
212
|
+
content: this.content,
|
|
213
|
+
replyTo: this.replyTo,
|
|
214
|
+
timestamp: this.timestamp,
|
|
215
|
+
editedAt: this.editedAt,
|
|
216
|
+
reactions,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Deserialize from transmission
|
|
222
|
+
*/
|
|
223
|
+
static fromJSON(json) {
|
|
224
|
+
const msg = new KathaMessage({
|
|
225
|
+
id: json.id,
|
|
226
|
+
channelId: json.channelId,
|
|
227
|
+
senderId: json.senderId,
|
|
228
|
+
content: json.content,
|
|
229
|
+
replyTo: json.replyTo,
|
|
230
|
+
timestamp: json.timestamp,
|
|
231
|
+
editedAt: json.editedAt,
|
|
232
|
+
});
|
|
233
|
+
msg.type = json.type || KATHA_CONFIG.messageTypes.TEXT;
|
|
234
|
+
|
|
235
|
+
// Restore reactions
|
|
236
|
+
if (json.reactions) {
|
|
237
|
+
for (const [emoji, users] of Object.entries(json.reactions)) {
|
|
238
|
+
msg.reactions.set(emoji, new Set(users));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return msg;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
247
|
+
// KATHA REACTION - Reaction event
|
|
248
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* KathaReaction - A reaction add/remove event
|
|
252
|
+
*/
|
|
253
|
+
export class KathaReaction {
|
|
254
|
+
constructor(options = {}) {
|
|
255
|
+
this.type = options.add !== false
|
|
256
|
+
? KATHA_CONFIG.messageTypes.REACTION_ADD
|
|
257
|
+
: KATHA_CONFIG.messageTypes.REACTION_REMOVE;
|
|
258
|
+
this.messageId = options.messageId;
|
|
259
|
+
this.channelId = options.channelId;
|
|
260
|
+
this.userId = options.userId;
|
|
261
|
+
this.emoji = options.emoji;
|
|
262
|
+
this.timestamp = options.timestamp || Date.now();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
validate() {
|
|
266
|
+
const errors = [];
|
|
267
|
+
if (!this.messageId) errors.push('messageId is required');
|
|
268
|
+
if (!this.channelId) errors.push('channelId is required');
|
|
269
|
+
if (!this.userId) errors.push('userId is required');
|
|
270
|
+
if (!this.emoji) errors.push('emoji is required');
|
|
271
|
+
// Basic emoji validation (single grapheme cluster or shortcode)
|
|
272
|
+
if (this.emoji && this.emoji.length > 32) errors.push('emoji too long');
|
|
273
|
+
|
|
274
|
+
return { valid: errors.length === 0, errors };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
toJSON() {
|
|
278
|
+
return {
|
|
279
|
+
type: this.type,
|
|
280
|
+
messageId: this.messageId,
|
|
281
|
+
channelId: this.channelId,
|
|
282
|
+
userId: this.userId,
|
|
283
|
+
emoji: this.emoji,
|
|
284
|
+
timestamp: this.timestamp,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static fromJSON(json) {
|
|
289
|
+
return new KathaReaction({
|
|
290
|
+
add: json.type === KATHA_CONFIG.messageTypes.REACTION_ADD,
|
|
291
|
+
messageId: json.messageId,
|
|
292
|
+
channelId: json.channelId,
|
|
293
|
+
userId: json.userId,
|
|
294
|
+
emoji: json.emoji,
|
|
295
|
+
timestamp: json.timestamp,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
301
|
+
// KATHA TYPING - Typing indicator
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* KathaTyping - Typing indicator event (ephemeral, not stored)
|
|
306
|
+
*/
|
|
307
|
+
export class KathaTyping {
|
|
308
|
+
constructor(options = {}) {
|
|
309
|
+
this.type = options.stop
|
|
310
|
+
? KATHA_CONFIG.messageTypes.TYPING_STOP
|
|
311
|
+
: KATHA_CONFIG.messageTypes.TYPING_START;
|
|
312
|
+
this.channelId = options.channelId;
|
|
313
|
+
this.userId = options.userId;
|
|
314
|
+
this.timestamp = options.timestamp || Date.now();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if this typing indicator has expired
|
|
319
|
+
*/
|
|
320
|
+
isExpired() {
|
|
321
|
+
return Date.now() - this.timestamp > KATHA_CONFIG.typingTimeout;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
toJSON() {
|
|
325
|
+
return {
|
|
326
|
+
type: this.type,
|
|
327
|
+
channelId: this.channelId,
|
|
328
|
+
userId: this.userId,
|
|
329
|
+
timestamp: this.timestamp,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static fromJSON(json) {
|
|
334
|
+
return new KathaTyping({
|
|
335
|
+
stop: json.type === KATHA_CONFIG.messageTypes.TYPING_STOP,
|
|
336
|
+
channelId: json.channelId,
|
|
337
|
+
userId: json.userId,
|
|
338
|
+
timestamp: json.timestamp,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
344
|
+
// KATHA READ RECEIPT - Read acknowledgment
|
|
345
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* KathaReadReceipt - Batch read receipt
|
|
349
|
+
*/
|
|
350
|
+
export class KathaReadReceipt {
|
|
351
|
+
constructor(options = {}) {
|
|
352
|
+
this.type = KATHA_CONFIG.messageTypes.READ_RECEIPT;
|
|
353
|
+
this.channelId = options.channelId;
|
|
354
|
+
this.userId = options.userId;
|
|
355
|
+
this.messageIds = options.messageIds || [];
|
|
356
|
+
this.lastReadId = options.lastReadId || null; // Alternative: just track last read
|
|
357
|
+
this.timestamp = options.timestamp || Date.now();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Add a message ID to the receipt batch
|
|
362
|
+
*/
|
|
363
|
+
addMessage(messageId) {
|
|
364
|
+
if (this.messageIds.length >= KATHA_CONFIG.maxReceiptBatch) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (!this.messageIds.includes(messageId)) {
|
|
368
|
+
this.messageIds.push(messageId);
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
toJSON() {
|
|
374
|
+
return {
|
|
375
|
+
type: this.type,
|
|
376
|
+
channelId: this.channelId,
|
|
377
|
+
userId: this.userId,
|
|
378
|
+
messageIds: this.messageIds,
|
|
379
|
+
lastReadId: this.lastReadId,
|
|
380
|
+
timestamp: this.timestamp,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static fromJSON(json) {
|
|
385
|
+
return new KathaReadReceipt({
|
|
386
|
+
channelId: json.channelId,
|
|
387
|
+
userId: json.userId,
|
|
388
|
+
messageIds: json.messageIds,
|
|
389
|
+
lastReadId: json.lastReadId,
|
|
390
|
+
timestamp: json.timestamp,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
396
|
+
// KATHA MEDIA - Image/GIF/File embed
|
|
397
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* KathaMedia - Media attachment
|
|
401
|
+
*/
|
|
402
|
+
export class KathaMedia {
|
|
403
|
+
constructor(options = {}) {
|
|
404
|
+
this.id = options.id || KathaMessage.generateId();
|
|
405
|
+
this.type = KATHA_CONFIG.messageTypes.MEDIA;
|
|
406
|
+
this.mediaType = options.mediaType || KATHA_CONFIG.mediaTypes.IMAGE;
|
|
407
|
+
this.channelId = options.channelId;
|
|
408
|
+
this.senderId = options.senderId;
|
|
409
|
+
this.filename = options.filename || null;
|
|
410
|
+
this.mimeType = options.mimeType || 'application/octet-stream';
|
|
411
|
+
this.size = options.size || 0;
|
|
412
|
+
this.data = options.data || null; // Base64 encoded
|
|
413
|
+
this.hash = options.hash || null; // SHA3-256 of raw data for integrity
|
|
414
|
+
this.caption = options.caption || null;
|
|
415
|
+
this.replyTo = options.replyTo || null;
|
|
416
|
+
this.timestamp = options.timestamp || Date.now();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create from a buffer
|
|
421
|
+
*/
|
|
422
|
+
static fromBuffer(buffer, options = {}) {
|
|
423
|
+
const base64 = buffer.toString('base64');
|
|
424
|
+
const hash = bytesToHex(sha3_256(buffer));
|
|
425
|
+
|
|
426
|
+
return new KathaMedia({
|
|
427
|
+
...options,
|
|
428
|
+
data: base64,
|
|
429
|
+
size: buffer.length,
|
|
430
|
+
hash,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get the raw buffer
|
|
436
|
+
*/
|
|
437
|
+
toBuffer() {
|
|
438
|
+
if (!this.data) return null;
|
|
439
|
+
return Buffer.from(this.data, 'base64');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Verify data integrity
|
|
444
|
+
*/
|
|
445
|
+
verify() {
|
|
446
|
+
if (!this.data || !this.hash) return false;
|
|
447
|
+
const buffer = this.toBuffer();
|
|
448
|
+
const computed = bytesToHex(sha3_256(buffer));
|
|
449
|
+
return computed === this.hash;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
validate() {
|
|
453
|
+
const errors = [];
|
|
454
|
+
if (!this.channelId) errors.push('channelId is required');
|
|
455
|
+
if (!this.senderId) errors.push('senderId is required');
|
|
456
|
+
if (!this.data) errors.push('data is required');
|
|
457
|
+
if (this.size > KATHA_CONFIG.maxMediaSize) {
|
|
458
|
+
errors.push(`size exceeds max of ${KATHA_CONFIG.maxMediaSize} bytes`);
|
|
459
|
+
}
|
|
460
|
+
if (!Object.values(KATHA_CONFIG.mediaTypes).includes(this.mediaType)) {
|
|
461
|
+
errors.push('invalid mediaType');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { valid: errors.length === 0, errors };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
toJSON() {
|
|
468
|
+
return {
|
|
469
|
+
id: this.id,
|
|
470
|
+
type: this.type,
|
|
471
|
+
mediaType: this.mediaType,
|
|
472
|
+
channelId: this.channelId,
|
|
473
|
+
senderId: this.senderId,
|
|
474
|
+
filename: this.filename,
|
|
475
|
+
mimeType: this.mimeType,
|
|
476
|
+
size: this.size,
|
|
477
|
+
data: this.data,
|
|
478
|
+
hash: this.hash,
|
|
479
|
+
caption: this.caption,
|
|
480
|
+
replyTo: this.replyTo,
|
|
481
|
+
timestamp: this.timestamp,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
static fromJSON(json) {
|
|
486
|
+
return new KathaMedia({
|
|
487
|
+
id: json.id,
|
|
488
|
+
mediaType: json.mediaType,
|
|
489
|
+
channelId: json.channelId,
|
|
490
|
+
senderId: json.senderId,
|
|
491
|
+
filename: json.filename,
|
|
492
|
+
mimeType: json.mimeType,
|
|
493
|
+
size: json.size,
|
|
494
|
+
data: json.data,
|
|
495
|
+
hash: json.hash,
|
|
496
|
+
caption: json.caption,
|
|
497
|
+
replyTo: json.replyTo,
|
|
498
|
+
timestamp: json.timestamp,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
504
|
+
// KATHA EDIT - Message edit
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* KathaEdit - Message edit event
|
|
509
|
+
*/
|
|
510
|
+
export class KathaEdit {
|
|
511
|
+
constructor(options = {}) {
|
|
512
|
+
this.type = KATHA_CONFIG.messageTypes.EDIT;
|
|
513
|
+
this.messageId = options.messageId;
|
|
514
|
+
this.channelId = options.channelId;
|
|
515
|
+
this.userId = options.userId;
|
|
516
|
+
this.newContent = options.newContent;
|
|
517
|
+
this.timestamp = options.timestamp || Date.now();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
validate() {
|
|
521
|
+
const errors = [];
|
|
522
|
+
if (!this.messageId) errors.push('messageId is required');
|
|
523
|
+
if (!this.channelId) errors.push('channelId is required');
|
|
524
|
+
if (!this.userId) errors.push('userId is required');
|
|
525
|
+
if (typeof this.newContent !== 'string') errors.push('newContent must be a string');
|
|
526
|
+
if (this.newContent && this.newContent.length > KATHA_CONFIG.maxMessageLength) {
|
|
527
|
+
errors.push(`newContent exceeds max length of ${KATHA_CONFIG.maxMessageLength}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return { valid: errors.length === 0, errors };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
toJSON() {
|
|
534
|
+
return {
|
|
535
|
+
type: this.type,
|
|
536
|
+
messageId: this.messageId,
|
|
537
|
+
channelId: this.channelId,
|
|
538
|
+
userId: this.userId,
|
|
539
|
+
newContent: this.newContent,
|
|
540
|
+
timestamp: this.timestamp,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
static fromJSON(json) {
|
|
545
|
+
return new KathaEdit({
|
|
546
|
+
messageId: json.messageId,
|
|
547
|
+
channelId: json.channelId,
|
|
548
|
+
userId: json.userId,
|
|
549
|
+
newContent: json.newContent,
|
|
550
|
+
timestamp: json.timestamp,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
556
|
+
// KATHA DELETE - Message deletion
|
|
557
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* KathaDelete - Message deletion event
|
|
561
|
+
*/
|
|
562
|
+
export class KathaDelete {
|
|
563
|
+
constructor(options = {}) {
|
|
564
|
+
this.type = KATHA_CONFIG.messageTypes.DELETE;
|
|
565
|
+
this.messageId = options.messageId;
|
|
566
|
+
this.channelId = options.channelId;
|
|
567
|
+
this.userId = options.userId;
|
|
568
|
+
this.timestamp = options.timestamp || Date.now();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
validate() {
|
|
572
|
+
const errors = [];
|
|
573
|
+
if (!this.messageId) errors.push('messageId is required');
|
|
574
|
+
if (!this.channelId) errors.push('channelId is required');
|
|
575
|
+
if (!this.userId) errors.push('userId is required');
|
|
576
|
+
|
|
577
|
+
return { valid: errors.length === 0, errors };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
toJSON() {
|
|
581
|
+
return {
|
|
582
|
+
type: this.type,
|
|
583
|
+
messageId: this.messageId,
|
|
584
|
+
channelId: this.channelId,
|
|
585
|
+
userId: this.userId,
|
|
586
|
+
timestamp: this.timestamp,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
static fromJSON(json) {
|
|
591
|
+
return new KathaDelete({
|
|
592
|
+
messageId: json.messageId,
|
|
593
|
+
channelId: json.channelId,
|
|
594
|
+
userId: json.userId,
|
|
595
|
+
timestamp: json.timestamp,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
601
|
+
// KATHA CHANNEL - Channel/room state manager
|
|
602
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* KathaChannel - Manages chat state for a channel
|
|
606
|
+
*/
|
|
607
|
+
export class KathaChannel {
|
|
608
|
+
constructor(channelId) {
|
|
609
|
+
this.channelId = channelId;
|
|
610
|
+
this.messages = new Map(); // id -> KathaMessage
|
|
611
|
+
this.threads = new Map(); // parentId -> Set of replyIds
|
|
612
|
+
this.typing = new Map(); // userId -> timestamp
|
|
613
|
+
this.readReceipts = new Map(); // userId -> lastReadId
|
|
614
|
+
this._typingCleanupInterval = null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Start typing indicator cleanup
|
|
619
|
+
*/
|
|
620
|
+
startTypingCleanup() {
|
|
621
|
+
if (this._typingCleanupInterval) return;
|
|
622
|
+
|
|
623
|
+
this._typingCleanupInterval = setInterval(() => {
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
for (const [userId, timestamp] of this.typing) {
|
|
626
|
+
if (now - timestamp > KATHA_CONFIG.typingTimeout) {
|
|
627
|
+
this.typing.delete(userId);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}, 1000);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Stop cleanup interval
|
|
635
|
+
*/
|
|
636
|
+
stopTypingCleanup() {
|
|
637
|
+
if (this._typingCleanupInterval) {
|
|
638
|
+
clearInterval(this._typingCleanupInterval);
|
|
639
|
+
this._typingCleanupInterval = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Add a message
|
|
645
|
+
*/
|
|
646
|
+
addMessage(message) {
|
|
647
|
+
if (!(message instanceof KathaMessage)) {
|
|
648
|
+
message = KathaMessage.fromJSON(message);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
this.messages.set(message.id, message);
|
|
652
|
+
|
|
653
|
+
// Track threading
|
|
654
|
+
if (message.replyTo) {
|
|
655
|
+
if (!this.threads.has(message.replyTo)) {
|
|
656
|
+
this.threads.set(message.replyTo, new Set());
|
|
657
|
+
}
|
|
658
|
+
this.threads.get(message.replyTo).add(message.id);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return message;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Get a message
|
|
666
|
+
*/
|
|
667
|
+
getMessage(messageId) {
|
|
668
|
+
return this.messages.get(messageId) || null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Get thread replies
|
|
673
|
+
*/
|
|
674
|
+
getThreadReplies(messageId) {
|
|
675
|
+
const replyIds = this.threads.get(messageId);
|
|
676
|
+
if (!replyIds) return [];
|
|
677
|
+
|
|
678
|
+
return Array.from(replyIds)
|
|
679
|
+
.map(id => this.messages.get(id))
|
|
680
|
+
.filter(Boolean)
|
|
681
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Apply a reaction event
|
|
686
|
+
*/
|
|
687
|
+
applyReaction(reaction) {
|
|
688
|
+
const message = this.messages.get(reaction.messageId);
|
|
689
|
+
if (!message) return false;
|
|
690
|
+
|
|
691
|
+
if (reaction.type === KATHA_CONFIG.messageTypes.REACTION_ADD) {
|
|
692
|
+
return message.addReaction(reaction.emoji, reaction.userId);
|
|
693
|
+
} else {
|
|
694
|
+
return message.removeReaction(reaction.emoji, reaction.userId);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Set typing indicator
|
|
700
|
+
*/
|
|
701
|
+
setTyping(userId, isTyping = true) {
|
|
702
|
+
if (isTyping) {
|
|
703
|
+
this.typing.set(userId, Date.now());
|
|
704
|
+
} else {
|
|
705
|
+
this.typing.delete(userId);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Get users currently typing
|
|
711
|
+
*/
|
|
712
|
+
getTypingUsers() {
|
|
713
|
+
const now = Date.now();
|
|
714
|
+
const users = [];
|
|
715
|
+
|
|
716
|
+
for (const [userId, timestamp] of this.typing) {
|
|
717
|
+
if (now - timestamp <= KATHA_CONFIG.typingTimeout) {
|
|
718
|
+
users.push(userId);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return users;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Apply read receipt
|
|
727
|
+
*/
|
|
728
|
+
applyReadReceipt(receipt) {
|
|
729
|
+
this.readReceipts.set(receipt.userId, receipt.lastReadId || receipt.messageIds[receipt.messageIds.length - 1]);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Get read status for a message
|
|
734
|
+
*/
|
|
735
|
+
getReadBy(messageId) {
|
|
736
|
+
const readers = [];
|
|
737
|
+
|
|
738
|
+
for (const [userId, lastReadId] of this.readReceipts) {
|
|
739
|
+
const lastRead = this.messages.get(lastReadId);
|
|
740
|
+
const target = this.messages.get(messageId);
|
|
741
|
+
|
|
742
|
+
if (lastRead && target && lastRead.timestamp >= target.timestamp) {
|
|
743
|
+
readers.push(userId);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return readers;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Edit a message
|
|
752
|
+
*/
|
|
753
|
+
editMessage(edit) {
|
|
754
|
+
const message = this.messages.get(edit.messageId);
|
|
755
|
+
if (!message) return null;
|
|
756
|
+
|
|
757
|
+
// Only sender can edit
|
|
758
|
+
if (message.senderId !== edit.userId) return null;
|
|
759
|
+
|
|
760
|
+
message.content = edit.newContent;
|
|
761
|
+
message.editedAt = edit.timestamp;
|
|
762
|
+
|
|
763
|
+
return message;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Delete a message
|
|
768
|
+
*/
|
|
769
|
+
deleteMessage(deletion) {
|
|
770
|
+
const message = this.messages.get(deletion.messageId);
|
|
771
|
+
if (!message) return false;
|
|
772
|
+
|
|
773
|
+
// Only sender can delete
|
|
774
|
+
if (message.senderId !== deletion.userId) return false;
|
|
775
|
+
|
|
776
|
+
// Remove from threads
|
|
777
|
+
if (message.replyTo) {
|
|
778
|
+
const thread = this.threads.get(message.replyTo);
|
|
779
|
+
if (thread) thread.delete(message.id);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Don't delete parent if it has replies - just mark content as deleted
|
|
783
|
+
if (this.threads.has(message.id) && this.threads.get(message.id).size > 0) {
|
|
784
|
+
message.content = '[deleted]';
|
|
785
|
+
message.editedAt = deletion.timestamp;
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
this.messages.delete(deletion.messageId);
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Get messages in time order
|
|
795
|
+
*/
|
|
796
|
+
getMessages(options = {}) {
|
|
797
|
+
const { limit = 50, before, after, threadOnly } = options;
|
|
798
|
+
|
|
799
|
+
let msgs = Array.from(this.messages.values());
|
|
800
|
+
|
|
801
|
+
// Filter by thread
|
|
802
|
+
if (threadOnly !== undefined) {
|
|
803
|
+
if (threadOnly) {
|
|
804
|
+
msgs = msgs.filter(m => m.replyTo !== null);
|
|
805
|
+
} else {
|
|
806
|
+
msgs = msgs.filter(m => m.replyTo === null);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Filter by time
|
|
811
|
+
if (before) {
|
|
812
|
+
msgs = msgs.filter(m => m.timestamp < before);
|
|
813
|
+
}
|
|
814
|
+
if (after) {
|
|
815
|
+
msgs = msgs.filter(m => m.timestamp > after);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Sort by timestamp
|
|
819
|
+
msgs.sort((a, b) => a.timestamp - b.timestamp);
|
|
820
|
+
|
|
821
|
+
// Apply limit
|
|
822
|
+
if (limit) {
|
|
823
|
+
msgs = msgs.slice(-limit);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return msgs;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Get channel stats
|
|
831
|
+
*/
|
|
832
|
+
getStats() {
|
|
833
|
+
return {
|
|
834
|
+
channelId: this.channelId,
|
|
835
|
+
messageCount: this.messages.size,
|
|
836
|
+
threadCount: this.threads.size,
|
|
837
|
+
typingUsers: this.getTypingUsers().length,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
843
|
+
// KATHA HUB - Multi-channel manager
|
|
844
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* KathaHub - Manages multiple chat channels
|
|
848
|
+
*/
|
|
849
|
+
export class KathaHub {
|
|
850
|
+
constructor() {
|
|
851
|
+
this.channels = new Map();
|
|
852
|
+
this.eventHandlers = new Map();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Get or create a channel
|
|
857
|
+
*/
|
|
858
|
+
getChannel(channelId) {
|
|
859
|
+
if (!this.channels.has(channelId)) {
|
|
860
|
+
const channel = new KathaChannel(channelId);
|
|
861
|
+
channel.startTypingCleanup();
|
|
862
|
+
this.channels.set(channelId, channel);
|
|
863
|
+
}
|
|
864
|
+
return this.channels.get(channelId);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Remove a channel
|
|
869
|
+
*/
|
|
870
|
+
removeChannel(channelId) {
|
|
871
|
+
const channel = this.channels.get(channelId);
|
|
872
|
+
if (channel) {
|
|
873
|
+
channel.stopTypingCleanup();
|
|
874
|
+
this.channels.delete(channelId);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Handle incoming KATHA event
|
|
880
|
+
*/
|
|
881
|
+
handleEvent(event) {
|
|
882
|
+
const channelId = event.channelId;
|
|
883
|
+
if (!channelId) return null;
|
|
884
|
+
|
|
885
|
+
const channel = this.getChannel(channelId);
|
|
886
|
+
let result = null;
|
|
887
|
+
|
|
888
|
+
switch (event.type) {
|
|
889
|
+
case KATHA_CONFIG.messageTypes.TEXT:
|
|
890
|
+
result = channel.addMessage(KathaMessage.fromJSON(event));
|
|
891
|
+
break;
|
|
892
|
+
|
|
893
|
+
case KATHA_CONFIG.messageTypes.MEDIA:
|
|
894
|
+
result = channel.addMessage(KathaMedia.fromJSON(event));
|
|
895
|
+
break;
|
|
896
|
+
|
|
897
|
+
case KATHA_CONFIG.messageTypes.REACTION_ADD:
|
|
898
|
+
case KATHA_CONFIG.messageTypes.REACTION_REMOVE:
|
|
899
|
+
result = channel.applyReaction(KathaReaction.fromJSON(event));
|
|
900
|
+
break;
|
|
901
|
+
|
|
902
|
+
case KATHA_CONFIG.messageTypes.TYPING_START:
|
|
903
|
+
channel.setTyping(event.userId, true);
|
|
904
|
+
result = true;
|
|
905
|
+
break;
|
|
906
|
+
|
|
907
|
+
case KATHA_CONFIG.messageTypes.TYPING_STOP:
|
|
908
|
+
channel.setTyping(event.userId, false);
|
|
909
|
+
result = true;
|
|
910
|
+
break;
|
|
911
|
+
|
|
912
|
+
case KATHA_CONFIG.messageTypes.READ_RECEIPT:
|
|
913
|
+
channel.applyReadReceipt(KathaReadReceipt.fromJSON(event));
|
|
914
|
+
result = true;
|
|
915
|
+
break;
|
|
916
|
+
|
|
917
|
+
case KATHA_CONFIG.messageTypes.EDIT:
|
|
918
|
+
result = channel.editMessage(KathaEdit.fromJSON(event));
|
|
919
|
+
break;
|
|
920
|
+
|
|
921
|
+
case KATHA_CONFIG.messageTypes.DELETE:
|
|
922
|
+
result = channel.deleteMessage(KathaDelete.fromJSON(event));
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Emit event
|
|
927
|
+
this._emit(event.type, event, result);
|
|
928
|
+
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Register event handler
|
|
934
|
+
*/
|
|
935
|
+
on(eventType, handler) {
|
|
936
|
+
if (!this.eventHandlers.has(eventType)) {
|
|
937
|
+
this.eventHandlers.set(eventType, []);
|
|
938
|
+
}
|
|
939
|
+
this.eventHandlers.get(eventType).push(handler);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Remove event handler
|
|
944
|
+
*/
|
|
945
|
+
off(eventType, handler) {
|
|
946
|
+
const handlers = this.eventHandlers.get(eventType);
|
|
947
|
+
if (handlers) {
|
|
948
|
+
const idx = handlers.indexOf(handler);
|
|
949
|
+
if (idx >= 0) handlers.splice(idx, 1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Emit event to handlers
|
|
955
|
+
*/
|
|
956
|
+
_emit(eventType, event, result) {
|
|
957
|
+
const handlers = this.eventHandlers.get(eventType) || [];
|
|
958
|
+
for (const handler of handlers) {
|
|
959
|
+
try {
|
|
960
|
+
handler(event, result);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
console.error('Katha handler error:', err);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Cleanup all channels
|
|
969
|
+
*/
|
|
970
|
+
cleanup() {
|
|
971
|
+
for (const channel of this.channels.values()) {
|
|
972
|
+
channel.stopTypingCleanup();
|
|
973
|
+
}
|
|
974
|
+
this.channels.clear();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Get hub stats
|
|
979
|
+
*/
|
|
980
|
+
getStats() {
|
|
981
|
+
const stats = {
|
|
982
|
+
channelCount: this.channels.size,
|
|
983
|
+
totalMessages: 0,
|
|
984
|
+
channels: {},
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
for (const [id, channel] of this.channels) {
|
|
988
|
+
const channelStats = channel.getStats();
|
|
989
|
+
stats.totalMessages += channelStats.messageCount;
|
|
990
|
+
stats.channels[id] = channelStats;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return stats;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
998
|
+
// EXPORTS
|
|
999
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1000
|
+
|
|
1001
|
+
export default {
|
|
1002
|
+
KATHA_CONFIG,
|
|
1003
|
+
KathaMessage,
|
|
1004
|
+
KathaReaction,
|
|
1005
|
+
KathaTyping,
|
|
1006
|
+
KathaReadReceipt,
|
|
1007
|
+
KathaMedia,
|
|
1008
|
+
KathaEdit,
|
|
1009
|
+
KathaDelete,
|
|
1010
|
+
KathaChannel,
|
|
1011
|
+
KathaHub,
|
|
1012
|
+
};
|