返回 AiToEarn
platforms.registry.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / channels / platforms / platforms.registry.ts
1 import { Injectable, Logger } from '@nestjs/common'
2 import { AccountType, AppException, ResponseCode, zodToJsonSchemaOptions } from '@yikart/common'
3 import { z } from 'zod'
4 import {
5 CallEngagementFunctionBodySchema,
6 CreateEngagementCommentBodySchema,
7 EngagementCommentsQuerySchema,
8 EngagementExecutionQuerySchema,
9 getEngagementFunctionDataSchema,
10 } from '../engagement/engagement.dto'
11 import {
12 AnalyticsProvider,
13 AuthProvider,
14 BrowseProvider,
15 CachedPlatformMetadata,
16 ChannelEngagementFunctionName,
17 ChannelEngagementTargetType,
18 ChannelPaginationMode,
19 ChannelWorkAnalyticsDataSource,
20 EngagementProvider,
21 PlatformIntegration,
22 PlatformStatus,
23 PlatformWebhookHandler,
24 PublishOptionSourceProvider,
25 PublishProvider,
26 WorkProvider,
27 } from './platforms.interface'
28
29 interface RegisteredPlatform<TOption = any, TDataOption = any> {
30 integration: PlatformIntegration<TOption, TDataOption>
31 metadata: CachedPlatformMetadata
32 }
33
34 @Injectable()
35 export class PlatformIntegrationRegistry {
36 private readonly logger = new Logger(PlatformIntegrationRegistry.name)
37 private readonly platforms = new Map<AccountType, RegisteredPlatform>()
38
39 register<TOption, TDataOption>(integration: PlatformIntegration<TOption, TDataOption>): void {
40 const status = integration.status ?? PlatformStatus.Available
41 if (status === PlatformStatus.Hidden) {
42 throw new Error('Hidden platforms are not registered')
43 }
44 if (status === PlatformStatus.Available && !this.hasCapabilityProvider(integration)) {
45 throw new Error(`Available platform must register at least one capability provider: ${integration.platform}`)
46 }
47 if (this.platforms.has(integration.platform)) {
48 throw new Error(`Platform already registered: ${integration.platform}`)
49 }
50 this.platforms.set(integration.platform, {
51 integration,
52 metadata: this.createMetadataCache(integration),
53 })
54 this.logger.log({ platform: integration.platform }, 'Registered platform')
55 }
56
57 get(platform: AccountType): PlatformIntegration {
58 const registeredPlatform = this.platforms.get(platform)
59 if (!registeredPlatform) {
60 this.logger.warn({ platform }, 'Platform not registered')
61 throw new AppException(ResponseCode.PlatformNotSupported, { platform })
62 }
63 return registeredPlatform.integration
64 }
65
66 getAuth(platform: AccountType): AuthProvider {
67 const integration = this.get(platform)
68 if (!integration.auth) {
69 this.logger.warn({ platform }, 'Platform does not support auth')
70 throw new AppException(ResponseCode.PlatformNotSupported, { platform, capability: 'auth' })
71 }
72 return integration.auth
73 }
74
75 getPublish(platform: AccountType): PublishProvider {
76 const integration = this.get(platform)
77 if (!integration.publish) {
78 this.logger.warn({ platform }, 'Platform does not support publish')
79 throw new AppException(ResponseCode.ChannelPublishPlatformNotSupported, { platform })
80 }
81 return integration.publish
82 }
83
84 getPublishOptions(platform: AccountType): PublishOptionSourceProvider | undefined {
85 const integration = this.get(platform)
86 return integration.publishOptions
87 }
88
89 getAnalytics(platform: AccountType): AnalyticsProvider | undefined {
90 const integration = this.get(platform)
91 return integration.analytics
92 }
93
94 getEngagement(platform: AccountType): EngagementProvider | undefined {
95 const integration = this.get(platform)
96 return integration.engagement
97 }
98
99 getBrowse(platform: AccountType): BrowseProvider | undefined {
100 const integration = this.get(platform)
101 return integration.browse
102 }
103
104 getWork(platform: AccountType): WorkProvider | undefined {
105 const integration = this.get(platform)
106 return integration.work
107 }
108
109 hasWorkAnalyticsDataSource(platform: AccountType, dataSource: ChannelWorkAnalyticsDataSource): boolean {
110 return this.listWorkAnalyticsDataSources(this.get(platform)).includes(dataSource)
111 }
112
113 getWebhook(platform: AccountType): PlatformWebhookHandler | undefined {
114 const integration = this.get(platform)
115 return integration.webhook
116 }
117
118 listMetadata(): CachedPlatformMetadata[] {
119 return Array.from(this.platforms.values()).map(platform => platform.metadata)
120 }
121
122 listIntegrations(): PlatformIntegration[] {
123 return Array.from(this.platforms.values()).map(platform => platform.integration)
124 }
125
126 has(platform: AccountType): boolean {
127 return this.platforms.has(platform)
128 }
129
130 private createMetadataCache<TOption, TDataOption>(integration: PlatformIntegration<TOption, TDataOption>): CachedPlatformMetadata {
131 const publishPolicy = integration.metadata.publishPolicy
132 const publishSupported = Boolean(integration.publish && publishPolicy)
133 const status = integration.status ?? PlatformStatus.Available
134
135 return {
136 ...integration.metadata,
137 status,
138 capabilities: status === PlatformStatus.Available
139 ? {
140 auth: {
141 supported: Boolean(integration.auth),
142 revoke: Boolean(integration.auth?.revoke),
143 selectableAccounts: Boolean(integration.auth?.listSelectableAccounts),
144 refreshAccountAccess: Boolean(integration.auth?.refreshAccountAccess),
145 },
146 publish: {
147 supported: publishSupported,
148 cancel: Boolean(publishSupported && integration.publish?.cancel),
149 update: Boolean(publishSupported && publishPolicy?.updateSupported && integration.publish?.update),
150 verify: Boolean(publishSupported && integration.publish?.verify),
151 finalize: Boolean(publishSupported && integration.publish?.finalize),
152 scheduleByPlatform: Boolean(publishSupported && publishPolicy?.scheduleByPlatform),
153 optionSources: Boolean(integration.publishOptions),
154 completionStrategy: publishSupported ? publishPolicy?.completionStrategy : undefined,
155 },
156 analytics: this.createAnalyticsCapabilities(integration),
157 engagement: this.createEngagementCapabilities(integration.engagement),
158 work: {
159 listWorks: Boolean(integration.work?.listWorks),
160 listWorksPagination: integration.work?.listWorksPagination ?? { mode: ChannelPaginationMode.None },
161 getLinkInfo: Boolean(integration.work?.getLinkInfo),
162 getDetail: Boolean(integration.work?.getDetail),
163 verifyOwnership: Boolean(integration.work?.verifyOwnership),
164 },
165 browse: {
166 search: Boolean(integration.browse?.search),
167 getDetail: Boolean(integration.browse?.getDetail),
168 },
169 webhook: {
170 supported: Boolean(integration.webhook),
171 },
172 }
173 : {
174 auth: { supported: false, revoke: false, selectableAccounts: false, refreshAccountAccess: false },
175 publish: { supported: false, cancel: false, update: false, verify: false, finalize: false, scheduleByPlatform: false, optionSources: false },
176 analytics: { account: false, work: false },
177 engagement: this.createEngagementCapabilities(undefined),
178 work: { listWorks: false, listWorksPagination: { mode: ChannelPaginationMode.None }, getLinkInfo: false, getDetail: false, verifyOwnership: false },
179 browse: { search: false, getDetail: false },
180 webhook: { supported: false },
181 },
182 optionSchema: z.toJSONSchema(integration.metadata.optionSchema, {
183 ...zodToJsonSchemaOptions,
184 io: 'input',
185 }) as Record<string, unknown>,
186 }
187 }
188
189 private createEngagementCapabilities(provider: EngagementProvider | undefined) {
190 const querySchema = this.toJsonSchema(EngagementExecutionQuerySchema)
191 const bodySchema = this.toJsonSchema(CallEngagementFunctionBodySchema)
192
193 return {
194 comments: {
195 list: {
196 supported: Boolean(provider?.listComments),
197 pagination: provider?.commentPagination ?? { mode: ChannelPaginationMode.None },
198 parameters: {
199 querySchema: this.toJsonSchema(EngagementCommentsQuerySchema),
200 },
201 },
202 create: {
203 supported: Boolean(provider?.createComment),
204 parameters: {
205 querySchema,
206 bodySchema: this.toJsonSchema(CreateEngagementCommentBodySchema),
207 },
208 },
209 },
210 functions: [
211 {
212 name: ChannelEngagementFunctionName.DeleteComment,
213 label: { 'en-US': 'Delete comment', 'zh-CN': '删除评论' },
214 target: ChannelEngagementTargetType.Comment,
215 supported: Boolean(provider?.deleteComment),
216 },
217 {
218 name: ChannelEngagementFunctionName.Like,
219 label: { 'en-US': 'Like work', 'zh-CN': '点赞作品' },
220 target: ChannelEngagementTargetType.Work,
221 supported: Boolean(provider?.like),
222 },
223 {
224 name: ChannelEngagementFunctionName.Unlike,
225 label: { 'en-US': 'Unlike work', 'zh-CN': '取消点赞' },
226 target: ChannelEngagementTargetType.Work,
227 supported: Boolean(provider?.unlike),
228 },
229 {
230 name: ChannelEngagementFunctionName.Repost,
231 label: { 'en-US': 'Repost work', 'zh-CN': '转发作品' },
232 target: ChannelEngagementTargetType.Work,
233 supported: Boolean(provider?.repost),
234 },
235 {
236 name: ChannelEngagementFunctionName.UndoRepost,
237 label: { 'en-US': 'Undo repost', 'zh-CN': '取消转发' },
238 target: ChannelEngagementTargetType.Work,
239 supported: Boolean(provider?.undoRepost),
240 },
241 {
242 name: ChannelEngagementFunctionName.Quote,
243 label: { 'en-US': 'Quote work', 'zh-CN': '引用作品' },
244 target: ChannelEngagementTargetType.Work,
245 supported: Boolean(provider?.quote),
246 },
247 {
248 name: ChannelEngagementFunctionName.Bookmark,
249 label: { 'en-US': 'Bookmark work', 'zh-CN': '收藏作品' },
250 target: ChannelEngagementTargetType.Work,
251 supported: Boolean(provider?.bookmark),
252 },
253 {
254 name: ChannelEngagementFunctionName.RemoveBookmark,
255 label: { 'en-US': 'Remove bookmark', 'zh-CN': '移除收藏' },
256 target: ChannelEngagementTargetType.Work,
257 supported: Boolean(provider?.removeBookmark),
258 },
259 {
260 name: ChannelEngagementFunctionName.HideReply,
261 label: { 'en-US': 'Hide reply', 'zh-CN': '隐藏回复' },
262 target: ChannelEngagementTargetType.Comment,
263 supported: Boolean(provider?.hideReply),
264 },
265 {
266 name: ChannelEngagementFunctionName.UnhideReply,
267 label: { 'en-US': 'Unhide reply', 'zh-CN': '取消隐藏回复' },
268 target: ChannelEngagementTargetType.Comment,
269 supported: Boolean(provider?.unhideReply),
270 },
271 {
272 name: ChannelEngagementFunctionName.Follow,
273 label: { 'en-US': 'Follow account', 'zh-CN': '关注账号' },
274 target: ChannelEngagementTargetType.Account,
275 supported: Boolean(provider?.follow),
276 },
277 {
278 name: ChannelEngagementFunctionName.Unfollow,
279 label: { 'en-US': 'Unfollow account', 'zh-CN': '取消关注账号' },
280 target: ChannelEngagementTargetType.Account,
281 supported: Boolean(provider?.unfollow),
282 },
283 ]
284 .filter(item => item.supported)
285 .map(item => ({
286 name: item.name,
287 label: item.label,
288 target: item.target,
289 parameters: {
290 querySchema,
291 bodySchema,
292 dataSchema: this.toJsonSchema(getEngagementFunctionDataSchema(item.name)),
293 },
294 })),
295 }
296 }
297
298 private createAnalyticsCapabilities<TOption, TDataOption>(integration: PlatformIntegration<TOption, TDataOption>) {
299 return {
300 account: Boolean(integration.analytics),
301 work: this.listWorkAnalyticsDataSources(integration).length > 0,
302 }
303 }
304
305 private listWorkAnalyticsDataSources<TOption, TDataOption>(integration: PlatformIntegration<TOption, TDataOption>) {
306 const sources = new Set(integration.metadata.analytics?.work?.dataSources ?? [])
307 if (integration.analytics?.fetchWorkAnalytics) {
308 sources.add(ChannelWorkAnalyticsDataSource.Official)
309 }
310 return [...sources]
311 }
312
313 private toJsonSchema(schema: z.ZodTypeAny): Record<string, unknown> {
314 return z.toJSONSchema(schema, {
315 ...zodToJsonSchemaOptions,
316 io: 'input',
317 }) as Record<string, unknown>
318 }
319
320 private hasCapabilityProvider<TOption, TDataOption>(integration: PlatformIntegration<TOption, TDataOption>): boolean {
321 return Boolean(
322 integration.auth
323 || integration.publish
324 || integration.publishOptions
325 || integration.analytics
326 || Boolean(integration.metadata.analytics?.work?.dataSources.length)
327 || integration.engagement
328 || integration.browse
329 || integration.work
330 || integration.webhook,
331 )
332 }
333 }
334
334 lines TYPESCRIPT