@lobehub/lobehub 2.0.0-next.225 → 2.0.0-next.226
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 +25 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/setting.json +4 -0
- package/locales/zh-CN/setting.json +4 -0
- package/package.json +2 -2
- package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/ForkConfirmModal.tsx +67 -0
- package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/PublishButton.tsx +92 -105
- package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/index.tsx +13 -48
- package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/useMarketPublish.ts +69 -93
- package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +11 -28
- package/src/layout/AuthProvider/MarketAuth/ProfileSetupModal.tsx +30 -25
- package/src/layout/AuthProvider/MarketAuth/useMarketUserProfile.ts +4 -9
- package/src/libs/trpc/lambda/middleware/index.ts +1 -0
- package/src/libs/trpc/lambda/middleware/marketSDK.ts +68 -0
- package/src/locales/default/setting.ts +5 -0
- package/src/server/routers/lambda/market/agent.ts +504 -0
- package/src/server/routers/lambda/market/index.ts +17 -0
- package/src/server/routers/lambda/market/oidc.ts +169 -0
- package/src/server/routers/lambda/market/social.ts +532 -0
- package/src/server/routers/lambda/market/user.ts +123 -0
- package/src/services/marketApi.ts +24 -84
- package/src/services/social.ts +70 -166
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
|
|
6
|
+
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
7
|
+
|
|
8
|
+
const log = debug('lambda-router:market:social');
|
|
9
|
+
|
|
10
|
+
// Authenticated procedure for social actions that require login
|
|
11
|
+
const socialAuthProcedure = authedProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
|
|
12
|
+
|
|
13
|
+
// Public procedure with optional auth for status checks
|
|
14
|
+
const socialPublicProcedure = publicProcedure
|
|
15
|
+
.use(serverDatabase)
|
|
16
|
+
.use(marketUserInfo)
|
|
17
|
+
.use(marketSDK);
|
|
18
|
+
|
|
19
|
+
// Schema definitions
|
|
20
|
+
const targetTypeSchema = z.enum(['agent', 'plugin']);
|
|
21
|
+
|
|
22
|
+
const paginationSchema = z.object({
|
|
23
|
+
limit: z.number().optional(),
|
|
24
|
+
offset: z.number().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const socialRouter = router({
|
|
28
|
+
// ============================== Favorite Actions ==============================
|
|
29
|
+
/**
|
|
30
|
+
* Add to favorites
|
|
31
|
+
* POST /market/social/favorite
|
|
32
|
+
*/
|
|
33
|
+
addFavorite: socialAuthProcedure
|
|
34
|
+
.input(
|
|
35
|
+
z.object({
|
|
36
|
+
identifier: z.string().optional(),
|
|
37
|
+
targetId: z.number().optional(),
|
|
38
|
+
targetType: targetTypeSchema,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
.mutation(async ({ input, ctx }) => {
|
|
42
|
+
log('addFavorite input: %O', input);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const targetValue = input.identifier ?? input.targetId;
|
|
46
|
+
if (!targetValue) {
|
|
47
|
+
throw new Error('Either identifier or targetId is required');
|
|
48
|
+
}
|
|
49
|
+
await ctx.marketSDK.favorites.addFavorite(input.targetType, targetValue as any);
|
|
50
|
+
return { success: true };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
log('Error adding favorite: %O', error);
|
|
53
|
+
throw new TRPCError({
|
|
54
|
+
cause: error,
|
|
55
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
56
|
+
message: error instanceof Error ? error.message : 'Failed to add favorite',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if item is favorited
|
|
63
|
+
* GET /market/social/favorite-status/[targetType]/[targetId]
|
|
64
|
+
*/
|
|
65
|
+
checkFavorite: socialPublicProcedure
|
|
66
|
+
.input(
|
|
67
|
+
z.object({
|
|
68
|
+
targetIdOrIdentifier: z.union([z.number(), z.string()]),
|
|
69
|
+
targetType: targetTypeSchema,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
.query(async ({ input, ctx }) => {
|
|
73
|
+
log('checkFavorite input: %O', input);
|
|
74
|
+
|
|
75
|
+
if (!ctx.marketSDK) {
|
|
76
|
+
return { isFavorited: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await ctx.marketSDK.favorites.checkFavorite(
|
|
81
|
+
input.targetType,
|
|
82
|
+
input.targetIdOrIdentifier as any,
|
|
83
|
+
);
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
log('Error checking favorite: %O', error);
|
|
87
|
+
return { isFavorited: false };
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
// ============================== Follow Actions ==============================
|
|
92
|
+
/**
|
|
93
|
+
* Check follow status between current user and target user
|
|
94
|
+
* GET /market/social/follow-status/[userId]
|
|
95
|
+
*/
|
|
96
|
+
checkFollowStatus: socialPublicProcedure
|
|
97
|
+
.input(z.object({ targetUserId: z.number() }))
|
|
98
|
+
.query(async ({ input, ctx }) => {
|
|
99
|
+
log('checkFollowStatus input: %O', input);
|
|
100
|
+
|
|
101
|
+
// If no auth, return default status
|
|
102
|
+
if (!ctx.marketSDK) {
|
|
103
|
+
return { isFollowing: false, isMutual: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = await ctx.marketSDK.follows.checkFollowStatus(input.targetUserId);
|
|
108
|
+
return result;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log('Error checking follow status: %O', error);
|
|
111
|
+
// Return default on error (user might not be authenticated)
|
|
112
|
+
return { isFollowing: false, isMutual: false };
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
// ============================== Like Actions ==============================
|
|
117
|
+
/**
|
|
118
|
+
* Check if item is liked
|
|
119
|
+
* GET /market/social/like-status/[targetType]/[targetId]
|
|
120
|
+
*/
|
|
121
|
+
checkLike: socialPublicProcedure
|
|
122
|
+
.input(
|
|
123
|
+
z.object({
|
|
124
|
+
targetIdOrIdentifier: z.union([z.number(), z.string()]),
|
|
125
|
+
targetType: targetTypeSchema,
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
.query(async ({ input, ctx }) => {
|
|
129
|
+
log('checkLike input: %O', input);
|
|
130
|
+
|
|
131
|
+
if (!ctx.marketSDK) {
|
|
132
|
+
return { isLiked: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await ctx.marketSDK.likes.checkLike(
|
|
137
|
+
input.targetType,
|
|
138
|
+
input.targetIdOrIdentifier as any,
|
|
139
|
+
);
|
|
140
|
+
return result;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log('Error checking like: %O', error);
|
|
143
|
+
return { isLiked: false };
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Follow a user
|
|
149
|
+
* POST /market/social/follow
|
|
150
|
+
*/
|
|
151
|
+
follow: socialAuthProcedure
|
|
152
|
+
.input(z.object({ followingId: z.number() }))
|
|
153
|
+
.mutation(async ({ input, ctx }) => {
|
|
154
|
+
log('follow input: %O', input);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await ctx.marketSDK.follows.follow(input.followingId);
|
|
158
|
+
return { success: true };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
log('Error following user: %O', error);
|
|
161
|
+
throw new TRPCError({
|
|
162
|
+
cause: error,
|
|
163
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
164
|
+
message: error instanceof Error ? error.message : 'Failed to follow user',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get follow counts for a user
|
|
171
|
+
* GET /market/social/follow-counts/[userId]
|
|
172
|
+
*/
|
|
173
|
+
getFollowCounts: socialPublicProcedure
|
|
174
|
+
.input(z.object({ userId: z.number() }))
|
|
175
|
+
.query(async ({ input, ctx }) => {
|
|
176
|
+
log('getFollowCounts input: %O', input);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const [following, followers] = await Promise.all([
|
|
180
|
+
ctx.marketSDK.follows.getFollowing(input.userId, { limit: 1 }),
|
|
181
|
+
ctx.marketSDK.follows.getFollowers(input.userId, { limit: 1 }),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
followersCount: (followers as any).totalCount || (followers as any).total || 0,
|
|
186
|
+
followingCount: (following as any).totalCount || (following as any).total || 0,
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
log('Error getting follow counts: %O', error);
|
|
190
|
+
throw new TRPCError({
|
|
191
|
+
cause: error,
|
|
192
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
193
|
+
message: error instanceof Error ? error.message : 'Failed to get follow counts',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get followers of a user
|
|
200
|
+
* GET /market/social/followers/[userId]
|
|
201
|
+
*/
|
|
202
|
+
getFollowers: socialPublicProcedure
|
|
203
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
204
|
+
.query(async ({ input, ctx }) => {
|
|
205
|
+
log('getFollowers input: %O', input);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const { userId, ...params } = input;
|
|
209
|
+
const result = await ctx.marketSDK.follows.getFollowers(userId, params);
|
|
210
|
+
return result;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
log('Error getting followers: %O', error);
|
|
213
|
+
throw new TRPCError({
|
|
214
|
+
cause: error,
|
|
215
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
216
|
+
message: error instanceof Error ? error.message : 'Failed to get followers',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}),
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get users that a user is following
|
|
223
|
+
* GET /market/social/following/[userId]
|
|
224
|
+
*/
|
|
225
|
+
getFollowing: socialPublicProcedure
|
|
226
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
227
|
+
.query(async ({ input, ctx }) => {
|
|
228
|
+
log('getFollowing input: %O', input);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const { userId, ...params } = input;
|
|
232
|
+
const result = await ctx.marketSDK.follows.getFollowing(userId, params);
|
|
233
|
+
return result;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
log('Error getting following: %O', error);
|
|
236
|
+
throw new TRPCError({
|
|
237
|
+
cause: error,
|
|
238
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
239
|
+
message: error instanceof Error ? error.message : 'Failed to get following',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get current user's favorites
|
|
246
|
+
* GET /market/social/favorites
|
|
247
|
+
*/
|
|
248
|
+
getMyFavorites: socialAuthProcedure
|
|
249
|
+
.input(paginationSchema.optional())
|
|
250
|
+
.query(async ({ input, ctx }) => {
|
|
251
|
+
log('getMyFavorites input: %O', input);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = await ctx.marketSDK.favorites.getMyFavorites(input);
|
|
255
|
+
return result;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
log('Error getting my favorites: %O', error);
|
|
258
|
+
throw new TRPCError({
|
|
259
|
+
cause: error,
|
|
260
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
261
|
+
message: error instanceof Error ? error.message : 'Failed to get favorites',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get user's favorite agents
|
|
268
|
+
* GET /market/social/favorite-agents/[userId]
|
|
269
|
+
*/
|
|
270
|
+
getUserFavoriteAgents: socialPublicProcedure
|
|
271
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
272
|
+
.query(async ({ input, ctx }) => {
|
|
273
|
+
log('getUserFavoriteAgents input: %O', input);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const { userId, ...params } = input;
|
|
277
|
+
const result = await ctx.marketSDK.favorites.getUserFavoriteAgents(userId, params);
|
|
278
|
+
return result;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
log('Error getting user favorite agents: %O', error);
|
|
281
|
+
throw new TRPCError({
|
|
282
|
+
cause: error,
|
|
283
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
284
|
+
message: error instanceof Error ? error.message : 'Failed to get favorite agents',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}),
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get user's favorite plugins
|
|
291
|
+
* GET /market/social/favorite-plugins/[userId]
|
|
292
|
+
*/
|
|
293
|
+
getUserFavoritePlugins: socialPublicProcedure
|
|
294
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
295
|
+
.query(async ({ input, ctx }) => {
|
|
296
|
+
log('getUserFavoritePlugins input: %O', input);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const { userId, ...params } = input;
|
|
300
|
+
const result = await ctx.marketSDK.favorites.getUserFavoritePlugins(userId, params);
|
|
301
|
+
return result;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
log('Error getting user favorite plugins: %O', error);
|
|
304
|
+
throw new TRPCError({
|
|
305
|
+
cause: error,
|
|
306
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
307
|
+
message: error instanceof Error ? error.message : 'Failed to get favorite plugins',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}),
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get user's all favorites
|
|
314
|
+
* GET /market/social/user-favorites/[userId]
|
|
315
|
+
*/
|
|
316
|
+
getUserFavorites: socialPublicProcedure
|
|
317
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
318
|
+
.query(async ({ input, ctx }) => {
|
|
319
|
+
log('getUserFavorites input: %O', input);
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const { userId, ...params } = input;
|
|
323
|
+
const result = await ctx.marketSDK.favorites.getUserFavorites(userId, params);
|
|
324
|
+
return result;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
log('Error getting user favorites: %O', error);
|
|
327
|
+
throw new TRPCError({
|
|
328
|
+
cause: error,
|
|
329
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
330
|
+
message: error instanceof Error ? error.message : 'Failed to get user favorites',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}),
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get user's liked agents
|
|
337
|
+
* GET /market/social/liked-agents/[userId]
|
|
338
|
+
*/
|
|
339
|
+
getUserLikedAgents: socialPublicProcedure
|
|
340
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
341
|
+
.query(async ({ input, ctx }) => {
|
|
342
|
+
log('getUserLikedAgents input: %O', input);
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const { userId, ...params } = input;
|
|
346
|
+
const result = await ctx.marketSDK.likes.getUserLikedAgents(userId, params);
|
|
347
|
+
return result;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
log('Error getting user liked agents: %O', error);
|
|
350
|
+
throw new TRPCError({
|
|
351
|
+
cause: error,
|
|
352
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
353
|
+
message: error instanceof Error ? error.message : 'Failed to get liked agents',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}),
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get user's liked plugins
|
|
360
|
+
* GET /market/social/liked-plugins/[userId]
|
|
361
|
+
*/
|
|
362
|
+
getUserLikedPlugins: socialPublicProcedure
|
|
363
|
+
.input(z.object({ userId: z.number() }).merge(paginationSchema))
|
|
364
|
+
.query(async ({ input, ctx }) => {
|
|
365
|
+
log('getUserLikedPlugins input: %O', input);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const { userId, ...params } = input;
|
|
369
|
+
const result = await ctx.marketSDK.likes.getUserLikedPlugins(userId, params);
|
|
370
|
+
return result;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
log('Error getting user liked plugins: %O', error);
|
|
373
|
+
throw new TRPCError({
|
|
374
|
+
cause: error,
|
|
375
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
376
|
+
message: error instanceof Error ? error.message : 'Failed to get liked plugins',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}),
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Like an item
|
|
383
|
+
* POST /market/social/like
|
|
384
|
+
*/
|
|
385
|
+
like: socialAuthProcedure
|
|
386
|
+
.input(
|
|
387
|
+
z.object({
|
|
388
|
+
identifier: z.string().optional(),
|
|
389
|
+
targetId: z.number().optional(),
|
|
390
|
+
targetType: targetTypeSchema,
|
|
391
|
+
}),
|
|
392
|
+
)
|
|
393
|
+
.mutation(async ({ input, ctx }) => {
|
|
394
|
+
log('like input: %O', input);
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const targetValue = input.identifier ?? input.targetId;
|
|
398
|
+
if (!targetValue) {
|
|
399
|
+
throw new Error('Either identifier or targetId is required');
|
|
400
|
+
}
|
|
401
|
+
await ctx.marketSDK.likes.like(input.targetType, targetValue as any);
|
|
402
|
+
return { success: true };
|
|
403
|
+
} catch (error) {
|
|
404
|
+
log('Error liking item: %O', error);
|
|
405
|
+
throw new TRPCError({
|
|
406
|
+
cause: error,
|
|
407
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
408
|
+
message: error instanceof Error ? error.message : 'Failed to like item',
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}),
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Remove from favorites
|
|
415
|
+
* POST /market/social/unfavorite
|
|
416
|
+
*/
|
|
417
|
+
removeFavorite: socialAuthProcedure
|
|
418
|
+
.input(
|
|
419
|
+
z.object({
|
|
420
|
+
identifier: z.string().optional(),
|
|
421
|
+
targetId: z.number().optional(),
|
|
422
|
+
targetType: targetTypeSchema,
|
|
423
|
+
}),
|
|
424
|
+
)
|
|
425
|
+
.mutation(async ({ input, ctx }) => {
|
|
426
|
+
log('removeFavorite input: %O', input);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const targetValue = input.identifier ?? input.targetId;
|
|
430
|
+
if (!targetValue) {
|
|
431
|
+
throw new Error('Either identifier or targetId is required');
|
|
432
|
+
}
|
|
433
|
+
await ctx.marketSDK.favorites.removeFavorite(input.targetType, targetValue as any);
|
|
434
|
+
return { success: true };
|
|
435
|
+
} catch (error) {
|
|
436
|
+
log('Error removing favorite: %O', error);
|
|
437
|
+
throw new TRPCError({
|
|
438
|
+
cause: error,
|
|
439
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
440
|
+
message: error instanceof Error ? error.message : 'Failed to remove favorite',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}),
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Toggle like on an item
|
|
447
|
+
* POST /market/social/toggle-like
|
|
448
|
+
*/
|
|
449
|
+
toggleLike: socialAuthProcedure
|
|
450
|
+
.input(
|
|
451
|
+
z.object({
|
|
452
|
+
identifier: z.string().optional(),
|
|
453
|
+
targetId: z.number().optional(),
|
|
454
|
+
targetType: targetTypeSchema,
|
|
455
|
+
}),
|
|
456
|
+
)
|
|
457
|
+
.mutation(async ({ input, ctx }) => {
|
|
458
|
+
log('toggleLike input: %O', input);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const targetValue = input.identifier ?? input.targetId;
|
|
462
|
+
if (!targetValue) {
|
|
463
|
+
throw new Error('Either identifier or targetId is required');
|
|
464
|
+
}
|
|
465
|
+
const result = await ctx.marketSDK.likes.toggleLike(input.targetType, targetValue as any);
|
|
466
|
+
return result;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
log('Error toggling like: %O', error);
|
|
469
|
+
throw new TRPCError({
|
|
470
|
+
cause: error,
|
|
471
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
472
|
+
message: error instanceof Error ? error.message : 'Failed to toggle like',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}),
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Unfollow a user
|
|
479
|
+
* POST /market/social/unfollow
|
|
480
|
+
*/
|
|
481
|
+
unfollow: socialAuthProcedure
|
|
482
|
+
.input(z.object({ followingId: z.number() }))
|
|
483
|
+
.mutation(async ({ input, ctx }) => {
|
|
484
|
+
log('unfollow input: %O', input);
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
await ctx.marketSDK.follows.unfollow(input.followingId);
|
|
488
|
+
return { success: true };
|
|
489
|
+
} catch (error) {
|
|
490
|
+
log('Error unfollowing user: %O', error);
|
|
491
|
+
throw new TRPCError({
|
|
492
|
+
cause: error,
|
|
493
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
494
|
+
message: error instanceof Error ? error.message : 'Failed to unfollow user',
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}),
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Unlike an item
|
|
501
|
+
* POST /market/social/unlike
|
|
502
|
+
*/
|
|
503
|
+
unlike: socialAuthProcedure
|
|
504
|
+
.input(
|
|
505
|
+
z.object({
|
|
506
|
+
identifier: z.string().optional(),
|
|
507
|
+
targetId: z.number().optional(),
|
|
508
|
+
targetType: targetTypeSchema,
|
|
509
|
+
}),
|
|
510
|
+
)
|
|
511
|
+
.mutation(async ({ input, ctx }) => {
|
|
512
|
+
log('unlike input: %O', input);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const targetValue = input.identifier ?? input.targetId;
|
|
516
|
+
if (!targetValue) {
|
|
517
|
+
throw new Error('Either identifier or targetId is required');
|
|
518
|
+
}
|
|
519
|
+
await ctx.marketSDK.likes.unlike(input.targetType, targetValue as any);
|
|
520
|
+
return { success: true };
|
|
521
|
+
} catch (error) {
|
|
522
|
+
log('Error unliking item: %O', error);
|
|
523
|
+
throw new TRPCError({
|
|
524
|
+
cause: error,
|
|
525
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
526
|
+
message: error instanceof Error ? error.message : 'Failed to unlike item',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
export type SocialRouter = typeof socialRouter;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
|
|
6
|
+
import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
7
|
+
|
|
8
|
+
const log = debug('lambda-router:market:user');
|
|
9
|
+
|
|
10
|
+
// Authenticated procedure for user profile updates
|
|
11
|
+
const userAuthProcedure = authedProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
|
|
12
|
+
|
|
13
|
+
// Public procedure for viewing user profiles
|
|
14
|
+
const userPublicProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo).use(marketSDK);
|
|
15
|
+
|
|
16
|
+
// Schema definitions
|
|
17
|
+
const socialLinksSchema = z.object({
|
|
18
|
+
github: z.string().optional(),
|
|
19
|
+
twitter: z.string().optional(),
|
|
20
|
+
website: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const userMetaSchema = z.object({
|
|
24
|
+
bannerUrl: z.string().optional(),
|
|
25
|
+
description: z.string().optional(),
|
|
26
|
+
socialLinks: socialLinksSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const updateUserProfileSchema = z.object({
|
|
30
|
+
avatarUrl: z.string().optional(),
|
|
31
|
+
displayName: z.string().optional(),
|
|
32
|
+
meta: userMetaSchema.optional(),
|
|
33
|
+
userName: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const userRouter = router({
|
|
37
|
+
/**
|
|
38
|
+
* Get user profile by username
|
|
39
|
+
* GET /market/user/[username]
|
|
40
|
+
*/
|
|
41
|
+
getUserByUsername: userPublicProcedure
|
|
42
|
+
.input(z.object({ username: z.string() }))
|
|
43
|
+
.query(async ({ input, ctx }) => {
|
|
44
|
+
log('getUserByUsername input: %O', input);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await ctx.marketSDK.user.getUserInfo(input.username);
|
|
48
|
+
|
|
49
|
+
if (!response?.user) {
|
|
50
|
+
throw new TRPCError({
|
|
51
|
+
code: 'NOT_FOUND',
|
|
52
|
+
message: `User not found: ${input.username}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { user } = response;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
avatarUrl: user.avatarUrl || null,
|
|
60
|
+
bannerUrl: user.meta?.bannerUrl || null,
|
|
61
|
+
createdAt: user.createdAt,
|
|
62
|
+
description: user.meta?.description || null,
|
|
63
|
+
displayName: user.displayName || null,
|
|
64
|
+
id: user.id,
|
|
65
|
+
namespace: user.namespace,
|
|
66
|
+
socialLinks: user.meta?.socialLinks || null,
|
|
67
|
+
type: user.type || null,
|
|
68
|
+
userName: user.userName || null,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof TRPCError) throw error;
|
|
72
|
+
|
|
73
|
+
log('Error getting user profile: %O', error);
|
|
74
|
+
throw new TRPCError({
|
|
75
|
+
cause: error,
|
|
76
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
77
|
+
message: error instanceof Error ? error.message : 'Failed to get user profile',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update current user's profile
|
|
84
|
+
* PUT /market/user/me
|
|
85
|
+
*/
|
|
86
|
+
updateUserProfile: userAuthProcedure
|
|
87
|
+
.input(updateUserProfileSchema)
|
|
88
|
+
.mutation(async ({ input, ctx }) => {
|
|
89
|
+
log('updateUserProfile input: %O', input);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Ensure meta is at least an empty object
|
|
93
|
+
const normalizedPayload = {
|
|
94
|
+
...input,
|
|
95
|
+
meta: input.meta ?? {},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const response = await ctx.marketSDK.user.updateUserInfo(normalizedPayload);
|
|
99
|
+
return response;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log('Error updating user profile: %O', error);
|
|
102
|
+
|
|
103
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
104
|
+
const isUserNameTaken = errorMessage.toLowerCase().includes('already taken');
|
|
105
|
+
|
|
106
|
+
if (isUserNameTaken) {
|
|
107
|
+
throw new TRPCError({
|
|
108
|
+
cause: error,
|
|
109
|
+
code: 'CONFLICT',
|
|
110
|
+
message: 'Username is already taken',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new TRPCError({
|
|
115
|
+
cause: error,
|
|
116
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
117
|
+
message: errorMessage,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export type UserRouter = typeof userRouter;
|