返回 AiToEarn
engagement.service.ts
1 import type { AccountType } from '@yikart/common'
2 import type {
3 ChannelPaginationInput,
4 CredentialContext,
5 EngagementAccountActionInput,
6 EngagementCommentActionInput,
7 EngagementCommentInput,
8 EngagementDeleteInput,
9 EngagementLikeInput,
10 EngagementListInput,
11 EngagementQuoteInput,
12 } from '../platforms/platforms.interface'
13 import { Injectable } from '@nestjs/common'
14 import { AppException, ResponseCode } from '@yikart/common'
15 import { AccountRepository } from '@yikart/mongodb'
16 import { AuthService } from '../auth/auth.service'
17 import { normalizeChannelPagination } from '../platforms/platform-pagination.helper'
18 import { ChannelEngagementFunctionName } from '../platforms/platforms.interface'
19 import { PlatformIntegrationRegistry } from '../platforms/platforms.registry'
20 import { RelayAccountException } from '../relay/relay-account.exception'
21 import {
22 CallEngagementFunctionBodyDto,
23 DeleteEngagementCommentFunctionDataSchema,
24 EngagementAccountFunctionDataSchema,
25 EngagementCommentFunctionDataSchema,
26 EngagementWorkFunctionDataSchema,
27 QuoteEngagementWorkFunctionDataSchema,
28 } from './engagement.dto'
29
30 @Injectable()
31 export class EngagementService {
32 constructor(
33 private readonly registry: PlatformIntegrationRegistry,
34 private readonly authService: AuthService,
35 private readonly accountRepository: AccountRepository,
36 ) {}
37
38 async listComments(
39 userId: string,
40 platform: AccountType,
41 platformWorkId: string,
42 accountId: string,
43 pagination: ChannelPaginationInput,
44 ) {
45 const provider = this.registry.getEngagement(platform)
46 if (!provider?.listComments) {
47 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
48 platform,
49 capability: 'engagement.listComments',
50 })
51 }
52 const input: EngagementListInput = {
53 accountId,
54 platformWorkId,
55 platform,
56 credential: await this.getPlatformCredential(userId, accountId, platform),
57 pagination: normalizeChannelPagination(
58 platform,
59 provider.commentPagination,
60 pagination,
61 ),
62 }
63
64 const result = await this.callPlatformProvider(accountId, () => provider.listComments!(input))
65 return {
66 platform,
67 items: result.items,
68 pagination: result.pagination,
69 }
70 }
71
72 async createComment(
73 userId: string,
74 accountId: string,
75 platform: AccountType,
76 platformWorkId: string,
77 content: string,
78 parentCommentId?: string,
79 ) {
80 const provider = this.registry.getEngagement(platform)
81 if (!provider?.createComment) {
82 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
83 platform,
84 capability: 'engagement.createComment',
85 })
86 }
87 const input: EngagementCommentInput = {
88 accountId,
89 platformWorkId,
90 platform,
91 credential: await this.getPlatformCredential(userId, accountId, platform),
92 content,
93 replyToId: parentCommentId,
94 }
95
96 return {
97 platform,
98 ...await this.callPlatformProvider(accountId, () => provider.createComment!(input)),
99 }
100 }
101
102 async callFunction(userId: string, accountId: string, body: CallEngagementFunctionBodyDto) {
103 const provider = this.registry.getEngagement(body.platform)
104 if (!provider) {
105 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
106 platform: body.platform,
107 capability: 'engagement',
108 })
109 }
110
111 switch (body.name) {
112 case ChannelEngagementFunctionName.DeleteComment: {
113 if (!provider.deleteComment) {
114 this.throwUnsupported(body.platform, body.name)
115 }
116 const data = DeleteEngagementCommentFunctionDataSchema.parse(body.data)
117 const input: EngagementDeleteInput = {
118 accountId,
119 commentId: data.commentId,
120 platform: body.platform,
121 credential: await this.getPlatformCredential(userId, accountId, body.platform),
122 }
123 return { platform: body.platform, ...await this.callPlatformProvider(accountId, () => provider.deleteComment!(input)) }
124 }
125 case ChannelEngagementFunctionName.Like: {
126 if (!provider.like) {
127 this.throwUnsupported(body.platform, body.name)
128 }
129 const data = EngagementWorkFunctionDataSchema.parse(body.data)
130 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
131 return {
132 platform: body.platform,
133 ...await this.callPlatformProvider(accountId, () => provider.like!(input)),
134 }
135 }
136 case ChannelEngagementFunctionName.Unlike: {
137 if (!provider.unlike) {
138 this.throwUnsupported(body.platform, body.name)
139 }
140 const data = EngagementWorkFunctionDataSchema.parse(body.data)
141 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
142 return {
143 platform: body.platform,
144 ...await this.callPlatformProvider(accountId, () => provider.unlike!(input)),
145 }
146 }
147 case ChannelEngagementFunctionName.Repost: {
148 if (!provider.repost) {
149 this.throwUnsupported(body.platform, body.name)
150 }
151 const data = EngagementWorkFunctionDataSchema.parse(body.data)
152 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
153 return {
154 platform: body.platform,
155 ...await this.callPlatformProvider(accountId, () => provider.repost!(input)),
156 }
157 }
158 case ChannelEngagementFunctionName.UndoRepost: {
159 if (!provider.undoRepost) {
160 this.throwUnsupported(body.platform, body.name)
161 }
162 const data = EngagementWorkFunctionDataSchema.parse(body.data)
163 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
164 return {
165 platform: body.platform,
166 ...await this.callPlatformProvider(accountId, () => provider.undoRepost!(input)),
167 }
168 }
169 case ChannelEngagementFunctionName.Quote: {
170 if (!provider.quote) {
171 this.throwUnsupported(body.platform, body.name)
172 }
173 const data = QuoteEngagementWorkFunctionDataSchema.parse(body.data)
174 const input: EngagementQuoteInput = {
175 ...await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId),
176 content: data.content,
177 }
178 return { platform: body.platform, ...await this.callPlatformProvider(accountId, () => provider.quote!(input)) }
179 }
180 case ChannelEngagementFunctionName.Bookmark: {
181 if (!provider.bookmark) {
182 this.throwUnsupported(body.platform, body.name)
183 }
184 const data = EngagementWorkFunctionDataSchema.parse(body.data)
185 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
186 return {
187 platform: body.platform,
188 ...await this.callPlatformProvider(accountId, () => provider.bookmark!(input)),
189 }
190 }
191 case ChannelEngagementFunctionName.RemoveBookmark: {
192 if (!provider.removeBookmark) {
193 this.throwUnsupported(body.platform, body.name)
194 }
195 const data = EngagementWorkFunctionDataSchema.parse(body.data)
196 const input = await this.toLikeInput(userId, body.platform, data.platformWorkId, accountId)
197 return {
198 platform: body.platform,
199 ...await this.callPlatformProvider(accountId, () => provider.removeBookmark!(input)),
200 }
201 }
202 case ChannelEngagementFunctionName.HideReply: {
203 if (!provider.hideReply) {
204 this.throwUnsupported(body.platform, body.name)
205 }
206 const data = EngagementCommentFunctionDataSchema.parse(body.data)
207 const input = await this.toCommentActionInput(userId, body.platform, data.commentId, accountId)
208 return {
209 platform: body.platform,
210 ...await this.callPlatformProvider(accountId, () => provider.hideReply!(input)),
211 }
212 }
213 case ChannelEngagementFunctionName.UnhideReply: {
214 if (!provider.unhideReply) {
215 this.throwUnsupported(body.platform, body.name)
216 }
217 const data = EngagementCommentFunctionDataSchema.parse(body.data)
218 const input = await this.toCommentActionInput(userId, body.platform, data.commentId, accountId)
219 return {
220 platform: body.platform,
221 ...await this.callPlatformProvider(accountId, () => provider.unhideReply!(input)),
222 }
223 }
224 case ChannelEngagementFunctionName.Follow: {
225 if (!provider.follow) {
226 this.throwUnsupported(body.platform, body.name)
227 }
228 const data = EngagementAccountFunctionDataSchema.parse(body.data)
229 const input = await this.toAccountActionInput(userId, body.platform, data.targetPlatformUid, accountId)
230 return {
231 platform: body.platform,
232 ...await this.callPlatformProvider(accountId, () => provider.follow!(input)),
233 }
234 }
235 case ChannelEngagementFunctionName.Unfollow: {
236 if (!provider.unfollow) {
237 this.throwUnsupported(body.platform, body.name)
238 }
239 const data = EngagementAccountFunctionDataSchema.parse(body.data)
240 const input = await this.toAccountActionInput(userId, body.platform, data.targetPlatformUid, accountId)
241 return {
242 platform: body.platform,
243 ...await this.callPlatformProvider(accountId, () => provider.unfollow!(input)),
244 }
245 }
246 }
247 this.throwUnsupported(body.platform, body.name)
248 }
249
250 private throwUnsupported(platform: AccountType, name: ChannelEngagementFunctionName): never {
251 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
252 platform,
253 capability: `engagement.${name}`,
254 })
255 }
256
257 private async toLikeInput(
258 userId: string,
259 platform: AccountType,
260 platformWorkId: string,
261 accountId: string,
262 ): Promise<EngagementLikeInput> {
263 return {
264 accountId,
265 platformWorkId,
266 platform,
267 credential: await this.getPlatformCredential(userId, accountId, platform),
268 }
269 }
270
271 private async toCommentActionInput(
272 userId: string,
273 platform: AccountType,
274 commentId: string,
275 accountId: string,
276 ): Promise<EngagementCommentActionInput> {
277 return {
278 accountId,
279 commentId,
280 platform,
281 credential: await this.getPlatformCredential(userId, accountId, platform),
282 }
283 }
284
285 private async toAccountActionInput(
286 userId: string,
287 platform: AccountType,
288 targetPlatformUid: string,
289 accountId: string,
290 ): Promise<EngagementAccountActionInput> {
291 return {
292 accountId,
293 targetPlatformUid,
294 platform,
295 credential: await this.getPlatformCredential(userId, accountId, platform),
296 }
297 }
298
299 private async getPlatformCredential(userId: string, accountId: string, platform: AccountType): Promise<CredentialContext> {
300 const account = await this.accountRepository.getByIdAndUserId(accountId, userId)
301 if (!account) {
302 throw new AppException(ResponseCode.AccountNotFound)
303 }
304 if (account.type !== platform) {
305 throw new AppException(ResponseCode.ChannelAuthPlatformMismatch)
306 }
307 if (account.relayAccountRef) {
308 throw new RelayAccountException(account.relayAccountRef, accountId)
309 }
310
311 const credential = await this.authService.getValidCredential(accountId, userId)
312 return {
313 accessToken: credential.accessToken,
314 refreshToken: credential.refreshToken,
315 platformUid: account.uid,
316 account: account.account,
317 }
318 }
319
320 private async callPlatformProvider<T>(accountId: string, action: () => Promise<T>): Promise<T> {
321 try {
322 return await action()
323 }
324 catch (error) {
325 await this.authService.markAccountOfflineForCredentialFailure(accountId, error, 'platform_auth_failed')
326 throw error
327 }
328 }
329 }
330
330 lines TYPESCRIPT