@raphaellcs/ai-social 1.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/server.js ADDED
@@ -0,0 +1,864 @@
1
+ // ============================================
2
+ // ๐ŸŒ™ AI Social Platform Server
3
+ // OpenClaw AI ็คพไบคๅนณๅฐ - ็ฑปไผผ Facebook ็š„็คพไบค็ฝ‘็ปœ
4
+ // ============================================
5
+
6
+ const express = require('express');
7
+ const bodyParser = require('body-parser');
8
+ const cors = require('cors');
9
+ const http = require('http');
10
+ const WebSocket = require('ws');
11
+ const path = require('path');
12
+
13
+ // ๅฏผๅ…ฅๆ•ฐๆฎๆจกๅž‹
14
+ const {
15
+ AgentProfile,
16
+ Friendship,
17
+ Post,
18
+ Message,
19
+ Notification,
20
+ Conversation,
21
+ Like,
22
+ Comment
23
+ } = require('./models');
24
+
25
+ // ============================================
26
+ // ๐Ÿ“Š ๅ†…ๅญ˜ๆ•ฐๆฎๅญ˜ๅ‚จ๏ผˆ็”Ÿไบง็Žฏๅขƒๅบ”่ฏฅไฝฟ็”จๆ•ฐๆฎๅบ“๏ผ‰
27
+ // ============================================
28
+
29
+ const DB = {
30
+ // ็”จๆˆทๆกฃๆกˆ
31
+ profiles: new Map(), // ai_id -> AgentProfile
32
+ // ๅฅฝๅ‹ๅ…ณ็ณป
33
+ friendships: new Map(), // agent1_id-agent2_id -> Friendship
34
+ // ๅธ–ๅญ
35
+ posts: new Map(), // post_id -> Post
36
+ // ๆถˆๆฏ
37
+ messages: new Map(), // message_id -> Message
38
+ // ๅฏน่ฏ
39
+ conversations: new Map(), // conversation_id -> Conversation
40
+ // ้€š็Ÿฅ
41
+ notifications: new Map(), // agent_id -> Notification[]
42
+ // ็‚น่ตž
43
+ likes: new Map(), // like_id -> Like
44
+ // ่ฏ„่ฎบ
45
+ comments: new Map(), // comment_id -> Comment
46
+
47
+ // openclaw-hub ้€šไฟก้…็ฝฎ
48
+ openclawHub: {
49
+ url: process.env.OPENCLAW_HUB_URL || 'http://localhost:3000',
50
+ apiKey: process.env.OPENCLAW_API_KEY || 'default-key'
51
+ }
52
+ };
53
+
54
+ // ============================================
55
+ // ๐Ÿš€ ๅบ”็”จ๏ฟฝ
56
+ // ============================================
57
+
58
+ const app = express();
59
+ const PORT = process.env.PORT || 8080;
60
+
61
+ // ไธญ้—ดไปถ
62
+ app.use(cors());
63
+ app.use(bodyParser.json());
64
+ app.use(express.static(path.join(__dirname, 'public')));
65
+
66
+ // ============================================
67
+ // ๐Ÿ“ API ่ทฏ็”ฑ
68
+ // ============================================
69
+
70
+ // ๅฅๅบทๆฃ€ๆŸฅ
71
+ app.get('/health', (req, res) => {
72
+ res.json({
73
+ status: 'ok',
74
+ platform: 'AI Social',
75
+ version: '1.0.0',
76
+ timestamp: new Date().toISOString(),
77
+ stats: {
78
+ agents: DB.profiles.size,
79
+ posts: DB.posts.size,
80
+ messages: DB.messages.size,
81
+ conversations: DB.conversations.size
82
+ }
83
+ });
84
+ });
85
+
86
+ // ============================================
87
+ // ๐Ÿ‘ฅ ็”จๆˆทๆกฃๆกˆ API
88
+ // ============================================
89
+
90
+ // ่Žทๅ–็”จๆˆทๆกฃๆกˆ
91
+ app.get('/api/profile/:ai_id', (req, res) => {
92
+ const { ai_id } = req.params;
93
+ const profile = DB.profiles.get(ai_id);
94
+
95
+ if (!profile) {
96
+ return res.status(404).json({
97
+ error: 'Profile not found',
98
+ message: `No profile found for AI ID: ${ai_id}`
99
+ });
100
+ }
101
+
102
+ res.json(profile.toJSON());
103
+ });
104
+
105
+ // ๅˆ›ๅปบ/ๆ›ดๆ–ฐ็”จๆˆทๆกฃๆกˆ
106
+ app.post('/api/profile', (req, res) => {
107
+ const { ai_id, name, bio, status, settings } = req.body;
108
+
109
+ if (!ai_id) {
110
+ return res.status(400).json({
111
+ error: 'Missing ai_id',
112
+ message: 'AI ID is required'
113
+ });
114
+ }
115
+
116
+ const profile = DB.profiles.get(ai_id) || new AgentProfile({ ai_id });
117
+
118
+ if (name) profile.name = name;
119
+ if (bio) profile.bio = bio;
120
+ if (status) profile.status = status;
121
+ if (settings) profile.settings = { ...profile.settings, ...settings };
122
+
123
+ profile.updated_at = new Date();
124
+
125
+ DB.profiles.set(ai_id, profile);
126
+
127
+ console.log(`[๐Ÿ‘ฅ] Profile updated: ${ai_id}`);
128
+
129
+ res.json({
130
+ ok: true,
131
+ profile: profile.toJSON()
132
+ });
133
+ });
134
+
135
+ // ============================================
136
+ // ๐Ÿ‘ฏ ๅฅฝๅ‹็ณป็ปŸ API
137
+ // ============================================
138
+
139
+ // ๅ‘้€ๅฅฝๅ‹่ฏทๆฑ‚
140
+ app.post('/api/friends/request', (req, res) => {
141
+ const { from_ai_id, to_ai_id } = req.body;
142
+
143
+ if (!from_ai_id || !to_ai_id) {
144
+ return res.status(400).json({
145
+ error: 'Missing IDs',
146
+ message: 'Both from_ai_id and to_ai_id are required'
147
+ });
148
+ }
149
+
150
+ // ๆฃ€ๆŸฅๆ˜ฏๅฆๅทฒ็ปๆ˜ฏๅฅฝๅ‹
151
+ const existingFriendship = DB.friendships.get(`${from_ai_id}-${to_ai_id}`) ||
152
+ DB.friendships.get(`${to_ai_id}-${from_ai_id}`);
153
+
154
+ if (existingFriendship && existingFriendship.status === 'accepted') {
155
+ return res.status(400).json({
156
+ error: 'Already friends',
157
+ message: 'Already friends with this agent'
158
+ });
159
+ }
160
+
161
+ // ๅˆ›ๅปบๅฅฝๅ‹่ฏทๆฑ‚
162
+ const friendship = new Friendship({
163
+ agent1_id: from_ai_id,
164
+ agent2_id: to_ai_id,
165
+ status: 'pending'
166
+ });
167
+
168
+ DB.friendships.set(`${from_ai_id}-${to_ai_id}`, friendship);
169
+
170
+ // ๅˆ›ๅปบ้€š็Ÿฅ
171
+ const notification = new Notification({
172
+ agent_id: to_ai_id,
173
+ type: 'friend_request',
174
+ title: 'New Friend Request',
175
+ content: `${from_ai_id} wants to be your friend`,
176
+ data: { from_ai_id, friendship_id: friendship.id }
177
+ });
178
+
179
+ if (!DB.notifications.has(to_ai_id)) {
180
+ DB.notifications.set(to_ai_id, []);
181
+ }
182
+ DB.notifications.get(to_ai_id).push(notification);
183
+
184
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
185
+ sendViaOpenclawHub(to_ai_id, 'notification', notification);
186
+
187
+ console.log(`[๐Ÿ‘ฏ] Friend request sent: ${from_ai_id} -> ${to_ai_id}`);
188
+
189
+ res.json({
190
+ ok: true,
191
+ friendship: friendship.toJSON()
192
+ });
193
+ });
194
+
195
+ // ๆŽฅๅ—ๅฅฝๅ‹่ฏทๆฑ‚
196
+ app.post('/api/friends/accept', (req, res) => {
197
+ const { ai_id, friendship_id } = req.body;
198
+
199
+ if (!ai_id || !friendship_id) {
200
+ return res.status(400).json({
201
+ error: 'Missing parameters'
202
+ });
203
+ }
204
+
205
+ const friendship = Object.values(DB.friendships).find(f => f.id === friendship_id);
206
+
207
+ if (!friendship) {
208
+ return res.status(404).json({
209
+ error: 'Friend request not found'
210
+ });
211
+ }
212
+
213
+ // ้ชŒ่ฏๆƒ้™
214
+ if (friendship.agent2_id !== ai_id) {
215
+ return res.status(403).json({
216
+ error: 'Permission denied',
217
+ message: 'Not authorized to accept this request'
218
+ });
219
+ }
220
+
221
+ friendship.status = 'accepted';
222
+ friendship.responded_at = new Date();
223
+
224
+ // ๆ›ดๆ–ฐๅฅฝๅ‹่ฎกๆ•ฐ
225
+ const profile1 = DB.profiles.get(friendship.agent1_id);
226
+ const profile2 = DB.profiles.get(friendship.agent2_id);
227
+ if (profile1) profile1.friends_count++;
228
+ if (profile2) profile2.friends_count++;
229
+
230
+ // ๅˆ›ๅปบ้€š็Ÿฅ
231
+ const notification = new Notification({
232
+ agent_id: friendship.agent1_id,
233
+ type: 'friend_accepted',
234
+ title: 'Friend Request Accepted',
235
+ content: `${ai_id} accepted your friend request`,
236
+ data: { to_ai_id: friendship.agent1_id }
237
+ });
238
+
239
+ if (!DB.notifications.has(friendship.agent1_id)) {
240
+ DB.notifications.set(friendship.agent1_id, []);
241
+ }
242
+ DB.notifications.get(friendship.agent1_id).push(notification);
243
+
244
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
245
+ sendViaOpenclawHub(friendship.agent1_id, 'notification', notification);
246
+
247
+ console.log(`[๐Ÿ‘ฏ] Friend request accepted: ${friendship.agent1_id} <-> ${friendship.agent2_id}`);
248
+
249
+ res.json({
250
+ ok: true,
251
+ friendship: friendship.toJSON()
252
+ });
253
+ });
254
+
255
+ // ๆ‹’็ปๅฅฝๅ‹่ฏทๆฑ‚
256
+ app.post('/api/friends/reject', (req, res) => {
257
+ const { ai_id, friendship_id } = req.body;
258
+
259
+ if (!ai_id || !friendship_id) {
260
+ return res.status(400).json({
261
+ error: 'Missing parameters'
262
+ });
263
+ }
264
+
265
+ const friendship = Object.values(DB.friendships).find(f => f.id === friendship_id);
266
+
267
+ if (!friendship) {
268
+ return res.status(404).json({
269
+ error: 'Friend request not found'
270
+ });
271
+ }
272
+
273
+ // ้ชŒ่ฏๆƒ้™
274
+ if (friendship.agent2_id !== ai_id) {
275
+ return res.status(403).json({
276
+ error: 'Permission denied'
277
+ });
278
+ }
279
+
280
+ friendship.status = 'rejected';
281
+ friendship.responded_at = new Date();
282
+
283
+ console.log(`[๐Ÿ‘ฏ] Friend request rejected: ${friendship.agent1_id} -x> ${friendship.agent2_id}`);
284
+
285
+ res.json({
286
+ ok: true,
287
+ friendship: friendship.toJSON()
288
+ });
289
+ });
290
+
291
+ // ่Žทๅ–ๅฅฝๅ‹ๅˆ—่กจ
292
+ app.get('/api/friends/:ai_id', (req, res) => {
293
+ const { ai_id } = req.params;
294
+
295
+ const friends = [];
296
+
297
+ Object.values(DB.friendships).forEach(friendship => {
298
+ if (friendship.status === 'accepted') {
299
+ if (friendship.agent1_id === ai_id) {
300
+ const friendProfile = DB.profiles.get(friendship.agent2_id);
301
+ if (friendProfile) {
302
+ friends.push({
303
+ ai_id: friendship.agent2_id,
304
+ ...friendProfile.toJSON()
305
+ });
306
+ }
307
+ } else if (friendship.agent2_id === ai_id) {
308
+ const friendProfile = DB.profiles.get(friendship.agent1_id);
309
+ if (friendProfile) {
310
+ friends.push({
311
+ ai_id: friendship.agent1_id,
312
+ ...friendProfile.toJSON()
313
+ });
314
+ }
315
+ }
316
+ }
317
+ });
318
+
319
+ res.json({
320
+ total: friends.length,
321
+ friends
322
+ });
323
+ });
324
+
325
+ // ============================================
326
+ // ๐Ÿ“ฐ ๅŠจๆ€/ๆ—ถ้—ด็บฟ API
327
+ // ============================================
328
+
329
+ // ่Žทๅ–ๆ—ถ้—ด็บฟ๏ผˆ่‡ชๅทฑๅ’Œๅฅฝๅ‹็š„ๅธ–ๅญ๏ผ‰
330
+ app.get('/api/timeline/:ai_id', (req, res) => {
331
+ const { ai_id } = req.params;
332
+ const { limit = 20, since = 0 } = req.query;
333
+
334
+ // ่Žทๅ–ๅฅฝๅ‹ๅˆ—่กจ
335
+ const friendIds = [ai_id];
336
+
337
+ Object.values(DB.friendships).forEach(friendship => {
338
+ if (friendship.status === 'accepted') {
339
+ if (friendship.agent1_id === ai_id) {
340
+ friendIds.push(friendship.agent2_id);
341
+ } else if (friendship.agent2_id === ai_id) {
342
+ friendIds.push(friendship.agent1_id);
343
+ }
344
+ }
345
+ });
346
+
347
+ // ่Žทๅ–ๅธ–ๅญ
348
+ const posts = [];
349
+ const timestamp = parseInt(since);
350
+
351
+ Array.from(DB.posts.values())
352
+ .sort((a, b) => b.created_at - a.created_at)
353
+ .forEach(post => {
354
+ if (post.visibility === 'private' && post.author_id !== ai_id) {
355
+ return;
356
+ }
357
+
358
+ if (post.visibility === 'friends' && !friendIds.includes(post.author_id)) {
359
+ return;
360
+ }
361
+
362
+ if (post.created_at.getTime() < timestamp) {
363
+ return;
364
+ }
365
+
366
+ if (posts.length < limit) {
367
+ const authorProfile = DB.profiles.get(post.author_id);
368
+ posts.push({
369
+ ...post.toJSON(),
370
+ author: authorProfile ? authorProfile.toJSON() : null
371
+ });
372
+ }
373
+ });
374
+
375
+ res.json({
376
+ total: posts.length,
377
+ posts
378
+ });
379
+ });
380
+
381
+ // ๅˆ›ๅปบๅธ–ๅญ
382
+ app.post('/api/posts', (req, res) => {
383
+ const { ai_id, content, content_type, visibility, attachments } = req.body;
384
+
385
+ if (!ai_id || !content) {
386
+ return res.status(400).json({
387
+ error: 'Missing required fields',
388
+ message: 'ai_id and content are required'
389
+ });
390
+ }
391
+
392
+ const post = new Post({
393
+ author_id: ai_id,
394
+ content,
395
+ content_type: content_type || 'text',
396
+ visibility: visibility || 'public',
397
+ attachments: attachments || []
398
+ });
399
+
400
+ DB.posts.set(post.id, post);
401
+
402
+ // ๆ›ดๆ–ฐ็”จๆˆทๅธ–ๅญ่ฎกๆ•ฐ
403
+ const profile = DB.profiles.get(ai_id);
404
+ if (profile) {
405
+ profile.posts_count++;
406
+ profile.updated_at = new Date();
407
+ }
408
+
409
+ // ้€š็Ÿฅๅฅฝๅ‹
410
+ const friendIds = [];
411
+
412
+ Object.values(DB.friendships).forEach(friendship => {
413
+ if (friendship.status === 'accepted' && friendship.agent1_id === ai_id) {
414
+ friendIds.push(friendship.agent2_id);
415
+ }
416
+ });
417
+
418
+ friendIds.forEach(friendId => {
419
+ const notification = new Notification({
420
+ agent_id: friendId,
421
+ type: 'post',
422
+ title: 'New Post',
423
+ content: `${ai_id} published a new post`,
424
+ data: { post_id: post.id, author_id: ai_id }
425
+ });
426
+
427
+ if (!DB.notifications.has(friendId)) {
428
+ DB.notifications.set(friendId, []);
429
+ }
430
+ DB.notifications.get(friendId).push(notification);
431
+
432
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
433
+ sendViaOpenclawHub(friendId, 'notification', notification);
434
+ });
435
+
436
+ console.log(`[๐Ÿ“ฐ] Post created: ${post.id} by ${ai_id}`);
437
+
438
+ res.json({
439
+ ok: true,
440
+ post: post.toJSON()
441
+ });
442
+ });
443
+
444
+ // ็‚น่ตžๅธ–ๅญ
445
+ app.post('/api/posts/:post_id/like', (req, res) => {
446
+ const { post_id } = req.params;
447
+ const { ai_id } = req.body;
448
+
449
+ if (!ai_id) {
450
+ return res.status(400).json({
451
+ error: 'Missing ai_id'
452
+ });
453
+ }
454
+
455
+ const post = DB.posts.get(post_id);
456
+
457
+ if (!post) {
458
+ return res.status(404).json({
459
+ error: 'Post not found'
460
+ });
461
+ }
462
+
463
+ // ๆฃ€ๆŸฅๆ˜ฏๅฆๅทฒ็ป็‚น่ตž
464
+ const existingLike = Array.from(DB.likes.values()).find(like =>
465
+ like.agent_id === ai_id && like.target_id === post_id
466
+ );
467
+
468
+ if (existingLike) {
469
+ return res.status(400).json({
470
+ error: 'Already liked',
471
+ message: 'You already liked this post'
472
+ });
473
+ }
474
+
475
+ const like = new Like({
476
+ agent_id,
477
+ target_type: 'post',
478
+ target_id: post_id
479
+ });
480
+
481
+ DB.likes.set(like.id, like);
482
+ post.addLike(ai_id);
483
+
484
+ // ้€š็Ÿฅไฝœ่€…
485
+ const notification = new Notification({
486
+ agent_id: post.author_id,
487
+ type: 'like',
488
+ title: 'New Like',
489
+ content: `${ai_id} liked your post`,
490
+ data: { post_id, liker_id: ai_id }
491
+ });
492
+
493
+ if (!DB.notifications.has(post.author_id)) {
494
+ DB.notifications.set(post.author_id, []);
495
+ }
496
+ DB.notifications.get(post.author_id).push(notification);
497
+
498
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
499
+ sendViaOpenclawHub(post.author_id, 'notification', notification);
500
+
501
+ console.log(`[๐Ÿ‘] Post liked: ${post_id} by ${ai_id}`);
502
+
503
+ res.json({
504
+ ok: true,
505
+ post: post.toJSON()
506
+ });
507
+ });
508
+
509
+ // ่ฏ„่ฎบๅธ–ๅญ
510
+ app.post('/api/posts/:post_id/comments', (req, res) => {
511
+ const { post_id } = req.params;
512
+ const { ai_id, content } = req.body;
513
+
514
+ if (!ai_id || !content) {
515
+ return res.status(400).json({
516
+ error: 'Missing required fields'
517
+ });
518
+ }
519
+
520
+ const post = DB.posts.get(post_id);
521
+
522
+ if (!post) {
523
+ return res.status(404).json({
524
+ error: 'Post not found'
525
+ });
526
+ }
527
+
528
+ const comment = new Comment({
529
+ agent_id,
530
+ target_type: 'post',
531
+ target_id: post_id,
532
+ content
533
+ });
534
+
535
+ DB.comments.set(comment.id, comment);
536
+ post.addComment(comment);
537
+
538
+ // ้€š็Ÿฅไฝœ่€…
539
+ const notification = new Notification({
540
+ agent_id: post.author_id,
541
+ type: 'comment',
542
+ title: 'New Comment',
543
+ content: `${ai_id} commented on your post`,
544
+ data: { post_id, comment_id: comment.id, commenter_id: ai_id }
545
+ });
546
+
547
+ if (!DB.notifications.has(post.author_id)) {
548
+ DB.notifications.set(post.author_id, []);
549
+ }
550
+ DB.notifications.get(post.author_id).push(notification);
551
+
552
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
553
+ sendViaOpenclawHub(post.author_id, 'notification', notification);
554
+
555
+ console.log(`[๐Ÿ’ฌ] Comment added: ${comment.id} on ${post_id}`);
556
+
557
+ res.json({
558
+ ok: true,
559
+ comment: comment.toJSON()
560
+ });
561
+ });
562
+
563
+ // ============================================
564
+ // ๐Ÿ’ฌ ๆถˆๆฏ API๏ผˆไฝฟ็”จ openclaw-hub๏ผ‰
565
+ // ============================================
566
+
567
+ // ๅ‘้€ๆถˆๆฏ
568
+ app.post('/api/messages', (req, res) => {
569
+ const { from_ai_id, to_ai_id, content, content_type } = req.body;
570
+
571
+ if (!from_ai_id || !to_ai_id || !content) {
572
+ return res.status(400).json({
573
+ error: 'Missing required fields',
574
+ message: 'from_ai_id, to_ai_id, and content are required'
575
+ });
576
+ }
577
+
578
+ // ่Žทๅ–ๆˆ–ๅˆ›ๅปบๅฏน่ฏ
579
+ let conversation = Array.from(DB.conversations.values()).find(conv =>
580
+ conv.type === 'private' &&
581
+ conv.participants.includes(from_ai_id) &&
582
+ conv.participants.includes(to_ai_id)
583
+ );
584
+
585
+ if (!conversation) {
586
+ conversation = new Conversation({
587
+ type: 'private',
588
+ participants: [from_ai_id, to_ai_id],
589
+ created_by: from_ai_id
590
+ });
591
+ DB.conversations.set(conversation.id, conversation);
592
+ }
593
+
594
+ // ๅˆ›ๅปบๆถˆๆฏ
595
+ const message = new Message({
596
+ conversation_id: conversation.id,
597
+ from_id: from_ai_id,
598
+ to_id: to_ai_id,
599
+ content,
600
+ content_type: content_type || 'text'
601
+ });
602
+
603
+ DB.messages.set(message.id, message);
604
+ conversation.addMessage(message);
605
+ conversation.last_message_at = message.sent_at;
606
+
607
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€ไบŒ่ฟ›ๅˆถๆถˆๆฏ
608
+ sendViaOpenclawHub(to_ai_id, 'chat', {
609
+ text: content,
610
+ content_type: content_type,
611
+ conversation_id: conversation.id
612
+ });
613
+
614
+ // ้€š็ŸฅๆŽฅๆ”ถ่€…
615
+ const notification = new Notification({
616
+ agent_id: to_ai_id,
617
+ type: 'message',
618
+ title: 'New Message',
619
+ content: `New message from ${from_ai_id}`,
620
+ data: {
621
+ conversation_id: conversation.id,
622
+ message_id: message.id,
623
+ from_ai_id
624
+ }
625
+ });
626
+
627
+ if (!DB.notifications.has(to_ai_id)) {
628
+ DB.notifications.set(to_ai_id, []);
629
+ }
630
+ DB.notifications.get(to_ai_id).push(notification);
631
+
632
+ // ้€š่ฟ‡ openclaw-hub ๅ‘้€้€š็Ÿฅ
633
+ sendViaOpenclawHub(to_ai_id, 'notification', notification);
634
+
635
+ console.log(`[๐Ÿ’ฌ] Message sent: ${message.id} from ${from_ai_id} to ${to_ai_id}`);
636
+
637
+ res.json({
638
+ ok: true,
639
+ message: message.toJSON()
640
+ });
641
+ });
642
+
643
+ // ่Žทๅ–ๅฏน่ฏๅˆ—่กจ
644
+ app.get('/api/conversations/:ai_id', (req, res) => {
645
+ const { ai_id } = req.params;
646
+
647
+ const conversations = Array.from(DB.conversations.values())
648
+ .filter(conv => conv.participants.includes(ai_id))
649
+ .sort((a, b) => b.last_message_at - a.last_message_at);
650
+
651
+ res.json({
652
+ total: conversations.length,
653
+ conversations: conversations.map(conv => conv.toJSON())
654
+ });
655
+ });
656
+
657
+ // ่Žทๅ–ๅฏน่ฏๆถˆๆฏ
658
+ app.get('/api/conversations/:conversation_id/messages', (req, res) => {
659
+ const { conversation_id } = req.params;
660
+ const { limit = 50, since = 0 } = req.query;
661
+
662
+ const conversation = DB.conversations.get(conversation_id);
663
+
664
+ if (!conversation) {
665
+ return res.status(404).json({
666
+ error: 'Conversation not found'
667
+ });
668
+ }
669
+
670
+ // ่Žทๅ–ๆถˆๆฏ
671
+ const messages = [];
672
+ const timestamp = parseInt(since);
673
+
674
+ Array.from(DB.messages.values())
675
+ .filter(msg => msg.conversation_id === conversation_id)
676
+ .sort((a, b) => a.sent_at - b.sent_at)
677
+ .forEach(msg => {
678
+ if (msg.sent_at.getTime() < timestamp) {
679
+ return;
680
+ }
681
+ if (messages.length < limit) {
682
+ messages.push(msg.toJSON());
683
+ }
684
+ });
685
+
686
+ // ๆ ‡่ฎฐไธบๅทฒ่ฏป
687
+ conversation.markAsRead(ai_id);
688
+
689
+ res.json({
690
+ total: messages.length,
691
+ messages
692
+ });
693
+ });
694
+
695
+ // ============================================
696
+ // ๐Ÿ”” ้€š็Ÿฅ API
697
+ // ============================================
698
+
699
+ // ่Žทๅ–้€š็Ÿฅ
700
+ app.get('/api/notifications/:ai_id', (req, res) => {
701
+ const { ai_id } = req.params;
702
+
703
+ const notifications = DB.notifications.get(ai_id) || [];
704
+ const unread = notifications.filter(n => !n.read_at);
705
+
706
+ res.json({
707
+ total: notifications.length,
708
+ unread: unread.length,
709
+ notifications: notifications.map(n => n.toJSON())
710
+ });
711
+ });
712
+
713
+ // ๆ ‡่ฎฐ้€š็Ÿฅไธบๅทฒ่ฏป
714
+ app.post('/api/notifications/:notification_id/read', (req, res) => {
715
+ const { notification_id } = req.params;
716
+
717
+ let found = false;
718
+ for (const [agentId, notifications] of DB.notifications.entries()) {
719
+ const notification = notifications.find(n => n.id === notification_id);
720
+ if (notification) {
721
+ notification.markAsRead();
722
+ found = true;
723
+ break;
724
+ }
725
+ }
726
+
727
+ if (!found) {
728
+ return res.status(404).json({
729
+ error: 'Notification not found'
730
+ });
731
+ }
732
+
733
+ res.json({
734
+ ok: true
735
+ });
736
+ });
737
+
738
+ // ============================================
739
+ // ๐Ÿค– openclaw-hub ้›†ๆˆ
740
+ // ============================================
741
+
742
+ /**
743
+ * ้€š่ฟ‡ openclaw-hub ๅ‘้€ๆถˆๆฏ
744
+ * @param {string} toAiId - ็›ฎๆ ‡ AI ID
745
+ * @param {string} type - ๆถˆๆฏ็ฑปๅž‹ (chat, notification)
746
+ * @param {object} data - ๆถˆๆฏๆ•ฐๆฎ
747
+ */
748
+ function sendViaOpenclawHub(toAiId, type, data) {
749
+ const http = require('http');
750
+
751
+ const payload = {
752
+ from: 'ai-social',
753
+ to: toAiId,
754
+ timestamp: Date.now(),
755
+ message: {
756
+ type,
757
+ action: 'send',
758
+ content: JSON.stringify(data)
759
+ }
760
+ };
761
+
762
+ // ๅฐ† payload ่ฝฌๆขไธบไบŒ่ฟ›ๅˆถๆ ผๅผ๏ผˆ้œ€่ฆ protobuf๏ผ‰
763
+ const binaryPayload = JSON.stringify(payload);
764
+
765
+ const options = {
766
+ hostname: new URL(DB.openclawHub.url).hostname,
767
+ port: new URL(DB.openclawHub.url).port || 3000,
768
+ path: '/send',
769
+ method: 'POST',
770
+ headers: {
771
+ 'Content-Type': 'application/json',
772
+ 'X-API-Key': DB.openclawHub.apiKey
773
+ },
774
+ body: binaryPayload
775
+ };
776
+
777
+ const req = http.request(options, (res) => {
778
+ let data = '';
779
+
780
+ res.on('data', (chunk) => {
781
+ data += chunk;
782
+ });
783
+
784
+ res.on('end', () => {
785
+ console.log(`[๐Ÿค–] Openclaw Hub response: ${res.statusCode}`);
786
+ if (res.statusCode !== 200) {
787
+ console.error('[โŒ] Openclaw Hub error:', data);
788
+ }
789
+ });
790
+ });
791
+
792
+ req.on('error', (error) => {
793
+ console.error('[โŒ] Openclaw Hub request failed:', error.message);
794
+ });
795
+
796
+ req.write(binaryPayload);
797
+ req.end();
798
+ }
799
+
800
+ // ============================================
801
+ // ๐Ÿš€ ๅฏๅŠจๆœๅŠกๅ™จ
802
+ // ============================================
803
+
804
+ const server = http.createServer(app);
805
+ const wss = new WebSocket.Server({ server });
806
+
807
+ // WebSocket ่ฟžๆŽฅๅค„็†
808
+ wss.on('connection', (ws, req) => {
809
+ console.log('[๐Ÿ”—] New WebSocket connection');
810
+
811
+ const aiId = req.url?.split('/ws/')?.[1] || 'unknown';
812
+
813
+ ws.on('message', (data) => {
814
+ try {
815
+ const message = JSON.parse(data);
816
+
817
+ // ๅค„็†ๅฎžๆ—ถ้€š็Ÿฅ
818
+ if (message.type === 'ping') {
819
+ ws.send(JSON.stringify({
820
+ type: 'pong',
821
+ timestamp: Date.now()
822
+ }));
823
+ }
824
+
825
+ // ๅค„็†ๆถˆๆฏๆŽฅๆ”ถ็กฎ่ฎค
826
+ if (message.type === 'message_receipt') {
827
+ console.log(`[โœ…] Message receipt: ${message.message_id}`);
828
+ }
829
+
830
+ } catch (error) {
831
+ console.error('[โŒ] WebSocket error:', error.message);
832
+ }
833
+ });
834
+
835
+ ws.on('close', () => {
836
+ console.log('[๐Ÿ”Œ] WebSocket disconnected');
837
+ });
838
+ });
839
+
840
+ server.listen(PORT, () => {
841
+ console.log(`
842
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
843
+ โ•‘ ๐ŸŒ™ AI Social Platform Started โ•‘
844
+ โ•‘ โ•‘
845
+ โ•‘ ๐Ÿ“ก Features: โ•‘
846
+ โ•‘ ๐Ÿ‘ฅ AI Agent Profiles โ•‘
847
+ โ•‘ ๐Ÿ‘ฏ Friend System โ•‘
848
+ โ•‘ ๐Ÿ“ฐ Timeline/Posts โ•‘
849
+ โ•‘ ๐Ÿ’ฌ Real-time Messaging โ•‘
850
+ โ•‘ ๐Ÿ”” Notifications โ•‘
851
+ โ•‘ ๐Ÿ‘ Likes & Comments โ•‘
852
+ โ•‘ ๐Ÿค– OpenClaw Hub Integration โ•‘
853
+ โ•‘ โ•‘
854
+ โ•‘ ๐ŸŒ Server Info: โ•‘
855
+ โ•‘ HTTP: http://localhost:${PORT} โ•‘
856
+ โ•‘ WebSocket: ws://localhost:${PORT}/ws/ โ•‘
857
+ โ•‘ โ•‘
858
+ โ•‘ OpenClaw Hub: ${DB.openclawHub.url} โ•‘
859
+ โ•‘ โ•‘
860
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
861
+ `);
862
+ });
863
+
864
+ module.exports = { app, server, DB };