返回 AiToEarn
twitter.auth.service.ts
根目录 / project / aitoearn-electron / server / src / modules / plat / twitter / twitter.auth.service.ts
1 import { Injectable, Inject, forwardRef, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
2 import { ConfigService } from '@nestjs/config';
3 import { HttpService } from '@nestjs/axios';
4 import { Model } from 'mongoose';
5 import { InjectModel } from '@nestjs/mongoose';
6 import { firstValueFrom } from 'rxjs';
7 import * as crypto from 'crypto';
8
9 import { RedisService } from 'src/lib/redis/redis.service';
10 import { AuthService } from 'src/auth/auth.service';
11 import { AccountService } from 'src/modules/account/account.service';
12 import { Account, AccountStatus, AccountType } from 'src/db/schema/account.schema';
13 import { AccountToken, TokenPlatform, TokenStatus } from 'src/db/schema/accountToken.schema';
14 import { User } from 'src/db/schema/user.schema';
15 import { IdService } from 'src/db/id.service';
16 import { getCurrentTimestamp } from 'src/util/time.util';
17 import axios from 'axios'
18
19 import { TwitterOAuthTokenResponse, TwitterUser } from './dto/twitter.dto';
20
21 // Twitter API Constants
22 const TWITTER_API_V2_BASE_URL = 'https://api.twitter.com/2';
23 const TOKEN_URL = 'https://api.twitter.com/2/oauth2/token';
24 const AUTHORIZE_URL = 'https://twitter.com/i/oauth2/authorize';
25
26 @Injectable()
27 export class TwitterAuthService {
28 private webClientSecret: string;
29 private webClientId: string;
30 private webRenderBaseUrl: string;
31
32 constructor(
33 private readonly configService: ConfigService,
34 private readonly httpService: HttpService,
35 private readonly redisService: RedisService,
36 private readonly idService: IdService,
37 @Inject(forwardRef(() => AuthService))
38 private readonly authService: AuthService,
39 private readonly accountService: AccountService,
40 @InjectModel(User.name) private readonly userModel: Model<User>,
41 @InjectModel(Account.name) private readonly accountModel: Model<Account>,
42 @InjectModel(AccountToken.name) private readonly accountTokenModel: Model<AccountToken>,
43 ) {
44 this.initTwitterSecrets();
45 }
46 /**
47 * 初始化Twitter API密钥
48 */
49 private initTwitterSecrets() {
50 // 从配置服务获取Twitter API密钥
51 this.webClientId = this.configService.get<string>('TWITTER_CONFIG.WEB_CLIENT_ID');
52 this.webClientSecret = this.configService.get<string>('TWITTER_CONFIG.WEB_CLIENT_SECRET');
53 this.webRenderBaseUrl = this.configService.get<string>('TWITTER_CONFIG.WEB_RENDER_URL');
54
55 if (!this.webClientId || !this.webClientSecret || !this.webRenderBaseUrl) {
56 console.warn('Twitter API配置缺失,请检查环境变量或配置文件');
57 }
58 }
59
60 /**
61 * 生成PKCE码校验器和挑战码
62 */
63 private generatePKCE(): { codeVerifier: string; codeChallenge: string } {
64 // 生成随机码验证器
65 const codeVerifier = crypto.randomBytes(32).toString('base64url');
66
67 // 生成码挑战
68 const codeChallenge = crypto
69 .createHash('sha256')
70 .update(codeVerifier)
71 .digest('base64url');
72
73 return { codeVerifier, codeChallenge };
74 }
75
76 /**
77 * 获取Twitter授权URL
78 * @param userId 用户ID
79 * @param mail 用户邮箱
80 * @returns 包含授权URL的对象
81 */
82 async getAuthorizationUrl(userId: string, mail?: string): Promise<object> {
83 const state = crypto.randomBytes(16).toString('hex');
84 const { codeVerifier, codeChallenge } = this.generatePKCE();
85
86 // 存储状态数据和PKCE验证码
87 const stateData = {
88 originalState: state,
89 userId: userId,
90 email: mail,
91 codeVerifier: codeVerifier
92 };
93
94 // 将状态数据保存到Redis,5分钟有效期
95 await this.redisService.setKey(`twitter:state:${state}`, JSON.stringify(stateData), 600);
96
97 // 定义请求的权限范围
98 const scopes = [
99 'tweet.write',
100 'tweet.read',
101 'tweet.moderate.write',
102 'users.read',
103 'space.read',
104 'like.read',
105 'like.write',
106 'list.read',
107 'list.write',
108 'media.write',
109 'offline.access']; // offline.access用于获取刷新令牌
110
111 // 构建授权URL参数
112 const params = new URLSearchParams({
113 response_type: 'code',
114 client_id: this.webClientId,
115 redirect_uri: `${this.webRenderBaseUrl}/api/plat/twitter/auth/callback`,
116 scope: scopes.join(' '),
117 state: state,
118 code_challenge: codeChallenge,
119 code_challenge_method: 'S256', // 使用SHA-256算法
120 });
121
122 // 构建完整的授权URL
123 const authUrl = `${AUTHORIZE_URL}?${params.toString()}`;
124 console.log('Twitter授权URL:', authUrl);
125
126 return { url: authUrl };
127 }
128
129 /**
130 * 处理Twitter授权回调
131 * @param code 授权码
132 * @param state 状态码
133 * @returns 处理结果
134 */
135 async handleAuthorizationCallback(code: string, state: string): Promise<object> {
136 // 从Redis获取存储的状态数据
137 const stateDataJson = await this.redisService.get(`twitter:state:${state}`);
138 if (!stateDataJson) {
139 throw new BadRequestException('无效或过期的状态码');
140 }
141 // 解析状态数据
142 const stateData = JSON.parse(stateDataJson);
143 const { userId, codeVerifier } = stateData;
144
145 // 验证状态数据
146 if (!userId || !codeVerifier) {
147 throw new BadRequestException('状态数据不完整');
148 }
149
150 try {
151 // 删除Redis中的状态数据
152 await this.redisService.del(`twitter:state:${state}`);
153
154 // 交换授权码获取访问令牌
155 const tokenResponse = await this.exchangeCodeForTokens(code, codeVerifier);
156 const { access_token, refresh_token, expires_in, scope } = tokenResponse;
157
158 // 获取Twitter用户资料
159 const twitterUser = await this.getTwitterUserProfile(access_token);
160
161 // 获取或存储刷新令牌
162 console.log('获取到Twitter访问令牌:', access_token.substring(0, 10) + '...');
163 console.log('有效期:', expires_in, '秒');
164
165 // 更新或创建账户信息
166 await this.updateTwitterAccountInfo(
167 userId,
168 twitterUser.id,
169 access_token,
170 refresh_token,
171 expires_in
172 );
173
174 // 缓存访问令牌
175 await this.redisService.setKey(
176 `twitter:accessToken:${twitterUser.id}`,
177 {
178 access_token,
179 refresh_token,
180 expires_in
181 },
182 expires_in
183 );
184
185 // 查询更新后的账号信息
186 const existingAccount = await this.accountModel.findOne({
187 type: AccountType.TWITTER,
188 uid: twitterUser.id
189 });
190
191 // 生成并返回结果
192 const results = {
193 data: {
194 accountInfo: existingAccount,
195 userInfo: {
196 userId: userId,
197 uid: twitterUser.id
198 }
199 },
200 msg: "success",
201 code: 0
202 };
203
204 console.log("最终返回", results);
205 return results;
206
207 } catch (error) {
208 console.error('处理Twitter授权回调失败:', error);
209 throw new HttpException(
210 error.response?.data?.error_description || '授权失败',
211 error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR
212 );
213 }
214 }
215 /**
216 * 交换授权码获取令牌
217 * @param code 授权码
218 * @param codeVerifier PKCE验证码
219 * @returns Twitter OAuth令牌响应
220 */
221 private async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<TwitterOAuthTokenResponse> {
222 const params = new URLSearchParams({
223 grant_type: 'authorization_code',
224 code: code,
225 client_id: this.webClientId,
226 redirect_uri: `${this.webRenderBaseUrl}/api/plat/twitter/auth/callback`,
227 code_verifier: codeVerifier,
228 });
229
230 const base64Credentials = Buffer.from(`${this.webClientId}:${this.webClientSecret}`).toString('base64');
231
232 try {
233 const { data } = await firstValueFrom(
234 this.httpService.post<TwitterOAuthTokenResponse>(TOKEN_URL, params.toString(), {
235 headers: {
236 'Content-Type': 'application/x-www-form-urlencoded',
237 'Authorization': `Basic ${base64Credentials}`,
238 },
239 }),
240 );
241 return data;
242 } catch (error) {
243 console.error('交换Twitter授权码失败:', error.response?.data || error.message);
244 throw new HttpException(
245 error.response?.data?.error_description || '获取Twitter令牌失败',
246 error.response?.status || HttpStatus.BAD_REQUEST,
247 );
248 }
249 }
250
251 /**
252 * 获取Twitter用户资料
253 * @param accessToken 访问令牌
254 * @returns Twitter用户资料
255 */
256 private async getTwitterUserProfile(accessToken: string): Promise<TwitterUser> {
257 try {
258 const { data } = await firstValueFrom(
259 this.httpService.get<{ data: TwitterUser }>(`${TWITTER_API_V2_BASE_URL}/users/me`, {
260 headers: {
261 Authorization: `Bearer ${accessToken}`,
262 },
263 params: {
264 'user.fields': 'id,name,username,profile_image_url,description,public_metrics,created_at,verified',
265 },
266 }),
267 );
268 return data.data;
269 } catch (error) {
270 console.error('获取Twitter用户资料失败:', error.response?.data || error.message);
271 throw new HttpException(
272 error.response?.data?.detail || '获取Twitter用户资料失败',
273 error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
274 );
275 }
276 }
277
278 /**
279 * 更新Twitter账户信息
280 * 根据Twitter用户ID创建或更新账户
281 * @param userId 用户ID
282 * @param twitterId Twitter用户ID
283 * @param accessToken 访问令牌
284 * @param refreshToken 刷新令牌
285 * @param expires_in 令牌有效期(秒)
286 */
287 private async updateTwitterAccountInfo(
288 userId: string,
289 twitterId: string,
290 accessToken: string,
291 refreshToken: string,
292 expires_in: number
293 ): Promise<void> {
294 try {
295 // 获取Twitter用户资料
296 const twitterUser = await this.getTwitterUserProfile(accessToken);
297
298 // 准备账号信息
299 const channelInfo = {
300 userId: userId,
301 type: AccountType.TWITTER,
302 uid: twitterId,
303 account: twitterUser.username,
304 nickname: twitterUser.name,
305 avatar: twitterUser.profile_image_url,
306 homePage: `https://twitter.com/${twitterUser.username}`,
307 fansCount: twitterUser.public_metrics?.followers_count || 0,
308 followCount: twitterUser.public_metrics?.following_count || 0,
309 workCount: twitterUser.public_metrics?.tweet_count || 0,
310 likeCount: 0,
311 readCount: 0,
312 collectCount: 0,
313 forwardCount: 0,
314 commentCount: 0,
315 updateTime: new Date(),
316 status: AccountStatus.USABLE,
317 loginCookie: "1111", // Twitter不使用cookie认证
318 token: "111", // 存储访问令牌
319 };
320
321 console.log(channelInfo);
322
323 // 使用AccountService创建或更新账户
324 const account = await this.accountService.addOrUpdateAccount(channelInfo);
325 console.log("成功创建或更新Twitter账号:", account);
326
327 // 检查是否存在账号Token
328 let accountToken = await this.accountTokenModel.findOne({
329 accountId: twitterId,
330 platform: TokenPlatform.TWITTER
331 });
332
333 if (accountToken) {
334 if (refreshToken && refreshToken.trim() !== '') {
335 accountToken.refreshToken = refreshToken;
336 }
337 // 更新现有Token
338 // await this.accountTokenModel.findOneAndUpdate(
339 // { accountId: twitterId, platform: TokenPlatform.TWITTER },
340 // {
341 // refreshToken: refreshToken,
342 // updateTime: new Date(),
343 // expiresAt: new Date((getCurrentTimestamp() + expires_in) * 1000)
344 // },
345 // );
346 accountToken.expiresAt = new Date((getCurrentTimestamp() + expires_in) * 1000);
347 accountToken.updateTime = new Date();
348 await accountToken.save();
349 console.log("成功更新Twitter账号Token");
350 } else {
351 // 创建新Token
352 await this.accountTokenModel.create({
353 userId,
354 accountId: twitterId,
355 platform: TokenPlatform.TWITTER,
356 refreshToken: refreshToken,
357 expiresAt: new Date((getCurrentTimestamp() + expires_in) * 1000),
358 status: TokenStatus.USABLE,
359 createTime: new Date(),
360 updateTime: new Date(),
361 });
362 console.log("成功创建Twitter账号Token");
363 }
364 } catch (error) {
365 console.error('更新Twitter账号信息失败:', error);
366 // 不抛出异常,避免影响授权流程
367 }
368 }
369
370 /**
371 * 刷新用户的Twitter访问令牌
372 * @param userId 用户ID
373 * @param accountId 账号ID
374 * @param refreshToken 刷新令牌
375 * @returns 新的访问令牌信息
376 */
377 async refreshAccessToken(userId: string, accountId: string, refreshToken: string): Promise<object> {
378 console.log(userId, accountId, refreshToken);
379 try {
380 const params = new URLSearchParams({
381 grant_type: 'refresh_token',
382 refresh_token: refreshToken,
383 client_id: this.webClientId,
384 // redirect_uri: `${this.webRenderBaseUrl}/api/plat/twitter/auth/callback`,
385 });
386 // 请求体的参数
387 // const params = new URLSearchParams({
388 // client_id: this.webClientId, // 使用你的 client_id
389 // client_secret: this.webClientSecret, // 使用你的 client_secret
390 // refresh_token: refreshToken, // 提供刷新令牌
391 // grant_type: 'refresh_token', // 认证类型是刷新令牌
392 // });
393
394 const base64Credentials = Buffer.from(`${this.webClientId}:${this.webClientSecret}`).toString('base64');
395
396 const { data } = await firstValueFrom(
397 this.httpService.post(TOKEN_URL, params.toString(), {
398 headers: {
399 'Content-Type': 'application/x-www-form-urlencoded',
400 'Authorization': `Basic ${base64Credentials}`,
401 },
402 // auth: {
403 // username: this.webClientId,
404 // password: this.webClientSecret
405 // }
406 })
407 );
408 // const response = await axios.post(TOKEN_URL, params.toString(), {
409 // headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
410 // });
411 // console.log("================response================")
412 // console.log(response);
413 // const accessTokenInfo = response.data;
414 // // console.log("================accessTokenInfo================")
415 // // console.log(accessTokenInfo);
416 // // 剩余有效秒数
417 // const expires = accessTokenInfo.expires_in
418 // const { data } = await firstValueFrom(
419 // this.httpService.post(TOKEN_URL, params.toString(), {
420 // headers: {
421 // 'Content-Type': 'application/x-www-form-urlencoded'
422 // }
423 // })
424 // );
425 // await this.redisService.setKey(
426 // `twitter:accessToken:${accountId}`,
427 // accessTokenInfo,
428 // expires
429 // );
430 console.log('Twitter API响应:', JSON.stringify(data));
431 const { access_token, refresh_token, expires_in } = data;
432
433 // 更新Redis中的令牌
434 const TokenInfo = { access_token, refresh_token, expires_in };
435 await this.redisService.setKey(
436 `twitter:accessToken:${accountId}`,
437 TokenInfo,
438 expires_in
439 );
440
441 console.log('刷新Twitter访问令牌成功');
442 const userInfo = await this.userModel.findOne({_id: userId});
443 const systemTokenInfo = {
444 phone: userInfo?.phone ?? '', // 如果 userInfo.phone 为 undefined 或 null,则使用空字符串
445 id: userId,
446 name: userInfo.name,
447 isManager: false,
448 googleId: userInfo?.googleAccount?.googleId ?? ''
449 }
450 // 生成系统令牌
451 const systemToken = await this.authService.generateToken(systemTokenInfo);
452
453 return { url: systemToken };
454 } catch (err) {
455 console.log(err);
456 console.error('刷新Twitter访问令牌失败:', err.response?.data || err.message);
457 throw new HttpException(
458 err.response?.data?.error_description || '刷新令牌失败',
459 err.response?.status || HttpStatus.BAD_REQUEST
460 );
461 }
462 }
463
464 /**
465 * 获取用户的Twitter访问令牌
466 * @param accountId 账号ID
467 * @returns 访问令牌
468 */
469 async getUserAccessToken(accountId: string): Promise<string> {
470 console.log("获取访问令牌,accountId:", accountId);
471
472 // 先检查Redis缓存
473 const cachedToken = await this.redisService.get(`twitter:accessToken:${accountId}`);
474 if (cachedToken && cachedToken.access_token) {
475 console.log("从Redis获取到有效令牌");
476 return cachedToken.access_token;
477 }
478
479 // 如果缓存中没有,尝试刷新
480 const accountTokenInfo = await this.accountTokenModel.findOne({accountId: accountId});
481 if (!accountTokenInfo || !accountTokenInfo.refreshToken) {
482 throw new BadRequestException('无效的账号或刷新令牌丢失');
483 }
484
485 // 刷新并获取新令牌
486 const refreshResult = await this.refreshAccessToken(
487 accountTokenInfo.userId,
488 accountTokenInfo.accountId,
489 accountTokenInfo.refreshToken
490 );
491
492 // 刷新后再次从Redis获取
493 const newToken = await this.redisService.get(`twitter:accessToken:${accountId}`);
494 if (!newToken || !newToken.access_token) {
495 throw new BadRequestException('刷新令牌后未能获取访问令牌');
496 }
497
498 return newToken.access_token;
499 }
500
501 /**
502 * 检查用户是否已授权Twitter
503 * @param accountId 账号ID
504 * @returns 是否已授权
505 */
506 async isAuthorized(accountId: string): Promise<boolean> {
507 try {
508 const accessToken = await this.getUserAccessToken(accountId);
509 return !!accessToken;
510 } catch (error) {
511 return false;
512 }
513 }
514
515 /**
516 * 撤销Twitter授权
517 * @param accountId 账号ID
518 * @returns 撤销结果
519 */
520 async revokeAuthorization(accountId: string): Promise<boolean> {
521 try {
522 const accessToken = await this.getUserAccessToken(accountId);
523 if (!accessToken) {
524 return true; // 已经没有授权了
525 }
526
527 // 撤销Twitter令牌
528 const params = new URLSearchParams({
529 token: accessToken,
530 client_id: this.webClientId,
531 token_type_hint: 'access_token'
532 });
533
534 const base64Credentials = Buffer.from(`${this.webClientId}:${this.webClientSecret}`).toString('base64');
535
536 await firstValueFrom(
537 this.httpService.post(`${TWITTER_API_V2_BASE_URL}/oauth2/revoke`, params.toString(), {
538 headers: {
539 'Content-Type': 'application/x-www-form-urlencoded',
540 'Authorization': `Basic ${base64Credentials}`,
541 },
542 }),
543 );
544
545 // 删除缓存的令牌
546 await this.redisService.del(`twitter:accessToken:${accountId}`);
547
548 // 更新用户信息,移除授权信息
549 await this.accountTokenModel.updateOne(
550 { accountId: accountId },
551 { $unset: { 'refreshToken': 1, 'expiresAt': 1 } }
552 );
553
554 return true;
555 } catch (error) {
556 console.error('撤销Twitter授权失败:', error);
557 return false;
558 }
559 }
560
561 /**
562 * 获取初始化后的Twitter API客户端
563 * @param accountId 账号ID
564 * @returns 初始化后的客户端
565 */
566 async getTwitterClient(accountId: string): Promise<any> {
567 const accessToken = await this.getUserAccessToken(accountId);
568 if (!accessToken) {
569 throw new Error('No access token available, user needs to authorize');
570 }
571
572 // 返回一个简单的API客户端,可以根据需要扩展
573 return {
574 headers: {
575 Authorization: `Bearer ${accessToken}`
576 },
577 baseUrl: TWITTER_API_V2_BASE_URL,
578 async get(endpoint: string, params = {}) {
579 // 这里可以实现实际的API调用逻辑
580 // 或者使用第三方Twitter客户端库
581 }
582 };
583 }
584 }
585
585 lines TYPESCRIPT