| 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 |