返回 AiToEarn
kwai.service.ts
1 import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
2 import type { Readable } from 'node:stream'
3 import type { GenerateAuthUrlInput } from '../platforms.interface'
4 import type { KwaiPlatformResponseBody } from './kwai.exception'
5 import type {
6 KwaiApiResponse,
7 KwaiOAuthCredentialsResponse,
8 KwaiPhotoInfo,
9 KwaiPhotoListResponse,
10 KwaiPublishVideoResponse,
11 KwaiStartUploadResponse,
12 KwaiUserInfoResponse,
13 KwaiVideoUploadResponse,
14 } from './kwai.interface'
15 import { Injectable } from '@nestjs/common'
16 import axios from 'axios'
17 import { KwaiConfig } from './kwai.config'
18 import { KwaiOAuthGrantType } from './kwai.constants'
19 import { KwaiPlatformException } from './kwai.exception'
20
21 const KWAI_API_HOST = 'https://open.kuaishou.com'
22
23 @Injectable()
24 export class KwaiService {
25 private readonly http: AxiosInstance
26
27 constructor(private readonly cfg: KwaiConfig) {
28 this.http = this.createHttpClient()
29 }
30
31 private createHttpClient(): AxiosInstance {
32 const http = axios.create()
33 http.interceptors.response.use(
34 (response) => {
35 if (KwaiPlatformException.hasPlatformError(response)) {
36 throw KwaiPlatformException.fromPlatformResponse(response)
37 }
38 return response
39 },
40 (error: AxiosError<KwaiPlatformResponseBody>) => {
41 throw KwaiPlatformException.fromAxiosError(error)
42 },
43 )
44 return http
45 }
46
47 private async request<T>(
48 url: string,
49 config: AxiosRequestConfig = {},
50 ): Promise<KwaiApiResponse<T>> {
51 const response = await this.http.request<KwaiApiResponse<T>>({
52 ...config,
53 method: config.method ?? 'GET',
54 url,
55 })
56 return response.data
57 }
58
59 generateAuthUrl(
60 scopes: string[],
61 state: string,
62 deviceType?: GenerateAuthUrlInput['deviceType'],
63 ): string {
64 const params = new URLSearchParams({
65 app_id: this.cfg.clientId,
66 scope: scopes.join(','),
67 response_type: 'code',
68 state,
69 redirect_uri: this.cfg.redirectUri,
70 ...(deviceType === 'desktop' ? { ua: 'pc' } : {}),
71 })
72
73 return `${KWAI_API_HOST}/oauth2/authorize?${params.toString()}`
74 }
75
76 async exchangeCode(code: string): Promise<{
77 accessToken: string
78 refreshToken: string
79 expiresAt: Date
80 scope: string
81 openId: string
82 }> {
83 const params = {
84 app_id: this.cfg.clientId,
85 app_secret: this.cfg.clientSecret,
86 code,
87 grant_type: KwaiOAuthGrantType.AuthorizationCode,
88 }
89
90 const url = `${KWAI_API_HOST}/oauth2/access_token`
91 const data = await this.request<KwaiOAuthCredentialsResponse>(
92 url,
93 { method: 'GET', params },
94 )
95
96 return {
97 accessToken: data.access_token,
98 refreshToken: data.refresh_token,
99 expiresAt: new Date(Date.now() + data.expires_in * 1000),
100 scope: data.scopes.join(','),
101 openId: data.open_id,
102 }
103 }
104
105 async refreshToken(refreshToken: string): Promise<{
106 accessToken: string
107 refreshToken: string
108 expiresAt: Date
109 scope: string
110 }> {
111 const params = {
112 app_id: this.cfg.clientId,
113 app_secret: this.cfg.clientSecret,
114 refresh_token: refreshToken,
115 grant_type: KwaiOAuthGrantType.RefreshToken,
116 }
117
118 const url = `${KWAI_API_HOST}/oauth2/refresh_token`
119 const data = await this.request<KwaiOAuthCredentialsResponse>(
120 url,
121 { params },
122 )
123
124 return {
125 accessToken: data.access_token,
126 refreshToken: data.refresh_token,
127 expiresAt: new Date(Date.now() + data.expires_in * 1000),
128 scope: data.scopes.join(','),
129 }
130 }
131
132 async getUserInfo(accessToken: string): Promise<{
133 displayName: string
134 avatarUrl?: string
135 sex?: string
136 fanCount?: number
137 followCount?: number
138 city?: string
139 }> {
140 const params = {
141 app_id: this.cfg.clientId,
142 access_token: accessToken,
143 }
144
145 const url = `${KWAI_API_HOST}/openapi/user_info`
146 const data = await this.request<KwaiUserInfoResponse>(
147 url,
148 { params },
149 )
150
151 const userInfo = data.user_info
152 return {
153 displayName: userInfo.name,
154 avatarUrl: userInfo.bigHead || userInfo.head,
155 sex: userInfo.sex,
156 fanCount: userInfo.fan,
157 followCount: userInfo.follow,
158 city: userInfo.city,
159 }
160 }
161
162 async listPhotos(
163 accessToken: string,
164 cursor?: string,
165 count?: number,
166 ): Promise<KwaiPhotoInfo[]> {
167 const page = await this.listPhotoPage(accessToken, cursor, count)
168 return page.items
169 }
170
171 async listPhotoPage(
172 accessToken: string,
173 cursor?: string,
174 count?: number,
175 ): Promise<{
176 items: KwaiPhotoInfo[]
177 rawResponse: KwaiPhotoListResponse
178 }> {
179 const params = {
180 app_id: this.cfg.clientId,
181 access_token: accessToken,
182 cursor,
183 count,
184 }
185
186 const url = `${KWAI_API_HOST}/openapi/photo/list`
187 const data = await this.request<KwaiPhotoListResponse>(
188 url,
189 { params },
190 )
191
192 return {
193 items: data.video_list ?? [],
194 rawResponse: data,
195 }
196 }
197
198 async startUpload(accessToken: string): Promise<{
199 uploadToken: string
200 endpoint: string
201 }> {
202 const params = {
203 app_id: this.cfg.clientId,
204 access_token: accessToken,
205 }
206
207 const url = `${KWAI_API_HOST}/openapi/photo/start_upload`
208 const data = await this.request<KwaiStartUploadResponse>(
209 url,
210 { method: 'POST', params },
211 )
212
213 return {
214 uploadToken: data.upload_token,
215 endpoint: data.endpoint,
216 }
217 }
218
219 async fragmentUploadVideo(
220 uploadToken: string,
221 fragmentId: number,
222 endpoint: string,
223 video: Buffer | Readable,
224 contentLength?: number,
225 ): Promise<void> {
226 const params = {
227 fragment_id: fragmentId,
228 upload_token: uploadToken,
229 }
230 const headers: Record<string, string> = { 'Content-Type': 'video/mp4' }
231 if (contentLength !== undefined) {
232 headers['Content-Length'] = String(contentLength)
233 }
234
235 const url = `http://${endpoint}/api/upload/fragment`
236 await this.request<KwaiVideoUploadResponse>(
237 url,
238 {
239 method: 'POST',
240 params,
241 headers,
242 data: video,
243 },
244 )
245 }
246
247 async completeFragmentUpload(
248 uploadToken: string,
249 fragmentCount: number,
250 endpoint: string,
251 ): Promise<void> {
252 const params = {
253 fragment_count: fragmentCount,
254 upload_token: uploadToken,
255 }
256
257 const url = `http://${endpoint}/api/upload/complete`
258 await this.request<KwaiVideoUploadResponse>(
259 url,
260 { method: 'POST', params },
261 )
262 }
263
264 async publishVideo(
265 accessToken: string,
266 caption: string,
267 cover: Blob,
268 uploadToken: string,
269 option?: {
270 stereo_type?: string
271 merchant_product_id?: string
272 },
273 ): Promise<{ photoId: string, playUrl?: string }> {
274 const formData = new FormData()
275 formData.append('caption', caption)
276 formData.append('cover', cover)
277
278 const params = {
279 upload_token: uploadToken,
280 app_id: this.cfg.clientId,
281 access_token: accessToken,
282 stereo_type: option?.stereo_type,
283 merchant_product_id: option?.merchant_product_id,
284 }
285
286 const url = `${KWAI_API_HOST}/openapi/photo/publish`
287 const data = await this.request<KwaiPublishVideoResponse>(
288 url,
289 {
290 method: 'POST',
291 params,
292 data: formData,
293 },
294 )
295
296 return {
297 photoId: data.video_info.photo_id,
298 playUrl: data.video_info.play_url,
299 }
300 }
301
302 async getVideoInfo(
303 accessToken: string,
304 photoId: string,
305 ): Promise<{
306 photoId: string
307 caption: string
308 cover: string
309 playUrl: string
310 createTime: number
311 likeCount: number
312 commentCount: number
313 viewCount: number
314 pending: boolean
315 }> {
316 const params = {
317 app_id: this.cfg.clientId,
318 access_token: accessToken,
319 photo_id: photoId,
320 }
321
322 const url = `${KWAI_API_HOST}/openapi/photo/info`
323 const data = await this.request<{ video_info: KwaiPublishVideoResponse['video_info'] }>(
324 url,
325 { params },
326 )
327
328 const video = data.video_info
329 return {
330 photoId: video.photo_id,
331 caption: video.caption,
332 cover: video.cover,
333 playUrl: video.play_url ?? '',
334 createTime: video.create_time,
335 likeCount: video.like_count,
336 commentCount: video.comment_count,
337 viewCount: video.view_count,
338 pending: video.pending,
339 }
340 }
341 }
342
342 lines TYPESCRIPT