openkbs 0.0.76 → 0.0.78
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/package.json +1 -1
- package/src/actions.js +29 -0
- package/templates/.claude/CLAUDE.md +1 -1
- package/templates/.claude/skills/openkbs/SKILL.md +1 -0
- package/templates/.claude/skills/openkbs/reference/elastic-services.md +96 -0
- package/version.json +3 -3
- package/.claude/skills/openkbs/SKILL.md +0 -250
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/app/icon.png +0 -0
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/app/instructions.txt +0 -96
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/app/settings.json +0 -6
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/actions.js +0 -132
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/handler.js +0 -57
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/onRequest.js +0 -3
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/onRequest.json +0 -5
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/onResponse.js +0 -3
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Events/onResponse.json +0 -5
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Frontend/contentRender.js +0 -147
- package/.claude/skills/openkbs/examples/ai-copywriter-agent/src/Frontend/contentRender.json +0 -11
- package/.claude/skills/openkbs/examples/ai-marketing-agent/README.md +0 -64
- package/.claude/skills/openkbs/examples/ai-marketing-agent/app/instructions.txt +0 -160
- package/.claude/skills/openkbs/examples/ai-marketing-agent/app/settings.json +0 -7
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Events/actions.js +0 -258
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Events/onRequest.js +0 -13
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Events/onRequest.json +0 -3
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Events/onResponse.js +0 -13
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Events/onResponse.json +0 -3
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Frontend/contentRender.js +0 -170
- package/.claude/skills/openkbs/examples/ai-marketing-agent/src/Frontend/contentRender.json +0 -3
- package/.claude/skills/openkbs/examples/monitoring-bot/README.md +0 -55
- package/.claude/skills/openkbs/examples/monitoring-bot/app/instructions.txt +0 -40
- package/.claude/skills/openkbs/examples/monitoring-bot/app/settings.json +0 -41
- package/.claude/skills/openkbs/examples/monitoring-bot/openkbs.json +0 -3
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/actions.js +0 -141
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/handler.js +0 -32
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/memoryHelpers.js +0 -91
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onCronjob.js +0 -105
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onPublicAPIRequest.js +0 -165
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onRequest.js +0 -2
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onResponse.js +0 -2
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.js +0 -74
- package/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.json +0 -3
- package/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/index.mjs +0 -228
- package/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/package.json +0 -7
- package/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/index.mjs +0 -287
- package/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/package.json +0 -10
- package/.claude/skills/openkbs/examples/nodejs-demo/functions/settings.json +0 -4
- package/.claude/skills/openkbs/examples/nodejs-demo/openkbs.json +0 -16
- package/.claude/skills/openkbs/examples/nodejs-demo/site/index.html +0 -658
- package/.claude/skills/openkbs/examples/nodejs-demo/site/settings.json +0 -4
- package/.claude/skills/openkbs/metadata.json +0 -1
- package/.claude/skills/openkbs/patterns/cronjob-batch-processing.md +0 -278
- package/.claude/skills/openkbs/patterns/cronjob-monitoring.md +0 -341
- package/.claude/skills/openkbs/patterns/file-upload.md +0 -205
- package/.claude/skills/openkbs/patterns/image-generation.md +0 -139
- package/.claude/skills/openkbs/patterns/memory-system.md +0 -264
- package/.claude/skills/openkbs/patterns/public-api-item-proxy.md +0 -254
- package/.claude/skills/openkbs/patterns/scheduled-tasks.md +0 -157
- package/.claude/skills/openkbs/patterns/telegram-webhook.md +0 -424
- package/.claude/skills/openkbs/patterns/telegram.md +0 -222
- package/.claude/skills/openkbs/patterns/vectordb-archive.md +0 -231
- package/.claude/skills/openkbs/patterns/video-generation.md +0 -145
- package/.claude/skills/openkbs/patterns/web-publishing.md +0 -257
- package/.claude/skills/openkbs/reference/backend-sdk.md +0 -439
- package/.claude/skills/openkbs/reference/commands.md +0 -370
- package/.claude/skills/openkbs/reference/elastic-services.md +0 -359
- package/.claude/skills/openkbs/reference/frontend-sdk.md +0 -299
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import pg from 'pg';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
const { Client } = pg;
|
|
4
|
-
|
|
5
|
-
const db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
6
|
-
let dbConnected = false;
|
|
7
|
-
|
|
8
|
-
async function connectDB() {
|
|
9
|
-
if (!dbConnected) {
|
|
10
|
-
await db.connect();
|
|
11
|
-
dbConnected = true;
|
|
12
|
-
|
|
13
|
-
// Create users table with private_channel for secure messaging
|
|
14
|
-
await db.query(`
|
|
15
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
16
|
-
id SERIAL PRIMARY KEY,
|
|
17
|
-
name VARCHAR(255) NOT NULL,
|
|
18
|
-
email VARCHAR(255) UNIQUE NOT NULL,
|
|
19
|
-
password VARCHAR(255) NOT NULL,
|
|
20
|
-
private_channel VARCHAR(64) UNIQUE NOT NULL,
|
|
21
|
-
avatar_color VARCHAR(7) DEFAULT '#007bff',
|
|
22
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
23
|
-
)
|
|
24
|
-
`);
|
|
25
|
-
|
|
26
|
-
// Add private_channel column if missing (migration)
|
|
27
|
-
await db.query(`
|
|
28
|
-
ALTER TABLE users ADD COLUMN IF NOT EXISTS private_channel VARCHAR(64) UNIQUE
|
|
29
|
-
`);
|
|
30
|
-
await db.query(`
|
|
31
|
-
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_color VARCHAR(7) DEFAULT '#007bff'
|
|
32
|
-
`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Generate unpredictable channel ID for secure private messaging
|
|
37
|
-
function generatePrivateChannel() {
|
|
38
|
-
return crypto.randomBytes(32).toString('hex');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Generate random avatar color
|
|
42
|
-
function generateAvatarColor() {
|
|
43
|
-
const colors = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#ff9800', '#ff5722'];
|
|
44
|
-
return colors[Math.floor(Math.random() * colors.length)];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get a Pulse token for WebSocket authentication
|
|
49
|
-
* Calls lambda-kb to get a signed JWT token
|
|
50
|
-
* Uses OPENKBS_API_KEY for authentication
|
|
51
|
-
*/
|
|
52
|
-
async function getPulseToken(userId) {
|
|
53
|
-
const kbId = process.env.OPENKBS_KB_ID || process.env.PULSE_KB_ID;
|
|
54
|
-
const apiKey = process.env.OPENKBS_API_KEY;
|
|
55
|
-
|
|
56
|
-
if (!kbId) {
|
|
57
|
-
console.log('OPENKBS_KB_ID not set, skipping pulse token');
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!apiKey) {
|
|
62
|
-
console.log('OPENKBS_API_KEY not set, skipping pulse token');
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const response = await fetch('https://kb.openkbs.com', {
|
|
68
|
-
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json' },
|
|
70
|
-
body: JSON.stringify({
|
|
71
|
-
action: 'createPulseToken',
|
|
72
|
-
kbId,
|
|
73
|
-
apiKey,
|
|
74
|
-
userId: String(userId)
|
|
75
|
-
})
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const data = await response.json();
|
|
79
|
-
if (data.error) {
|
|
80
|
-
console.error('Pulse token error:', data.error);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return data;
|
|
84
|
-
} catch (e) {
|
|
85
|
-
console.error('Failed to get pulse token:', e);
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function handler(event) {
|
|
91
|
-
const headers = {
|
|
92
|
-
'Content-Type': 'application/json',
|
|
93
|
-
'Access-Control-Allow-Origin': '*',
|
|
94
|
-
'Access-Control-Allow-Headers': 'Content-Type'
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Handle OPTIONS preflight
|
|
98
|
-
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
99
|
-
return { statusCode: 200, headers, body: '' };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await connectDB();
|
|
104
|
-
|
|
105
|
-
const body = JSON.parse(event.body || '{}');
|
|
106
|
-
const { action, email, password, name } = body;
|
|
107
|
-
|
|
108
|
-
if (action === 'register') {
|
|
109
|
-
// Check if user exists
|
|
110
|
-
const existing = await db.query('SELECT id FROM users WHERE email = $1', [email]);
|
|
111
|
-
if (existing.rows.length > 0) {
|
|
112
|
-
return {
|
|
113
|
-
statusCode: 400,
|
|
114
|
-
headers,
|
|
115
|
-
body: JSON.stringify({ error: 'Email already registered' })
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Generate unique private channel for secure messaging
|
|
120
|
-
const privateChannel = generatePrivateChannel();
|
|
121
|
-
const avatarColor = generateAvatarColor();
|
|
122
|
-
|
|
123
|
-
// Create user with private channel
|
|
124
|
-
const result = await db.query(
|
|
125
|
-
'INSERT INTO users (name, email, password, private_channel, avatar_color) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, email, private_channel, avatar_color, created_at',
|
|
126
|
-
[name, email, password, privateChannel, avatarColor]
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const user = result.rows[0];
|
|
130
|
-
|
|
131
|
-
// Get Pulse token for real-time features
|
|
132
|
-
const pulseData = await getPulseToken(user.id);
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
statusCode: 200,
|
|
136
|
-
headers,
|
|
137
|
-
body: JSON.stringify({
|
|
138
|
-
user: {
|
|
139
|
-
id: user.id,
|
|
140
|
-
name: user.name,
|
|
141
|
-
email: user.email,
|
|
142
|
-
avatarColor: user.avatar_color,
|
|
143
|
-
// Private channel is SECRET - only this user knows it
|
|
144
|
-
privateChannel: user.private_channel,
|
|
145
|
-
pulseToken: pulseData?.token || null,
|
|
146
|
-
pulseEndpoint: pulseData?.endpoint || null
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (action === 'login') {
|
|
153
|
-
const result = await db.query(
|
|
154
|
-
'SELECT id, name, email, private_channel, avatar_color FROM users WHERE email = $1 AND password = $2',
|
|
155
|
-
[email, password]
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (result.rows.length === 0) {
|
|
159
|
-
return {
|
|
160
|
-
statusCode: 401,
|
|
161
|
-
headers,
|
|
162
|
-
body: JSON.stringify({ error: 'Invalid email or password' })
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const user = result.rows[0];
|
|
167
|
-
|
|
168
|
-
// Generate private channel if missing (for existing users)
|
|
169
|
-
let privateChannel = user.private_channel;
|
|
170
|
-
if (!privateChannel) {
|
|
171
|
-
privateChannel = generatePrivateChannel();
|
|
172
|
-
await db.query('UPDATE users SET private_channel = $1 WHERE id = $2', [privateChannel, user.id]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Get Pulse token for real-time features
|
|
176
|
-
const pulseData = await getPulseToken(user.id);
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
statusCode: 200,
|
|
180
|
-
headers,
|
|
181
|
-
body: JSON.stringify({
|
|
182
|
-
user: {
|
|
183
|
-
id: user.id,
|
|
184
|
-
name: user.name,
|
|
185
|
-
email: user.email,
|
|
186
|
-
avatarColor: user.avatar_color || '#007bff',
|
|
187
|
-
privateChannel: privateChannel,
|
|
188
|
-
pulseToken: pulseData?.token || null,
|
|
189
|
-
pulseEndpoint: pulseData?.endpoint || null
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Get list of users (for chat) - WITHOUT exposing private channels
|
|
196
|
-
if (action === 'users') {
|
|
197
|
-
const result = await db.query(
|
|
198
|
-
'SELECT id, name, avatar_color FROM users ORDER BY name'
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
statusCode: 200,
|
|
203
|
-
headers,
|
|
204
|
-
body: JSON.stringify({
|
|
205
|
-
users: result.rows.map(u => ({
|
|
206
|
-
id: u.id,
|
|
207
|
-
name: u.name,
|
|
208
|
-
avatarColor: u.avatar_color || '#007bff'
|
|
209
|
-
}))
|
|
210
|
-
})
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
statusCode: 400,
|
|
216
|
-
headers,
|
|
217
|
-
body: JSON.stringify({ error: 'Invalid action' })
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
} catch (error) {
|
|
221
|
-
console.error('Auth error:', error);
|
|
222
|
-
return {
|
|
223
|
-
statusCode: 500,
|
|
224
|
-
headers,
|
|
225
|
-
body: JSON.stringify({ error: error.message })
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import pg from 'pg';
|
|
2
|
-
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
3
|
-
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
4
|
-
import pulse from 'openkbs-pulse/server';
|
|
5
|
-
|
|
6
|
-
const { Client } = pg;
|
|
7
|
-
|
|
8
|
-
const db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
9
|
-
let dbConnected = false;
|
|
10
|
-
|
|
11
|
-
const s3 = new S3Client({ region: process.env.STORAGE_REGION || 'us-east-1' });
|
|
12
|
-
|
|
13
|
-
async function connectDB() {
|
|
14
|
-
if (!dbConnected) {
|
|
15
|
-
await db.connect();
|
|
16
|
-
dbConnected = true;
|
|
17
|
-
|
|
18
|
-
// Posts table
|
|
19
|
-
await db.query(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS posts (
|
|
21
|
-
id SERIAL PRIMARY KEY,
|
|
22
|
-
user_id INTEGER NOT NULL,
|
|
23
|
-
user_name VARCHAR(255) NOT NULL,
|
|
24
|
-
content TEXT,
|
|
25
|
-
image_url TEXT,
|
|
26
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
27
|
-
)
|
|
28
|
-
`);
|
|
29
|
-
|
|
30
|
-
await db.query(`
|
|
31
|
-
ALTER TABLE posts ADD COLUMN IF NOT EXISTS image_url TEXT
|
|
32
|
-
`);
|
|
33
|
-
|
|
34
|
-
// Messages table for private chat
|
|
35
|
-
await db.query(`
|
|
36
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
37
|
-
id SERIAL PRIMARY KEY,
|
|
38
|
-
from_user_id INTEGER NOT NULL,
|
|
39
|
-
from_user_name VARCHAR(255) NOT NULL,
|
|
40
|
-
to_user_id INTEGER NOT NULL,
|
|
41
|
-
content TEXT NOT NULL,
|
|
42
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
43
|
-
)
|
|
44
|
-
`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function handler(event) {
|
|
49
|
-
const headers = {
|
|
50
|
-
'Content-Type': 'application/json',
|
|
51
|
-
'Access-Control-Allow-Origin': '*',
|
|
52
|
-
'Access-Control-Allow-Headers': 'Content-Type'
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Handle OPTIONS preflight
|
|
56
|
-
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
57
|
-
return { statusCode: 200, headers, body: '' };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const body = JSON.parse(event.body || '{}');
|
|
62
|
-
const { action, content, imageUrl, userId, userName, fileName, contentType } = body;
|
|
63
|
-
|
|
64
|
-
// getUploadUrl doesn't need DB connection
|
|
65
|
-
if (action === 'getUploadUrl') {
|
|
66
|
-
const bucket = process.env.STORAGE_BUCKET;
|
|
67
|
-
if (!bucket) {
|
|
68
|
-
return {
|
|
69
|
-
statusCode: 500,
|
|
70
|
-
headers,
|
|
71
|
-
body: JSON.stringify({ error: 'Storage not configured' })
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Generate unique key for the file
|
|
76
|
-
// Key must match CloudFront path prefix (e.g., /media/* -> media/...)
|
|
77
|
-
const timestamp = Date.now();
|
|
78
|
-
const safeName = (fileName || 'image.jpg').replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
79
|
-
const key = `media/uploads/${timestamp}-${safeName}`;
|
|
80
|
-
|
|
81
|
-
// Create presigned URL for PUT
|
|
82
|
-
const command = new PutObjectCommand({
|
|
83
|
-
Bucket: bucket,
|
|
84
|
-
Key: key,
|
|
85
|
-
ContentType: contentType || 'image/jpeg'
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
|
|
89
|
-
|
|
90
|
-
// Use CloudFront URL - key already contains the path prefix (media/uploads/...)
|
|
91
|
-
// Final URL: /media/uploads/filename.png
|
|
92
|
-
const publicUrl = `/${key}`;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
statusCode: 200,
|
|
96
|
-
headers,
|
|
97
|
-
body: JSON.stringify({ uploadUrl, publicUrl, key })
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// All other actions need DB
|
|
102
|
-
await connectDB();
|
|
103
|
-
|
|
104
|
-
if (action === 'list') {
|
|
105
|
-
// Get latest 50 posts
|
|
106
|
-
const result = await db.query(
|
|
107
|
-
'SELECT id, user_id, user_name, content, image_url, created_at FROM posts ORDER BY created_at DESC LIMIT 50'
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const posts = result.rows.map(row => ({
|
|
111
|
-
id: row.id,
|
|
112
|
-
userId: row.user_id,
|
|
113
|
-
userName: row.user_name,
|
|
114
|
-
content: row.content,
|
|
115
|
-
imageUrl: row.image_url,
|
|
116
|
-
createdAt: row.created_at
|
|
117
|
-
}));
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
statusCode: 200,
|
|
121
|
-
headers,
|
|
122
|
-
body: JSON.stringify({ posts })
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (action === 'presence') {
|
|
127
|
-
const result = await pulse.presence(body.channel || 'posts');
|
|
128
|
-
return {
|
|
129
|
-
statusCode: 200,
|
|
130
|
-
headers,
|
|
131
|
-
body: JSON.stringify({ count: result.count || 0 })
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (action === 'create') {
|
|
136
|
-
// Allow posts with just image (no content required)
|
|
137
|
-
if (!content && !imageUrl) {
|
|
138
|
-
return {
|
|
139
|
-
statusCode: 400,
|
|
140
|
-
headers,
|
|
141
|
-
body: JSON.stringify({ error: 'Content or image required' })
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (!userId || !userName) {
|
|
145
|
-
return {
|
|
146
|
-
statusCode: 400,
|
|
147
|
-
headers,
|
|
148
|
-
body: JSON.stringify({ error: 'Missing user info' })
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Insert post with optional image
|
|
153
|
-
const result = await db.query(
|
|
154
|
-
'INSERT INTO posts (user_id, user_name, content, image_url) VALUES ($1, $2, $3, $4) RETURNING id, created_at',
|
|
155
|
-
[userId, userName, content || '', imageUrl || null]
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const post = {
|
|
159
|
-
id: result.rows[0].id,
|
|
160
|
-
userId,
|
|
161
|
-
userName,
|
|
162
|
-
content: content || '',
|
|
163
|
-
imageUrl: imageUrl || null,
|
|
164
|
-
createdAt: result.rows[0].created_at
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Broadcast to Pulse
|
|
168
|
-
const kbId = process.env.OPENKBS_KB_ID;
|
|
169
|
-
const apiKey = process.env.OPENKBS_API_KEY;
|
|
170
|
-
const postPublish = await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
|
|
171
|
-
console.log('Post publish result:', JSON.stringify(postPublish));
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
statusCode: 200,
|
|
175
|
-
headers,
|
|
176
|
-
body: JSON.stringify({ post, debug: { postPublish } })
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Send private message - publishes to recipient's SECRET channel
|
|
181
|
-
if (action === 'sendMessage') {
|
|
182
|
-
const { toUserId, message, fromUserId, fromUserName } = body;
|
|
183
|
-
|
|
184
|
-
if (!toUserId || !message || !fromUserId || !fromUserName) {
|
|
185
|
-
return {
|
|
186
|
-
statusCode: 400,
|
|
187
|
-
headers,
|
|
188
|
-
body: JSON.stringify({ error: 'Missing required fields' })
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Look up recipient's PRIVATE channel (stored in users table)
|
|
193
|
-
const recipientResult = await db.query(
|
|
194
|
-
'SELECT private_channel, name FROM users WHERE id = $1',
|
|
195
|
-
[toUserId]
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (recipientResult.rows.length === 0) {
|
|
199
|
-
return {
|
|
200
|
-
statusCode: 404,
|
|
201
|
-
headers,
|
|
202
|
-
body: JSON.stringify({ error: 'Recipient not found' })
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const recipientChannel = recipientResult.rows[0].private_channel;
|
|
207
|
-
|
|
208
|
-
// Store message in database
|
|
209
|
-
const msgResult = await db.query(
|
|
210
|
-
'INSERT INTO messages (from_user_id, from_user_name, to_user_id, content) VALUES ($1, $2, $3, $4) RETURNING id, created_at',
|
|
211
|
-
[fromUserId, fromUserName, toUserId, message]
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const msgData = {
|
|
215
|
-
id: msgResult.rows[0].id,
|
|
216
|
-
fromUserId,
|
|
217
|
-
fromUserName,
|
|
218
|
-
toUserId,
|
|
219
|
-
content: message,
|
|
220
|
-
createdAt: msgResult.rows[0].created_at
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
// Publish to recipient's SECRET private channel
|
|
224
|
-
// Only the recipient is subscribed to this channel!
|
|
225
|
-
const kbId = process.env.OPENKBS_KB_ID;
|
|
226
|
-
const apiKey = process.env.OPENKBS_API_KEY;
|
|
227
|
-
console.log('Publishing to private channel:', recipientChannel.substring(0, 8) + '...');
|
|
228
|
-
const publishResult = await pulse.publish(recipientChannel, 'new_message', msgData, { kbId, apiKey });
|
|
229
|
-
console.log('Publish result:', JSON.stringify(publishResult));
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
statusCode: 200,
|
|
233
|
-
headers,
|
|
234
|
-
body: JSON.stringify({
|
|
235
|
-
message: msgData,
|
|
236
|
-
debug: {
|
|
237
|
-
recipientChannel: recipientChannel.substring(0, 16) + '...',
|
|
238
|
-
publishResult
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Get chat history with a specific user
|
|
245
|
-
if (action === 'getMessages') {
|
|
246
|
-
const { userId, withUserId } = body;
|
|
247
|
-
|
|
248
|
-
const result = await db.query(
|
|
249
|
-
`SELECT id, from_user_id, from_user_name, to_user_id, content, created_at
|
|
250
|
-
FROM messages
|
|
251
|
-
WHERE (from_user_id = $1 AND to_user_id = $2) OR (from_user_id = $2 AND to_user_id = $1)
|
|
252
|
-
ORDER BY created_at ASC
|
|
253
|
-
LIMIT 100`,
|
|
254
|
-
[userId, withUserId]
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
statusCode: 200,
|
|
259
|
-
headers,
|
|
260
|
-
body: JSON.stringify({
|
|
261
|
-
messages: result.rows.map(m => ({
|
|
262
|
-
id: m.id,
|
|
263
|
-
fromUserId: m.from_user_id,
|
|
264
|
-
fromUserName: m.from_user_name,
|
|
265
|
-
toUserId: m.to_user_id,
|
|
266
|
-
content: m.content,
|
|
267
|
-
createdAt: m.created_at
|
|
268
|
-
}))
|
|
269
|
-
})
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
statusCode: 400,
|
|
275
|
-
headers,
|
|
276
|
-
body: JSON.stringify({ error: 'Invalid action' })
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
} catch (error) {
|
|
280
|
-
console.error('Posts error:', error);
|
|
281
|
-
return {
|
|
282
|
-
statusCode: 500,
|
|
283
|
-
headers,
|
|
284
|
-
body: JSON.stringify({ error: error.message })
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|