@commonpub/server 2.3.4 → 2.4.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/dist/federation/hubMirroring.d.ts +100 -0
- package/dist/federation/hubMirroring.d.ts.map +1 -0
- package/dist/federation/hubMirroring.js +352 -0
- package/dist/federation/hubMirroring.js.map +1 -0
- package/dist/federation/inboxHandlers.d.ts.map +1 -1
- package/dist/federation/inboxHandlers.js +60 -1
- package/dist/federation/inboxHandlers.js.map +1 -1
- package/dist/federation/index.d.ts +1 -0
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js +1 -0
- package/dist/federation/index.js.map +1 -1
- package/dist/hub/hub.d.ts +5 -107
- package/dist/hub/hub.d.ts.map +1 -1
- package/dist/hub/hub.js +54 -851
- package/dist/hub/hub.js.map +1 -1
- package/dist/hub/index.d.ts +4 -1
- package/dist/hub/index.d.ts.map +1 -1
- package/dist/hub/index.js +8 -1
- package/dist/hub/index.js.map +1 -1
- package/dist/hub/members.d.ts +26 -0
- package/dist/hub/members.d.ts.map +1 -0
- package/dist/hub/members.js +188 -0
- package/dist/hub/members.js.map +1 -0
- package/dist/hub/moderation.d.ts +29 -0
- package/dist/hub/moderation.d.ts.map +1 -0
- package/dist/hub/moderation.js +203 -0
- package/dist/hub/moderation.js.map +1 -0
- package/dist/hub/posts.d.ts +55 -0
- package/dist/hub/posts.d.ts.map +1 -0
- package/dist/hub/posts.js +473 -0
- package/dist/hub/posts.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/hub/hub.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { eq, and,
|
|
2
|
-
import { hubs, hubMembers,
|
|
3
|
-
import { generateSlug, hasPermission
|
|
1
|
+
import { eq, and, desc, ilike, isNull } from 'drizzle-orm';
|
|
2
|
+
import { hubs, hubMembers, users, } from '@commonpub/schema';
|
|
3
|
+
import { generateSlug, hasPermission } from '../utils.js';
|
|
4
4
|
import { ensureUniqueSlugFor, USER_REF_SELECT, normalizePagination, countRows, escapeLike } from '../query.js';
|
|
5
|
+
import { checkBan } from './moderation.js';
|
|
6
|
+
import { listFederatedHubs } from '../federation/hubMirroring.js';
|
|
5
7
|
// --- Hub CRUD ---
|
|
6
|
-
export async function listHubs(db, filters = {}) {
|
|
8
|
+
export async function listHubs(db, filters = {}, options) {
|
|
7
9
|
const conditions = [isNull(hubs.deletedAt)];
|
|
8
10
|
if (filters.search) {
|
|
9
11
|
conditions.push(ilike(hubs.name, `%${escapeLike(filters.search)}%`));
|
|
@@ -27,7 +29,7 @@ export async function listHubs(db, filters = {}) {
|
|
|
27
29
|
.offset(offset),
|
|
28
30
|
countRows(db, hubs, where),
|
|
29
31
|
]);
|
|
30
|
-
const
|
|
32
|
+
const localItems = rows.map((row) => ({
|
|
31
33
|
id: row.hub.id,
|
|
32
34
|
name: row.hub.name,
|
|
33
35
|
slug: row.hub.slug,
|
|
@@ -42,7 +44,53 @@ export async function listHubs(db, filters = {}) {
|
|
|
42
44
|
createdAt: row.hub.createdAt,
|
|
43
45
|
createdBy: row.createdBy,
|
|
44
46
|
}));
|
|
45
|
-
|
|
47
|
+
if (!options?.includeFederated) {
|
|
48
|
+
return { items: localItems, total };
|
|
49
|
+
}
|
|
50
|
+
// Merge with federated hubs — fetch enough from both sources to fill the page
|
|
51
|
+
const maxItems = offset + limit;
|
|
52
|
+
const [localAll, fedResult] = await Promise.all([
|
|
53
|
+
// Re-fetch local without offset to get enough for merge (only if offset > 0)
|
|
54
|
+
offset > 0
|
|
55
|
+
? db
|
|
56
|
+
.select({ hub: hubs, createdBy: USER_REF_SELECT })
|
|
57
|
+
.from(hubs)
|
|
58
|
+
.innerJoin(users, eq(hubs.createdById, users.id))
|
|
59
|
+
.where(where)
|
|
60
|
+
.orderBy(desc(hubs.createdAt))
|
|
61
|
+
.limit(maxItems)
|
|
62
|
+
.then((r) => r.map((row) => ({
|
|
63
|
+
id: row.hub.id,
|
|
64
|
+
name: row.hub.name,
|
|
65
|
+
slug: row.hub.slug,
|
|
66
|
+
description: row.hub.description,
|
|
67
|
+
hubType: row.hub.hubType,
|
|
68
|
+
iconUrl: row.hub.iconUrl,
|
|
69
|
+
bannerUrl: row.hub.bannerUrl,
|
|
70
|
+
joinPolicy: row.hub.joinPolicy,
|
|
71
|
+
isOfficial: row.hub.isOfficial,
|
|
72
|
+
memberCount: row.hub.memberCount,
|
|
73
|
+
postCount: row.hub.postCount,
|
|
74
|
+
createdAt: row.hub.createdAt,
|
|
75
|
+
createdBy: row.createdBy,
|
|
76
|
+
})))
|
|
77
|
+
: Promise.resolve(localItems),
|
|
78
|
+
listFederatedHubs(db, { search: filters.search, limit: maxItems }),
|
|
79
|
+
]);
|
|
80
|
+
// Merge and sort by creation date (local createdAt vs federated receivedAt)
|
|
81
|
+
const merged = [
|
|
82
|
+
...localAll,
|
|
83
|
+
...fedResult.items,
|
|
84
|
+
];
|
|
85
|
+
merged.sort((a, b) => {
|
|
86
|
+
const dateA = 'createdAt' in a ? new Date(a.createdAt).getTime() : new Date(a.receivedAt).getTime();
|
|
87
|
+
const dateB = 'createdAt' in b ? new Date(b.createdAt).getTime() : new Date(b.receivedAt).getTime();
|
|
88
|
+
return dateB - dateA;
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
items: merged.slice(offset, offset + limit),
|
|
92
|
+
total: total + fedResult.total,
|
|
93
|
+
};
|
|
46
94
|
}
|
|
47
95
|
export async function getHubBySlug(db, slug, requesterId) {
|
|
48
96
|
const rows = await db
|
|
@@ -176,849 +224,4 @@ export async function deleteHub(db, hubId, userId) {
|
|
|
176
224
|
.where(eq(hubs.id, hubId));
|
|
177
225
|
return true;
|
|
178
226
|
}
|
|
179
|
-
// --- Membership ---
|
|
180
|
-
export async function joinHub(db, userId, hubId, inviteToken) {
|
|
181
|
-
// Check ban
|
|
182
|
-
const ban = await checkBan(db, hubId, userId);
|
|
183
|
-
if (ban) {
|
|
184
|
-
return { joined: false, error: 'You are banned from this hub' };
|
|
185
|
-
}
|
|
186
|
-
// Check join policy
|
|
187
|
-
const hubRow = await db
|
|
188
|
-
.select({ joinPolicy: hubs.joinPolicy })
|
|
189
|
-
.from(hubs)
|
|
190
|
-
.where(eq(hubs.id, hubId))
|
|
191
|
-
.limit(1);
|
|
192
|
-
if (hubRow.length === 0) {
|
|
193
|
-
return { joined: false, error: 'Hub not found' };
|
|
194
|
-
}
|
|
195
|
-
const policy = hubRow[0].joinPolicy;
|
|
196
|
-
if (policy !== 'open') {
|
|
197
|
-
if (!inviteToken) {
|
|
198
|
-
return { joined: false, error: 'Invite token required' };
|
|
199
|
-
}
|
|
200
|
-
const tokenResult = await validateAndUseInvite(db, inviteToken);
|
|
201
|
-
if (!tokenResult.valid) {
|
|
202
|
-
return { joined: false, error: 'Invalid or expired invite token' };
|
|
203
|
-
}
|
|
204
|
-
// Verify the invite belongs to this specific hub
|
|
205
|
-
if (tokenResult.hubId !== hubId) {
|
|
206
|
-
return { joined: false, error: 'Invite token is not valid for this hub' };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return db.transaction(async (tx) => {
|
|
210
|
-
const inserted = await tx
|
|
211
|
-
.insert(hubMembers)
|
|
212
|
-
.values({ hubId, userId, role: 'member' })
|
|
213
|
-
.onConflictDoNothing()
|
|
214
|
-
.returning();
|
|
215
|
-
if (inserted.length === 0) {
|
|
216
|
-
return { joined: true };
|
|
217
|
-
}
|
|
218
|
-
await tx
|
|
219
|
-
.update(hubs)
|
|
220
|
-
.set({ memberCount: sql `${hubs.memberCount} + 1` })
|
|
221
|
-
.where(eq(hubs.id, hubId));
|
|
222
|
-
return { joined: true };
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
export async function leaveHub(db, userId, hubId) {
|
|
226
|
-
const member = await db
|
|
227
|
-
.select({ role: hubMembers.role })
|
|
228
|
-
.from(hubMembers)
|
|
229
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
230
|
-
.limit(1);
|
|
231
|
-
if (member.length === 0) {
|
|
232
|
-
return { left: false, error: 'Not a member' };
|
|
233
|
-
}
|
|
234
|
-
if (member[0].role === 'owner') {
|
|
235
|
-
return { left: false, error: 'Owner cannot leave the hub' };
|
|
236
|
-
}
|
|
237
|
-
await db
|
|
238
|
-
.delete(hubMembers)
|
|
239
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)));
|
|
240
|
-
await db
|
|
241
|
-
.update(hubs)
|
|
242
|
-
.set({ memberCount: sql `GREATEST(${hubs.memberCount} - 1, 0)` })
|
|
243
|
-
.where(eq(hubs.id, hubId));
|
|
244
|
-
return { left: true };
|
|
245
|
-
}
|
|
246
|
-
export async function getMember(db, hubId, userId) {
|
|
247
|
-
const rows = await db
|
|
248
|
-
.select({
|
|
249
|
-
member: hubMembers,
|
|
250
|
-
user: USER_REF_SELECT,
|
|
251
|
-
})
|
|
252
|
-
.from(hubMembers)
|
|
253
|
-
.innerJoin(users, eq(hubMembers.userId, users.id))
|
|
254
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
255
|
-
.limit(1);
|
|
256
|
-
if (rows.length === 0)
|
|
257
|
-
return null;
|
|
258
|
-
const row = rows[0];
|
|
259
|
-
return {
|
|
260
|
-
hubId: row.member.hubId,
|
|
261
|
-
userId: row.member.userId,
|
|
262
|
-
role: row.member.role,
|
|
263
|
-
joinedAt: row.member.joinedAt,
|
|
264
|
-
user: row.user,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
export async function listMembers(db, hubId, opts = {}) {
|
|
268
|
-
const { limit, offset } = normalizePagination(opts);
|
|
269
|
-
const where = eq(hubMembers.hubId, hubId);
|
|
270
|
-
const [rows, total] = await Promise.all([
|
|
271
|
-
db
|
|
272
|
-
.select({
|
|
273
|
-
member: hubMembers,
|
|
274
|
-
user: USER_REF_SELECT,
|
|
275
|
-
})
|
|
276
|
-
.from(hubMembers)
|
|
277
|
-
.innerJoin(users, eq(hubMembers.userId, users.id))
|
|
278
|
-
.where(where)
|
|
279
|
-
.orderBy(desc(hubMembers.joinedAt))
|
|
280
|
-
.limit(limit)
|
|
281
|
-
.offset(offset),
|
|
282
|
-
countRows(db, hubMembers, where),
|
|
283
|
-
]);
|
|
284
|
-
const items = rows.map((row) => ({
|
|
285
|
-
hubId: row.member.hubId,
|
|
286
|
-
userId: row.member.userId,
|
|
287
|
-
role: row.member.role,
|
|
288
|
-
joinedAt: row.member.joinedAt,
|
|
289
|
-
user: row.user,
|
|
290
|
-
}));
|
|
291
|
-
return { items, total };
|
|
292
|
-
}
|
|
293
|
-
export async function changeRole(db, actorId, hubId, targetUserId, newRole) {
|
|
294
|
-
const [actorMember, targetMember] = await Promise.all([
|
|
295
|
-
db
|
|
296
|
-
.select({ role: hubMembers.role })
|
|
297
|
-
.from(hubMembers)
|
|
298
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, actorId)))
|
|
299
|
-
.limit(1),
|
|
300
|
-
db
|
|
301
|
-
.select({ role: hubMembers.role })
|
|
302
|
-
.from(hubMembers)
|
|
303
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)))
|
|
304
|
-
.limit(1),
|
|
305
|
-
]);
|
|
306
|
-
if (actorMember.length === 0) {
|
|
307
|
-
return { changed: false, error: 'Not a member' };
|
|
308
|
-
}
|
|
309
|
-
if (targetMember.length === 0) {
|
|
310
|
-
return { changed: false, error: 'Target is not a member' };
|
|
311
|
-
}
|
|
312
|
-
if (!hasPermission(actorMember[0].role, 'manageMembers')) {
|
|
313
|
-
return { changed: false, error: 'Insufficient permissions' };
|
|
314
|
-
}
|
|
315
|
-
if (!canManageRole(actorMember[0].role, targetMember[0].role)) {
|
|
316
|
-
return { changed: false, error: 'Cannot manage a user with equal or higher role' };
|
|
317
|
-
}
|
|
318
|
-
if (newRole === 'owner') {
|
|
319
|
-
return { changed: false, error: 'Cannot promote to owner' };
|
|
320
|
-
}
|
|
321
|
-
await db
|
|
322
|
-
.update(hubMembers)
|
|
323
|
-
.set({ role: newRole })
|
|
324
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)));
|
|
325
|
-
return { changed: true };
|
|
326
|
-
}
|
|
327
|
-
export async function kickMember(db, actorId, hubId, targetUserId) {
|
|
328
|
-
const [actorMember, targetMember] = await Promise.all([
|
|
329
|
-
db
|
|
330
|
-
.select({ role: hubMembers.role })
|
|
331
|
-
.from(hubMembers)
|
|
332
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, actorId)))
|
|
333
|
-
.limit(1),
|
|
334
|
-
db
|
|
335
|
-
.select({ role: hubMembers.role })
|
|
336
|
-
.from(hubMembers)
|
|
337
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)))
|
|
338
|
-
.limit(1),
|
|
339
|
-
]);
|
|
340
|
-
if (actorMember.length === 0) {
|
|
341
|
-
return { kicked: false, error: 'Not a member' };
|
|
342
|
-
}
|
|
343
|
-
if (targetMember.length === 0) {
|
|
344
|
-
return { kicked: false, error: 'Target is not a member' };
|
|
345
|
-
}
|
|
346
|
-
if (!hasPermission(actorMember[0].role, 'kickMember')) {
|
|
347
|
-
return { kicked: false, error: 'Insufficient permissions' };
|
|
348
|
-
}
|
|
349
|
-
if (!canManageRole(actorMember[0].role, targetMember[0].role)) {
|
|
350
|
-
return { kicked: false, error: 'Cannot kick a user with equal or higher role' };
|
|
351
|
-
}
|
|
352
|
-
await db
|
|
353
|
-
.delete(hubMembers)
|
|
354
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)));
|
|
355
|
-
await db
|
|
356
|
-
.update(hubs)
|
|
357
|
-
.set({ memberCount: sql `GREATEST(${hubs.memberCount} - 1, 0)` })
|
|
358
|
-
.where(eq(hubs.id, hubId));
|
|
359
|
-
return { kicked: true };
|
|
360
|
-
}
|
|
361
|
-
// --- Posts & Replies ---
|
|
362
|
-
export async function createPost(db, authorId, input) {
|
|
363
|
-
const member = await db
|
|
364
|
-
.select({ role: hubMembers.role })
|
|
365
|
-
.from(hubMembers)
|
|
366
|
-
.where(and(eq(hubMembers.hubId, input.hubId), eq(hubMembers.userId, authorId)))
|
|
367
|
-
.limit(1);
|
|
368
|
-
if (member.length === 0) {
|
|
369
|
-
throw new Error('Must be a member to post');
|
|
370
|
-
}
|
|
371
|
-
return db.transaction(async (tx) => {
|
|
372
|
-
const [post] = await tx
|
|
373
|
-
.insert(hubPosts)
|
|
374
|
-
.values({
|
|
375
|
-
hubId: input.hubId,
|
|
376
|
-
authorId,
|
|
377
|
-
type: input.type ?? 'text',
|
|
378
|
-
content: input.content,
|
|
379
|
-
})
|
|
380
|
-
.returning();
|
|
381
|
-
await tx
|
|
382
|
-
.update(hubs)
|
|
383
|
-
.set({ postCount: sql `${hubs.postCount} + 1` })
|
|
384
|
-
.where(eq(hubs.id, input.hubId));
|
|
385
|
-
const author = await tx
|
|
386
|
-
.select(USER_REF_SELECT)
|
|
387
|
-
.from(users)
|
|
388
|
-
.where(eq(users.id, authorId))
|
|
389
|
-
.limit(1);
|
|
390
|
-
return {
|
|
391
|
-
id: post.id,
|
|
392
|
-
hubId: post.hubId,
|
|
393
|
-
type: post.type,
|
|
394
|
-
content: post.content,
|
|
395
|
-
isPinned: post.isPinned,
|
|
396
|
-
isLocked: post.isLocked,
|
|
397
|
-
likeCount: 0,
|
|
398
|
-
replyCount: 0,
|
|
399
|
-
createdAt: post.createdAt,
|
|
400
|
-
updatedAt: post.updatedAt,
|
|
401
|
-
author: author[0] ?? { id: authorId, username: 'unknown', displayName: null, avatarUrl: null },
|
|
402
|
-
};
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
export async function listPosts(db, hubId, filters = {}) {
|
|
406
|
-
const conditions = [eq(hubPosts.hubId, hubId)];
|
|
407
|
-
if (filters.type) {
|
|
408
|
-
conditions.push(eq(hubPosts.type, filters.type));
|
|
409
|
-
}
|
|
410
|
-
const where = and(...conditions);
|
|
411
|
-
const { limit, offset } = normalizePagination(filters);
|
|
412
|
-
const [rows, total] = await Promise.all([
|
|
413
|
-
db
|
|
414
|
-
.select({
|
|
415
|
-
post: hubPosts,
|
|
416
|
-
author: USER_REF_SELECT,
|
|
417
|
-
})
|
|
418
|
-
.from(hubPosts)
|
|
419
|
-
.innerJoin(users, eq(hubPosts.authorId, users.id))
|
|
420
|
-
.where(where)
|
|
421
|
-
.orderBy(desc(hubPosts.isPinned), desc(hubPosts.createdAt))
|
|
422
|
-
.limit(limit)
|
|
423
|
-
.offset(offset),
|
|
424
|
-
countRows(db, hubPosts, where),
|
|
425
|
-
]);
|
|
426
|
-
const items = rows.map((row) => {
|
|
427
|
-
const item = {
|
|
428
|
-
id: row.post.id,
|
|
429
|
-
hubId: row.post.hubId,
|
|
430
|
-
type: row.post.type,
|
|
431
|
-
content: row.post.content,
|
|
432
|
-
isPinned: row.post.isPinned,
|
|
433
|
-
isLocked: row.post.isLocked,
|
|
434
|
-
likeCount: row.post.likeCount,
|
|
435
|
-
replyCount: row.post.replyCount,
|
|
436
|
-
createdAt: row.post.createdAt,
|
|
437
|
-
updatedAt: row.post.updatedAt,
|
|
438
|
-
author: row.author,
|
|
439
|
-
};
|
|
440
|
-
if (row.post.type === 'share') {
|
|
441
|
-
try {
|
|
442
|
-
item.sharedContent = JSON.parse(row.post.content);
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
// Content is not valid JSON, leave as-is
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return item;
|
|
449
|
-
});
|
|
450
|
-
// Backfill missing cover images on share posts (for shares created before enrichment)
|
|
451
|
-
const sharesToEnrich = items.filter((i) => i.type === 'share' && i.sharedContent && !i.sharedContent.coverImageUrl && i.sharedContent.contentId);
|
|
452
|
-
if (sharesToEnrich.length > 0) {
|
|
453
|
-
const contentIds = sharesToEnrich.map((i) => i.sharedContent.contentId);
|
|
454
|
-
const enrichData = await db
|
|
455
|
-
.select({ id: contentItems.id, coverImageUrl: contentItems.coverImageUrl, description: contentItems.description })
|
|
456
|
-
.from(contentItems)
|
|
457
|
-
.where(inArray(contentItems.id, contentIds));
|
|
458
|
-
const enrichMap = new Map(enrichData.map((e) => [e.id, e]));
|
|
459
|
-
for (const item of sharesToEnrich) {
|
|
460
|
-
const sc = item.sharedContent;
|
|
461
|
-
const enrich = enrichMap.get(sc.contentId);
|
|
462
|
-
if (enrich) {
|
|
463
|
-
sc.coverImageUrl = enrich.coverImageUrl;
|
|
464
|
-
sc.description = enrich.description;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return { items, total };
|
|
469
|
-
}
|
|
470
|
-
export async function deletePost(db, postId, userId, hubId) {
|
|
471
|
-
const post = await db
|
|
472
|
-
.select({ authorId: hubPosts.authorId })
|
|
473
|
-
.from(hubPosts)
|
|
474
|
-
.where(eq(hubPosts.id, postId))
|
|
475
|
-
.limit(1);
|
|
476
|
-
if (post.length === 0)
|
|
477
|
-
return false;
|
|
478
|
-
if (post[0].authorId !== userId) {
|
|
479
|
-
const member = await db
|
|
480
|
-
.select({ role: hubMembers.role })
|
|
481
|
-
.from(hubMembers)
|
|
482
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
483
|
-
.limit(1);
|
|
484
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'deletePost')) {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
await db.delete(hubPosts).where(eq(hubPosts.id, postId));
|
|
489
|
-
await db
|
|
490
|
-
.update(hubs)
|
|
491
|
-
.set({ postCount: sql `GREATEST(${hubs.postCount} - 1, 0)` })
|
|
492
|
-
.where(eq(hubs.id, hubId));
|
|
493
|
-
return true;
|
|
494
|
-
}
|
|
495
|
-
export async function togglePinPost(db, postId, userId, hubId) {
|
|
496
|
-
const member = await db
|
|
497
|
-
.select({ role: hubMembers.role })
|
|
498
|
-
.from(hubMembers)
|
|
499
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
500
|
-
.limit(1);
|
|
501
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'pinPost')) {
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
const post = await db
|
|
505
|
-
.select({ isPinned: hubPosts.isPinned })
|
|
506
|
-
.from(hubPosts)
|
|
507
|
-
.where(eq(hubPosts.id, postId))
|
|
508
|
-
.limit(1);
|
|
509
|
-
if (post.length === 0)
|
|
510
|
-
return null;
|
|
511
|
-
const newPinned = !post[0].isPinned;
|
|
512
|
-
await db
|
|
513
|
-
.update(hubPosts)
|
|
514
|
-
.set({ isPinned: newPinned, updatedAt: new Date() })
|
|
515
|
-
.where(eq(hubPosts.id, postId));
|
|
516
|
-
return { pinned: newPinned };
|
|
517
|
-
}
|
|
518
|
-
export async function toggleLockPost(db, postId, userId, hubId) {
|
|
519
|
-
const member = await db
|
|
520
|
-
.select({ role: hubMembers.role })
|
|
521
|
-
.from(hubMembers)
|
|
522
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
523
|
-
.limit(1);
|
|
524
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'lockPost')) {
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
const post = await db
|
|
528
|
-
.select({ isLocked: hubPosts.isLocked })
|
|
529
|
-
.from(hubPosts)
|
|
530
|
-
.where(eq(hubPosts.id, postId))
|
|
531
|
-
.limit(1);
|
|
532
|
-
if (post.length === 0)
|
|
533
|
-
return null;
|
|
534
|
-
const newLocked = !post[0].isLocked;
|
|
535
|
-
await db
|
|
536
|
-
.update(hubPosts)
|
|
537
|
-
.set({ isLocked: newLocked, updatedAt: new Date() })
|
|
538
|
-
.where(eq(hubPosts.id, postId));
|
|
539
|
-
return { locked: newLocked };
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Get a single hub post by ID with author info.
|
|
543
|
-
*/
|
|
544
|
-
export async function getPostById(db, postId) {
|
|
545
|
-
const [row] = await db
|
|
546
|
-
.select({
|
|
547
|
-
post: hubPosts,
|
|
548
|
-
author: USER_REF_SELECT,
|
|
549
|
-
})
|
|
550
|
-
.from(hubPosts)
|
|
551
|
-
.innerJoin(users, eq(hubPosts.authorId, users.id))
|
|
552
|
-
.where(eq(hubPosts.id, postId))
|
|
553
|
-
.limit(1);
|
|
554
|
-
if (!row)
|
|
555
|
-
return null;
|
|
556
|
-
const item = {
|
|
557
|
-
id: row.post.id,
|
|
558
|
-
hubId: row.post.hubId,
|
|
559
|
-
type: row.post.type,
|
|
560
|
-
content: row.post.content,
|
|
561
|
-
isPinned: row.post.isPinned,
|
|
562
|
-
isLocked: row.post.isLocked,
|
|
563
|
-
likeCount: row.post.likeCount,
|
|
564
|
-
replyCount: row.post.replyCount,
|
|
565
|
-
createdAt: row.post.createdAt,
|
|
566
|
-
updatedAt: row.post.updatedAt,
|
|
567
|
-
author: row.author,
|
|
568
|
-
};
|
|
569
|
-
if (row.post.type === 'share') {
|
|
570
|
-
try {
|
|
571
|
-
item.sharedContent = JSON.parse(row.post.content);
|
|
572
|
-
}
|
|
573
|
-
catch { /* not JSON */ }
|
|
574
|
-
}
|
|
575
|
-
return item;
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Like a hub post. Returns true if liked, false if already liked.
|
|
579
|
-
*/
|
|
580
|
-
export async function likePost(db, userId, postId) {
|
|
581
|
-
// Use ON CONFLICT to handle concurrent requests atomically
|
|
582
|
-
const result = await db
|
|
583
|
-
.insert(hubPostLikes)
|
|
584
|
-
.values({ postId, userId })
|
|
585
|
-
.onConflictDoNothing({ target: [hubPostLikes.postId, hubPostLikes.userId] })
|
|
586
|
-
.returning({ id: hubPostLikes.id });
|
|
587
|
-
if (result.length === 0)
|
|
588
|
-
return false; // Already liked
|
|
589
|
-
await db.update(hubPosts).set({ likeCount: sql `${hubPosts.likeCount} + 1` }).where(eq(hubPosts.id, postId));
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Unlike a hub post. Returns true if unliked, false if wasn't liked.
|
|
594
|
-
*/
|
|
595
|
-
export async function unlikePost(db, userId, postId) {
|
|
596
|
-
const existing = await db
|
|
597
|
-
.select({ id: hubPostLikes.id })
|
|
598
|
-
.from(hubPostLikes)
|
|
599
|
-
.where(and(eq(hubPostLikes.postId, postId), eq(hubPostLikes.userId, userId)))
|
|
600
|
-
.limit(1);
|
|
601
|
-
if (existing.length === 0)
|
|
602
|
-
return false;
|
|
603
|
-
await db.delete(hubPostLikes).where(eq(hubPostLikes.id, existing[0].id));
|
|
604
|
-
await db.update(hubPosts).set({ likeCount: sql `GREATEST(${hubPosts.likeCount} - 1, 0)` }).where(eq(hubPosts.id, postId));
|
|
605
|
-
return true;
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Check if a user has liked a hub post.
|
|
609
|
-
*/
|
|
610
|
-
export async function hasLikedPost(db, userId, postId) {
|
|
611
|
-
const [row] = await db
|
|
612
|
-
.select({ id: hubPostLikes.id })
|
|
613
|
-
.from(hubPostLikes)
|
|
614
|
-
.where(and(eq(hubPostLikes.postId, postId), eq(hubPostLikes.userId, userId)))
|
|
615
|
-
.limit(1);
|
|
616
|
-
return !!row;
|
|
617
|
-
}
|
|
618
|
-
export async function createReply(db, authorId, input) {
|
|
619
|
-
const post = await db
|
|
620
|
-
.select({ hubId: hubPosts.hubId, isLocked: hubPosts.isLocked })
|
|
621
|
-
.from(hubPosts)
|
|
622
|
-
.where(eq(hubPosts.id, input.postId))
|
|
623
|
-
.limit(1);
|
|
624
|
-
if (post.length === 0)
|
|
625
|
-
throw new Error('Post not found');
|
|
626
|
-
if (post[0].isLocked)
|
|
627
|
-
throw new Error('Post is locked');
|
|
628
|
-
const member = await db
|
|
629
|
-
.select({ role: hubMembers.role })
|
|
630
|
-
.from(hubMembers)
|
|
631
|
-
.where(and(eq(hubMembers.hubId, post[0].hubId), eq(hubMembers.userId, authorId)))
|
|
632
|
-
.limit(1);
|
|
633
|
-
if (member.length === 0)
|
|
634
|
-
throw new Error('Must be a member to reply');
|
|
635
|
-
const ban = await checkBan(db, post[0].hubId, authorId);
|
|
636
|
-
if (ban)
|
|
637
|
-
throw new Error('You are banned from this hub');
|
|
638
|
-
const [reply] = await db
|
|
639
|
-
.insert(hubPostReplies)
|
|
640
|
-
.values({
|
|
641
|
-
postId: input.postId,
|
|
642
|
-
authorId,
|
|
643
|
-
content: input.content,
|
|
644
|
-
parentId: input.parentId ?? null,
|
|
645
|
-
})
|
|
646
|
-
.returning();
|
|
647
|
-
await db
|
|
648
|
-
.update(hubPosts)
|
|
649
|
-
.set({ replyCount: sql `${hubPosts.replyCount} + 1` })
|
|
650
|
-
.where(eq(hubPosts.id, input.postId));
|
|
651
|
-
const author = await db
|
|
652
|
-
.select(USER_REF_SELECT)
|
|
653
|
-
.from(users)
|
|
654
|
-
.where(eq(users.id, authorId))
|
|
655
|
-
.limit(1);
|
|
656
|
-
return {
|
|
657
|
-
id: reply.id,
|
|
658
|
-
postId: reply.postId,
|
|
659
|
-
content: reply.content,
|
|
660
|
-
likeCount: 0,
|
|
661
|
-
createdAt: reply.createdAt,
|
|
662
|
-
updatedAt: reply.updatedAt,
|
|
663
|
-
parentId: reply.parentId,
|
|
664
|
-
author: author[0],
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
export async function listReplies(db, postId, opts = {}) {
|
|
668
|
-
const { limit, offset } = normalizePagination(opts);
|
|
669
|
-
// Fetch root replies with pagination
|
|
670
|
-
const rootWhere = and(eq(hubPostReplies.postId, postId), isNull(hubPostReplies.parentId));
|
|
671
|
-
const [rootRows, total] = await Promise.all([
|
|
672
|
-
db
|
|
673
|
-
.select({ id: hubPostReplies.id })
|
|
674
|
-
.from(hubPostReplies)
|
|
675
|
-
.where(rootWhere)
|
|
676
|
-
.orderBy(desc(hubPostReplies.createdAt))
|
|
677
|
-
.limit(limit)
|
|
678
|
-
.offset(offset),
|
|
679
|
-
countRows(db, hubPostReplies, rootWhere),
|
|
680
|
-
]);
|
|
681
|
-
if (rootRows.length === 0)
|
|
682
|
-
return { items: [], total };
|
|
683
|
-
const rootIds = rootRows.map((r) => r.id);
|
|
684
|
-
// Fetch root + children in one query
|
|
685
|
-
const rows = await db
|
|
686
|
-
.select({
|
|
687
|
-
reply: hubPostReplies,
|
|
688
|
-
author: {
|
|
689
|
-
id: users.id,
|
|
690
|
-
username: users.username,
|
|
691
|
-
displayName: users.displayName,
|
|
692
|
-
avatarUrl: users.avatarUrl,
|
|
693
|
-
},
|
|
694
|
-
})
|
|
695
|
-
.from(hubPostReplies)
|
|
696
|
-
.innerJoin(users, eq(hubPostReplies.authorId, users.id))
|
|
697
|
-
.where(and(eq(hubPostReplies.postId, postId), or(and(isNull(hubPostReplies.parentId), inArray(hubPostReplies.id, rootIds)), inArray(hubPostReplies.parentId, rootIds))))
|
|
698
|
-
.orderBy(desc(hubPostReplies.createdAt));
|
|
699
|
-
const replyMap = new Map();
|
|
700
|
-
const rootReplies = [];
|
|
701
|
-
for (const row of rows) {
|
|
702
|
-
const item = {
|
|
703
|
-
id: row.reply.id,
|
|
704
|
-
postId: row.reply.postId,
|
|
705
|
-
content: row.reply.content,
|
|
706
|
-
likeCount: row.reply.likeCount,
|
|
707
|
-
createdAt: row.reply.createdAt,
|
|
708
|
-
updatedAt: row.reply.updatedAt,
|
|
709
|
-
parentId: row.reply.parentId,
|
|
710
|
-
author: row.author,
|
|
711
|
-
replies: [],
|
|
712
|
-
};
|
|
713
|
-
replyMap.set(item.id, item);
|
|
714
|
-
}
|
|
715
|
-
// Preserve root ordering
|
|
716
|
-
for (const rootId of rootIds) {
|
|
717
|
-
const item = replyMap.get(rootId);
|
|
718
|
-
if (item)
|
|
719
|
-
rootReplies.push(item);
|
|
720
|
-
}
|
|
721
|
-
for (const item of replyMap.values()) {
|
|
722
|
-
if (item.parentId && replyMap.has(item.parentId)) {
|
|
723
|
-
replyMap.get(item.parentId).replies.push(item);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return { items: rootReplies, total };
|
|
727
|
-
}
|
|
728
|
-
export async function deleteReply(db, replyId, userId, hubId) {
|
|
729
|
-
const reply = await db
|
|
730
|
-
.select({ authorId: hubPostReplies.authorId, postId: hubPostReplies.postId })
|
|
731
|
-
.from(hubPostReplies)
|
|
732
|
-
.where(eq(hubPostReplies.id, replyId))
|
|
733
|
-
.limit(1);
|
|
734
|
-
if (reply.length === 0)
|
|
735
|
-
return false;
|
|
736
|
-
if (reply[0].authorId !== userId) {
|
|
737
|
-
const member = await db
|
|
738
|
-
.select({ role: hubMembers.role })
|
|
739
|
-
.from(hubMembers)
|
|
740
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
741
|
-
.limit(1);
|
|
742
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'deletePost')) {
|
|
743
|
-
return false;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
await db.delete(hubPostReplies).where(eq(hubPostReplies.id, replyId));
|
|
747
|
-
await db
|
|
748
|
-
.update(hubPosts)
|
|
749
|
-
.set({ replyCount: sql `GREATEST(${hubPosts.replyCount} - 1, 0)` })
|
|
750
|
-
.where(eq(hubPosts.id, reply[0].postId));
|
|
751
|
-
return true;
|
|
752
|
-
}
|
|
753
|
-
// --- Bans ---
|
|
754
|
-
export async function banUser(db, actorId, hubId, targetUserId, reason, expiresAt) {
|
|
755
|
-
const [actorMember, targetMember] = await Promise.all([
|
|
756
|
-
db
|
|
757
|
-
.select({ role: hubMembers.role })
|
|
758
|
-
.from(hubMembers)
|
|
759
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, actorId)))
|
|
760
|
-
.limit(1),
|
|
761
|
-
db
|
|
762
|
-
.select({ role: hubMembers.role })
|
|
763
|
-
.from(hubMembers)
|
|
764
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)))
|
|
765
|
-
.limit(1),
|
|
766
|
-
]);
|
|
767
|
-
if (actorMember.length === 0 || !hasPermission(actorMember[0].role, 'banUser')) {
|
|
768
|
-
return { banned: false, error: 'Insufficient permissions' };
|
|
769
|
-
}
|
|
770
|
-
if (actorMember[0].role === 'moderator' && !expiresAt) {
|
|
771
|
-
return { banned: false, error: 'Moderators can only issue temporary bans' };
|
|
772
|
-
}
|
|
773
|
-
if (targetMember.length > 0) {
|
|
774
|
-
if (!canManageRole(actorMember[0].role, targetMember[0].role)) {
|
|
775
|
-
return { banned: false, error: 'Cannot ban a user with equal or higher role' };
|
|
776
|
-
}
|
|
777
|
-
await db
|
|
778
|
-
.delete(hubMembers)
|
|
779
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, targetUserId)));
|
|
780
|
-
await db
|
|
781
|
-
.update(hubs)
|
|
782
|
-
.set({ memberCount: sql `GREATEST(${hubs.memberCount} - 1, 0)` })
|
|
783
|
-
.where(eq(hubs.id, hubId));
|
|
784
|
-
}
|
|
785
|
-
await db.insert(hubBans).values({
|
|
786
|
-
hubId,
|
|
787
|
-
userId: targetUserId,
|
|
788
|
-
bannedById: actorId,
|
|
789
|
-
reason: reason ?? null,
|
|
790
|
-
expiresAt: expiresAt ?? null,
|
|
791
|
-
}).onConflictDoNothing();
|
|
792
|
-
return { banned: true };
|
|
793
|
-
}
|
|
794
|
-
export async function unbanUser(db, actorId, hubId, targetUserId) {
|
|
795
|
-
const actorMember = await db
|
|
796
|
-
.select({ role: hubMembers.role })
|
|
797
|
-
.from(hubMembers)
|
|
798
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, actorId)))
|
|
799
|
-
.limit(1);
|
|
800
|
-
if (actorMember.length === 0 || !hasPermission(actorMember[0].role, 'banUser')) {
|
|
801
|
-
return { unbanned: false, error: 'Insufficient permissions' };
|
|
802
|
-
}
|
|
803
|
-
await db
|
|
804
|
-
.delete(hubBans)
|
|
805
|
-
.where(and(eq(hubBans.hubId, hubId), eq(hubBans.userId, targetUserId)));
|
|
806
|
-
return { unbanned: true };
|
|
807
|
-
}
|
|
808
|
-
export async function checkBan(db, hubId, userId) {
|
|
809
|
-
const rows = await db
|
|
810
|
-
.select({
|
|
811
|
-
id: hubBans.id,
|
|
812
|
-
reason: hubBans.reason,
|
|
813
|
-
expiresAt: hubBans.expiresAt,
|
|
814
|
-
})
|
|
815
|
-
.from(hubBans)
|
|
816
|
-
.where(and(eq(hubBans.hubId, hubId), eq(hubBans.userId, userId)))
|
|
817
|
-
.limit(1);
|
|
818
|
-
if (rows.length === 0)
|
|
819
|
-
return null;
|
|
820
|
-
const ban = rows[0];
|
|
821
|
-
if (ban.expiresAt && ban.expiresAt < new Date()) {
|
|
822
|
-
await db.delete(hubBans).where(eq(hubBans.id, ban.id));
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
return ban;
|
|
826
|
-
}
|
|
827
|
-
export async function listBans(db, hubId, opts = {}) {
|
|
828
|
-
// Alias for the banner user (self-join on users table)
|
|
829
|
-
const bannerUser = {
|
|
830
|
-
bannerId: sql `banner.id`.as('banner_id'),
|
|
831
|
-
bannerUsername: sql `banner.username`.as('banner_username'),
|
|
832
|
-
bannerDisplayName: sql `banner.display_name`.as('banner_display_name'),
|
|
833
|
-
bannerAvatarUrl: sql `banner.avatar_url`.as('banner_avatar_url'),
|
|
834
|
-
};
|
|
835
|
-
const limit = Math.min(opts.limit ?? 50, 100);
|
|
836
|
-
const offset = opts.offset ?? 0;
|
|
837
|
-
const rows = await db
|
|
838
|
-
.select({
|
|
839
|
-
ban: hubBans,
|
|
840
|
-
user: USER_REF_SELECT,
|
|
841
|
-
...bannerUser,
|
|
842
|
-
})
|
|
843
|
-
.from(hubBans)
|
|
844
|
-
.innerJoin(users, eq(hubBans.userId, users.id))
|
|
845
|
-
.innerJoin(sql `users AS banner`, sql `banner.id = ${hubBans.bannedById}`)
|
|
846
|
-
.where(eq(hubBans.hubId, hubId))
|
|
847
|
-
.orderBy(desc(hubBans.createdAt))
|
|
848
|
-
.limit(limit)
|
|
849
|
-
.offset(offset);
|
|
850
|
-
return rows.map((row) => ({
|
|
851
|
-
id: row.ban.id,
|
|
852
|
-
reason: row.ban.reason,
|
|
853
|
-
expiresAt: row.ban.expiresAt,
|
|
854
|
-
createdAt: row.ban.createdAt,
|
|
855
|
-
user: row.user,
|
|
856
|
-
bannedBy: {
|
|
857
|
-
id: row.bannerId,
|
|
858
|
-
username: row.bannerUsername,
|
|
859
|
-
displayName: row.bannerDisplayName,
|
|
860
|
-
avatarUrl: row.bannerAvatarUrl,
|
|
861
|
-
},
|
|
862
|
-
}));
|
|
863
|
-
}
|
|
864
|
-
// --- Invites ---
|
|
865
|
-
export async function createInvite(db, userId, hubId, maxUses, expiresAt) {
|
|
866
|
-
const member = await db
|
|
867
|
-
.select({ role: hubMembers.role })
|
|
868
|
-
.from(hubMembers)
|
|
869
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
870
|
-
.limit(1);
|
|
871
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'manageMembers')) {
|
|
872
|
-
return null;
|
|
873
|
-
}
|
|
874
|
-
const token = crypto.randomUUID().replace(/-/g, '');
|
|
875
|
-
const [invite] = await db
|
|
876
|
-
.insert(hubInvites)
|
|
877
|
-
.values({
|
|
878
|
-
hubId,
|
|
879
|
-
createdById: userId,
|
|
880
|
-
token,
|
|
881
|
-
maxUses: maxUses ?? null,
|
|
882
|
-
expiresAt: expiresAt ?? null,
|
|
883
|
-
})
|
|
884
|
-
.returning();
|
|
885
|
-
const author = await db
|
|
886
|
-
.select(USER_REF_SELECT)
|
|
887
|
-
.from(users)
|
|
888
|
-
.where(eq(users.id, userId))
|
|
889
|
-
.limit(1);
|
|
890
|
-
return {
|
|
891
|
-
id: invite.id,
|
|
892
|
-
token: invite.token,
|
|
893
|
-
maxUses: invite.maxUses,
|
|
894
|
-
useCount: 0,
|
|
895
|
-
expiresAt: invite.expiresAt,
|
|
896
|
-
createdAt: invite.createdAt,
|
|
897
|
-
createdBy: author[0],
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
export async function validateAndUseInvite(db, token) {
|
|
901
|
-
const updated = await db
|
|
902
|
-
.update(hubInvites)
|
|
903
|
-
.set({ useCount: sql `${hubInvites.useCount} + 1` })
|
|
904
|
-
.where(and(eq(hubInvites.token, token), sql `(${hubInvites.expiresAt} IS NULL OR ${hubInvites.expiresAt} > NOW())`, sql `(${hubInvites.maxUses} IS NULL OR ${hubInvites.useCount} < ${hubInvites.maxUses})`))
|
|
905
|
-
.returning({ hubId: hubInvites.hubId });
|
|
906
|
-
if (updated.length === 0)
|
|
907
|
-
return { valid: false };
|
|
908
|
-
return { valid: true, hubId: updated[0].hubId };
|
|
909
|
-
}
|
|
910
|
-
export async function revokeInvite(db, inviteId, userId, hubId) {
|
|
911
|
-
const member = await db
|
|
912
|
-
.select({ role: hubMembers.role })
|
|
913
|
-
.from(hubMembers)
|
|
914
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
915
|
-
.limit(1);
|
|
916
|
-
if (member.length === 0 || !hasPermission(member[0].role, 'manageMembers')) {
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
await db.delete(hubInvites).where(eq(hubInvites.id, inviteId));
|
|
920
|
-
return true;
|
|
921
|
-
}
|
|
922
|
-
export async function listInvites(db, hubId, opts = {}) {
|
|
923
|
-
const limit = Math.min(opts.limit ?? 50, 100);
|
|
924
|
-
const offset = opts.offset ?? 0;
|
|
925
|
-
const rows = await db
|
|
926
|
-
.select({
|
|
927
|
-
invite: hubInvites,
|
|
928
|
-
createdBy: {
|
|
929
|
-
id: users.id,
|
|
930
|
-
username: users.username,
|
|
931
|
-
displayName: users.displayName,
|
|
932
|
-
avatarUrl: users.avatarUrl,
|
|
933
|
-
},
|
|
934
|
-
})
|
|
935
|
-
.from(hubInvites)
|
|
936
|
-
.innerJoin(users, eq(hubInvites.createdById, users.id))
|
|
937
|
-
.where(eq(hubInvites.hubId, hubId))
|
|
938
|
-
.orderBy(desc(hubInvites.createdAt))
|
|
939
|
-
.limit(limit)
|
|
940
|
-
.offset(offset);
|
|
941
|
-
return rows.map((row) => ({
|
|
942
|
-
id: row.invite.id,
|
|
943
|
-
token: row.invite.token,
|
|
944
|
-
maxUses: row.invite.maxUses,
|
|
945
|
-
useCount: row.invite.useCount,
|
|
946
|
-
expiresAt: row.invite.expiresAt,
|
|
947
|
-
createdAt: row.invite.createdAt,
|
|
948
|
-
createdBy: row.createdBy,
|
|
949
|
-
}));
|
|
950
|
-
}
|
|
951
|
-
// --- Content Sharing ---
|
|
952
|
-
export async function shareContent(db, userId, hubId, contentId) {
|
|
953
|
-
const member = await db
|
|
954
|
-
.select({ role: hubMembers.role })
|
|
955
|
-
.from(hubMembers)
|
|
956
|
-
.where(and(eq(hubMembers.hubId, hubId), eq(hubMembers.userId, userId)))
|
|
957
|
-
.limit(1);
|
|
958
|
-
if (member.length === 0)
|
|
959
|
-
return null;
|
|
960
|
-
const content = await db
|
|
961
|
-
.select({
|
|
962
|
-
id: contentItems.id,
|
|
963
|
-
title: contentItems.title,
|
|
964
|
-
slug: contentItems.slug,
|
|
965
|
-
type: contentItems.type,
|
|
966
|
-
coverImageUrl: contentItems.coverImageUrl,
|
|
967
|
-
description: contentItems.description,
|
|
968
|
-
})
|
|
969
|
-
.from(contentItems)
|
|
970
|
-
.where(eq(contentItems.id, contentId))
|
|
971
|
-
.limit(1);
|
|
972
|
-
if (content.length === 0)
|
|
973
|
-
return null;
|
|
974
|
-
// Check for duplicate share
|
|
975
|
-
const existing = await db
|
|
976
|
-
.select({ id: hubShares.id })
|
|
977
|
-
.from(hubShares)
|
|
978
|
-
.where(and(eq(hubShares.hubId, hubId), eq(hubShares.contentId, contentId)))
|
|
979
|
-
.limit(1);
|
|
980
|
-
if (existing.length > 0)
|
|
981
|
-
return null;
|
|
982
|
-
const sharePayload = JSON.stringify({
|
|
983
|
-
contentId: content[0].id,
|
|
984
|
-
title: content[0].title,
|
|
985
|
-
slug: content[0].slug,
|
|
986
|
-
type: content[0].type,
|
|
987
|
-
coverImageUrl: content[0].coverImageUrl ?? null,
|
|
988
|
-
description: content[0].description ?? null,
|
|
989
|
-
});
|
|
990
|
-
await db.insert(hubShares).values({
|
|
991
|
-
hubId,
|
|
992
|
-
contentId,
|
|
993
|
-
sharedById: userId,
|
|
994
|
-
});
|
|
995
|
-
return createPost(db, userId, {
|
|
996
|
-
hubId,
|
|
997
|
-
type: 'share',
|
|
998
|
-
content: sharePayload,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
export async function unshareContent(db, userId, hubId, contentId) {
|
|
1002
|
-
const share = await db
|
|
1003
|
-
.select({ id: hubShares.id })
|
|
1004
|
-
.from(hubShares)
|
|
1005
|
-
.where(and(eq(hubShares.hubId, hubId), eq(hubShares.contentId, contentId), eq(hubShares.sharedById, userId)))
|
|
1006
|
-
.limit(1);
|
|
1007
|
-
if (share.length === 0)
|
|
1008
|
-
return false;
|
|
1009
|
-
await db.delete(hubShares).where(eq(hubShares.id, share[0].id));
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
export async function listShares(db, hubId) {
|
|
1013
|
-
return db
|
|
1014
|
-
.select({
|
|
1015
|
-
id: hubShares.id,
|
|
1016
|
-
contentId: hubShares.contentId,
|
|
1017
|
-
sharedById: hubShares.sharedById,
|
|
1018
|
-
createdAt: hubShares.createdAt,
|
|
1019
|
-
})
|
|
1020
|
-
.from(hubShares)
|
|
1021
|
-
.where(eq(hubShares.hubId, hubId))
|
|
1022
|
-
.orderBy(desc(hubShares.createdAt));
|
|
1023
|
-
}
|
|
1024
227
|
//# sourceMappingURL=hub.js.map
|