返回 AiToEarn
platforms.service.ts
1 import type { AccountType } from '@yikart/common'
2 import type { Request, Response } from 'express'
3 import type {
4 PublishOptionCreateResult,
5 PublishOptionFilters,
6 PublishOptionJsonSchemaView,
7 PublishOptionSource,
8 PublishOptionValuesResult,
9 } from './platforms.interface'
10 import { Injectable, Logger } from '@nestjs/common'
11 import { AppException, ResponseCode, zodToJsonSchemaOptions } from '@yikart/common'
12 import { AccountRepository } from '@yikart/mongodb'
13 import { z } from 'zod'
14 import { AuthService } from '../auth/auth.service'
15 import { RelayAccountException } from '../relay/relay-account.exception'
16 import { ChannelPlatformException, PlatformErrorCategory, PlatformErrorCauseType } from './platforms.exception'
17 import { PlatformIntegrationRegistry } from './platforms.registry'
18
19 @Injectable()
20 export class PlatformsService {
21 private readonly logger = new Logger(PlatformsService.name)
22
23 constructor(
24 private readonly registry: PlatformIntegrationRegistry,
25 private readonly authService: AuthService,
26 private readonly accountRepository: AccountRepository,
27 ) {}
28
29 listSources(platform: AccountType) {
30 const provider = this.registry.getPublishOptions(platform)
31 return (provider?.listSources() ?? []).map(source => this.toSourceView(source))
32 }
33
34 async getAccountValues(
35 userId: string,
36 accountId: string,
37 field: string,
38 filters?: object,
39 ): Promise<PublishOptionValuesResult> {
40 const { account, provider, source } = await this.getAccountOptionContext(userId, accountId, field)
41
42 const parsedFilters = source.filterSchema
43 ? source.filterSchema.parse(filters ?? {}) as PublishOptionFilters
44 : filters as PublishOptionFilters | undefined
45 const credential = await this.authService.getValidCredential(accountId, userId)
46 return this.callAccountProvider(accountId, () => provider.getValues({
47 userId,
48 accountId,
49 field,
50 filters: parsedFilters,
51 credential: {
52 accessToken: credential.accessToken,
53 refreshToken: credential.refreshToken,
54 platformUid: account.uid,
55 account: account.account,
56 },
57 }))
58 }
59
60 async createAccountValue(
61 userId: string,
62 accountId: string,
63 field: string,
64 data?: object,
65 ): Promise<PublishOptionCreateResult> {
66 const { account, provider, source } = await this.getAccountOptionContext(userId, accountId, field)
67 if (!provider.createValue || !source.createSchema) {
68 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
69 platform: account.type,
70 field,
71 })
72 }
73
74 const parsedData = source.createSchema.parse(data ?? {}) as Record<string, unknown>
75 const credential = await this.authService.getValidCredential(accountId, userId)
76 return this.callAccountProvider(accountId, () => provider.createValue!({
77 userId,
78 accountId,
79 field,
80 data: parsedData,
81 credential: {
82 accessToken: credential.accessToken,
83 refreshToken: credential.refreshToken,
84 platformUid: account.uid,
85 account: account.account,
86 },
87 }))
88 }
89
90 async dispatchWebhook(platform: AccountType, request: Request, response: Response): Promise<void> {
91 const handler = this.registry.getWebhook(platform)
92 if (!handler) {
93 this.logger.warn({
94 platform,
95 method: request.method,
96 url: request.originalUrl,
97 }, 'Webhook is not supported by platform')
98 throw new AppException(ResponseCode.ChannelWebhookNotSupported, { platform })
99 }
100
101 this.logger.log({
102 platform,
103 method: request.method,
104 url: request.originalUrl,
105 }, 'Dispatching platform webhook')
106 await handler.handle(request, response, { platform })
107 if (!response.headersSent) {
108 const exception = new ChannelPlatformException({
109 code: ResponseCode.ChannelWebhookPublishFailed,
110 platform,
111 category: PlatformErrorCategory.WebhookInvalid,
112 context: {
113 method: request.method,
114 endpoint: request.originalUrl,
115 },
116 cause: {
117 type: PlatformErrorCauseType.Unknown,
118 platformMessage: 'Platform webhook handler completed without response',
119 },
120 })
121 try {
122 response.status(204).send()
123 this.logger.fatal(exception, 'Platform webhook fallback response sent')
124 }
125 catch (err) {
126 this.logger.fatal(err, 'Failed to send platform webhook fallback response')
127 throw err
128 }
129 }
130 }
131
132 private toSourceView(source: PublishOptionSource) {
133 return {
134 field: source.field,
135 label: source.label,
136 description: source.description,
137 valueType: source.valueType,
138 requiresAccount: source.requiresAccount,
139 filterSchema: source.filterSchema
140 ? z.toJSONSchema(source.filterSchema, { ...zodToJsonSchemaOptions, io: 'input' }) as PublishOptionJsonSchemaView
141 : undefined,
142 createSchema: source.createSchema
143 ? z.toJSONSchema(source.createSchema, { ...zodToJsonSchemaOptions, io: 'input' }) as PublishOptionJsonSchemaView
144 : undefined,
145 }
146 }
147
148 private async getAccountOptionContext(userId: string, accountId: string, field: string) {
149 const account = await this.accountRepository.getByIdAndUserId(accountId, userId)
150 if (!account) {
151 throw new AppException(ResponseCode.AccountNotFound)
152 }
153 if (account.relayAccountRef) {
154 throw new RelayAccountException(account.relayAccountRef, accountId)
155 }
156
157 const provider = this.registry.getPublishOptions(account.type)
158 if (!provider) {
159 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
160 platform: account.type,
161 field,
162 })
163 }
164
165 const source = provider.listSources().find(item => item.field === field)
166 if (!source) {
167 throw new AppException(ResponseCode.ChannelPlatformOperationNotSupported, {
168 platform: account.type,
169 field,
170 })
171 }
172
173 return { account, provider, source }
174 }
175
176 private async callAccountProvider<T>(accountId: string, action: () => Promise<T>): Promise<T> {
177 try {
178 return await action()
179 }
180 catch (error) {
181 await this.authService.markAccountOfflineForCredentialFailure(accountId, error, 'platform_auth_failed')
182 throw error
183 }
184 }
185 }
186
186 lines TYPESCRIPT