返回 AiToEarn
twitter.service.ts
根目录 / project / aitoearn-electron / server / src / modules / plat / twitter / twitter.service.ts
1 import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
2 import { ConfigService } from '@nestjs/config';
3 import { HttpService } from '@nestjs/axios';
4 import { InjectModel } from '@nestjs/mongoose';
5 import { Model } from 'mongoose';
6 import { lastValueFrom } from 'rxjs';
7 import { PubType, PubStatus, PubRecord } from 'src/db/schema/pubRecord.schema'
8
9 import { TwitterAuthService } from './twitter.auth.service';
10
11 // Twitter API V2基础URL
12 const TWITTER_API_V2_URL = 'https://api.twitter.com/2';
13
14 @Injectable()
15 export class TwitterService {
16 private readonly logger = new Logger(TwitterService.name);
17
18 constructor(
19 private readonly configService: ConfigService,
20 private readonly httpService: HttpService,
21 private readonly twitterAuthService: TwitterAuthService,
22 @InjectModel(PubRecord.name)
23 private readonly PubRecordModel: Model<PubRecord>,
24
25 ) {}
26
27
28 /**
29 * 获取用户的Twitter时间线
30 * @param userId 用户ID
31 * @param accountId Twitter账号ID
32 * @param maxResults 最大结果数
33 * @returns Twitter时间线数据
34 */
35 async getUserTimeline(accessToken: string, userId: string, accountId: string, maxResults: number = 10) {
36 try {
37 // 确保maxResults是一个有效的整数
38 const validMaxResults = Number(maxResults);
39
40 // 调用Twitter API获取时间线
41 const url = `${TWITTER_API_V2_URL}/users/${accountId}/tweets`;
42 // console.log("timeline url:", url);
43 const params = {
44 'max_results': isNaN(validMaxResults) ? 10 : validMaxResults, // 如果是NaN则使用默认值10
45 'tweet.fields': 'created_at,public_metrics,text,source',
46 'expansions': 'attachments.media_keys',
47 'media.fields': 'url,preview_image_url,type'
48 };
49 console.log("请求参数:", params);
50 // console.log("accessToken:", accessToken);
51 const response = await lastValueFrom(
52 this.httpService.get(url, {
53 params,
54 headers: {
55 Authorization: `Bearer ${accessToken}`,
56 },
57 })
58 );
59 console.log("==============response============");
60 console.log(response);
61 return response.data
62 } catch (error) {
63 console.error('完整错误对象:', JSON.stringify(error.response?.data || error.message));
64
65 this.logger.error(`获取Twitter时间线失败: ${error.message}`, error.stack);
66 throw new BadRequestException(`获取Twitter时间线失败: ${error.response?.data?.error || error.message}`);
67 }
68 }
69
70 /**
71 * 发布新推文
72 * @param userId 用户ID
73 * @param accountId Twitter账号ID
74 * @param text 推文内容
75 * @param mediaIds 媒体ID数组
76 * @returns 发布结果
77 */
78 async createTweet(accessToken: string, userId: string, accountId: string, text: string, mediaIds?: string[]) {
79 // 获取当前最大的 id
80 const maxRecord = await this.PubRecordModel.findOne().sort({ id: -1 });
81 const newId = maxRecord ? maxRecord.id + 1 : 1;
82
83 try {
84
85
86 const url = `${TWITTER_API_V2_URL}/tweets`;
87 const payload: any = { text };
88
89 // 如果有媒体附件,添加媒体信息
90 if (mediaIds && mediaIds.length > 0) {
91 payload.media = {
92 media_ids: mediaIds
93 };
94 }
95
96 // 创建发布记录
97 let newData: any = {
98 userId: userId,
99 type: PubType.ARTICLE,
100 title: text,
101 desc: text,
102 accountId: accountId,
103 status:PubStatus.UNPUBLISH,
104 // timingTime: publishAt,
105 publishTime: new Date()
106 }
107
108 // if (publishAt) {
109 // requestBody.status.publishAt = publishAt; // 如果提供了 publishAt 则使用 publishAt
110 // }
111
112 // console.log(requestBody);
113
114 await this.PubRecordModel.create({
115 ...newData,
116 id: newId,
117 });
118
119 const response = await lastValueFrom(
120 this.httpService.post(url, payload, {
121 headers: {
122 'Content-Type': 'application/json',
123 'Authorization': `Bearer ${accessToken}`
124 }
125 })
126 );
127
128 this.logger.log(`成功发布推文: userId=${userId}, accountId=${accountId}`);
129
130 // 更新发布记录
131 await this.PubRecordModel.updateOne({ id:newId }, {
132 status: PubStatus.RELEASED,
133 publishTime: new Date()
134 });
135
136 return response.data.data
137 } catch (error) {
138 await this.PubRecordModel.updateOne({ id:newId }, { status: PubStatus.FAIL });
139 this.logger.error(`发布推文失败: ${error.message}`, error.stack);
140 throw new BadRequestException(`发布推文失败: ${error.response?.data?.error || error.message}`);
141 }
142 }
143
144 /**
145 * 上传媒体文件
146 * @param userId 用户ID
147 * @param accountId Twitter账号ID
148 * @param mediaFile 媒体文件Buffer
149 * @param mimeType 媒体类型
150 * @returns 媒体上传结果
151 */
152 async uploadMedia(accessToken, userId: string, accountId: string, mediaFile: Buffer, mimeType: string) {
153 try {
154
155
156 // Twitter有单独的媒体上传API
157 const url = 'https://upload.twitter.com/1.1/media/upload.json';
158
159 // 创建表单数据
160 const formData = new FormData();
161 formData.append('media', new Blob([mediaFile], { type: mimeType }));
162
163 const response = await lastValueFrom(
164 this.httpService.post(url, formData, {
165 headers: {
166 'Content-Type': 'multipart/form-data',
167 'Authorization': `Bearer ${accessToken}`
168 }
169 })
170 );
171
172 this.logger.log(`媒体上传成功: userId=${userId}, accountId=${accountId}`);
173
174 return response.data
175 } catch (error) {
176 this.logger.error(`上传媒体失败: ${error.message}`, error.stack);
177 throw new BadRequestException(`上传媒体失败: ${error.response?.data?.error || error.message}`);
178 }
179 }
180
181 /**
182 * 删除推文
183 * @param userId 用户ID
184 * @param accountId Twitter账号ID
185 * @param tweetId 推文ID
186 * @returns 删除结果
187 */
188 async deleteTweet(accessToken, userId: string, accountId: string, tweetId: string) {
189 try {
190
191 const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;
192
193 await lastValueFrom(
194 this.httpService.delete(url, {
195 headers: {
196 'Authorization': `Bearer ${accessToken}`
197 }
198 })
199 );
200
201 this.logger.log(`成功删除推文: tweetId=${tweetId}, accountId=${accountId}`);
202
203 return true
204 } catch (error) {
205 this.logger.error(`删除推文失败: ${error.message}`, error.stack);
206 throw new BadRequestException(`删除推文失败: ${error.response?.data?.error || error.message}`);
207 }
208 }
209
210 /**
211 * 获取推文详情
212 * @param userId 用户ID
213 * @param accountId Twitter账号ID
214 * @param tweetId 推文ID
215 * @returns 推文详情
216 */
217 async getTweetDetail(accessToken, userId: string, accountId: string, tweetId: string) {
218 try {
219
220 const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;
221 const params = {
222 'tweet.fields': 'created_at,public_metrics,text,source',
223 'expansions': 'attachments.media_keys,author_id',
224 'media.fields': 'url,preview_image_url,type'
225 };
226
227 const response = await lastValueFrom(
228 this.httpService.get(url, {
229 params,
230 headers: {
231 'Authorization': `Bearer ${accessToken}`
232 }
233 })
234 );
235
236 return response.data.data
237 } catch (error) {
238 this.logger.error(`获取推文详情失败: ${error.message}`, error.stack);
239 throw new BadRequestException(`获取推文详情失败: ${error.response?.data?.error || error.message}`);
240 }
241 }
242
243 /**
244 * 获取推文的统计数据
245 * @param userId 用户ID
246 * @param accountId Twitter账号ID
247 * @param tweetId 推文ID
248 * @returns 推文统计数据
249 */
250 async getTweetMetrics(accessToken, userId: string, accountId: string, tweetId: string) {
251 try {
252
253 const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;
254 const params = {
255 'tweet.fields': 'public_metrics,non_public_metrics,organic_metrics', // 注意:某些指标需要高级API访问权限
256 };
257
258 const response = await lastValueFrom(
259 this.httpService.get(url, {
260 params,
261 headers: {
262 'Authorization': `Bearer ${accessToken}`
263 }
264 })
265 );
266
267 return response.data;
268 } catch (error) {
269 this.logger.error(`获取推文统计数据失败: ${error.message}`, error.stack);
270 throw new BadRequestException(`获取推文统计数据失败: ${error.response?.data?.error || error.message}`);
271 }
272 }
273
274 /**
275 * 查找用户的推文
276 * @param userId 用户ID
277 * @param accountId Twitter账号ID
278 * @param query 查询内容
279 * @param maxResults 最大结果数
280 * @returns 搜索结果
281 */
282 async searchTweets(accessToken, userId: string, accountId: string, query: string, maxResults: number = 10) {
283 try {
284 // 确保maxResults是一个有效的整数
285 const validMaxResults = Number(maxResults);
286
287 const url = `${TWITTER_API_V2_URL}/tweets/search/recent`;
288 const params = {
289 'query': `from:${accountId} ${query}`,
290 'max_results': isNaN(validMaxResults) ? 10 : validMaxResults, // 如果是NaN则使用默认值10
291 'tweet.fields': 'created_at,public_metrics,text',
292 'expansions': 'attachments.media_keys',
293 'media.fields': 'url,preview_image_url,type'
294 };
295
296 const response = await lastValueFrom(
297 this.httpService.get(url, {
298 params,
299 headers: {
300 'Authorization': `Bearer ${accessToken}`
301 }
302 })
303 );
304
305 return response.data;
306 } catch (error) {
307 this.logger.error(`搜索推文失败: ${error.message}`, error.stack);
308 throw new BadRequestException(`搜索推文失败: ${error.response?.data?.error || error.message}`);
309 }
310 }
311
312 /**
313 * 对推文点赞
314 * @param accessToken 访问令牌
315 * @param userId 用户ID
316 * @param accountId Twitter账号ID
317 * @param tweetId 推文ID
318 * @returns 点赞结果
319 */
320 async likeTweet(accessToken: string, userId: string, accountId: string, tweetId: string) {
321 try {
322 // Twitter API V2 点赞端点
323 const url = `${TWITTER_API_V2_URL}/users/${accountId}/likes`;
324 const data = {
325 tweet_id: tweetId
326 };
327
328 const response = await lastValueFrom(
329 this.httpService.post(url, data, {
330 headers: {
331 'Authorization': `Bearer ${accessToken}`,
332 'Content-Type': 'application/json'
333 }
334 })
335 );
336
337 this.logger.log(`成功点赞推文: userId=${userId}, accountId=${accountId}, tweetId=${tweetId}`);
338 return response.data;
339 } catch (error) {
340 this.logger.error(`点赞推文失败: ${error.message}`, error.stack);
341 throw new BadRequestException(`点赞推文失败: ${error.response?.data?.error || error.message}`);
342 }
343 }
344
345 /**
346 * 取消对推文的点赞
347 * @param accessToken 访问令牌
348 * @param userId 用户ID
349 * @param accountId Twitter账号ID
350 * @param tweetId 推文ID
351 * @returns 取消点赞结果
352 */
353 async unlikeTweet(accessToken: string, userId: string, accountId: string, tweetId: string) {
354 try {
355 // Twitter API V2 取消点赞端点
356 const url = `${TWITTER_API_V2_URL}/users/${accountId}/likes/${tweetId}`;
357
358 const response = await lastValueFrom(
359 this.httpService.delete(url, {
360 headers: {
361 'Authorization': `Bearer ${accessToken}`
362 }
363 })
364 );
365
366 this.logger.log(`成功取消点赞推文: userId=${userId}, accountId=${accountId}, tweetId=${tweetId}`);
367 return response.data;
368 } catch (error) {
369 this.logger.error(`取消点赞推文失败: ${error.message}`, error.stack);
370 throw new BadRequestException(`取消点赞推文失败: ${error.response?.data?.error || error.message}`);
371 }
372 }
373
374 }
375
375 lines TYPESCRIPT