Update
This commit is contained in:
+2
-1
@@ -71,7 +71,8 @@
|
||||
"rss-parser": "^3.13.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"telegraf": "^4.16.3",
|
||||
"twitter-api-v2": "^1.29.0"
|
||||
"twitter-api-v2": "^1.29.0",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
||||
Generated
+9
@@ -164,6 +164,9 @@ importers:
|
||||
twitter-api-v2:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
uuid:
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3.2.0
|
||||
@@ -4493,6 +4496,10 @@ packages:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@14.0.0:
|
||||
resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==}
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
|
||||
@@ -9569,6 +9576,8 @@ snapshots:
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@14.0.0: {}
|
||||
|
||||
v8-compile-cache-lib@3.0.1: {}
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
|
||||
@@ -205,9 +205,13 @@ export class AppController {
|
||||
}
|
||||
|
||||
@Get('/tw/read')
|
||||
async getTwitterMe(@Query('url') url: string, @Query('via') via: string = 'api') {
|
||||
async getTwitterMe(
|
||||
@Query('url') url: string,
|
||||
@Query('via') via: string = 'api',
|
||||
@Query('nocache') nocache: number = 0,
|
||||
) {
|
||||
if (via === 'browser') {
|
||||
return this.xReaderService.readXPostViaBrowserV2(url)
|
||||
return this.xReaderService.readXPostViaBrowserV2(url, 0, !nocache);
|
||||
}
|
||||
return this.xReaderService.readXPostViaApi(url)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const TOKENS_PER_CHAR: Record<Language, number> = {
|
||||
* Target character length theo platform + buffer để AI có không gian "thở".
|
||||
*/
|
||||
const PLATFORM_TARGET_CHARS: Record<Platform, { min: number; max: number; buffer: number }> = {
|
||||
[Platform.X]: { min: 180, max: 280, buffer: 1.3 },
|
||||
[Platform.X]: { min: 50, max: 220, buffer: 1.3 },
|
||||
[Platform.FACEBOOK]: { min: 400, max: 1200, buffer: 1.5 },
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export function calculateLengthBudget(
|
||||
|
||||
// Safe minimums để đảm bảo không bị cắt
|
||||
const SAFE_MIN: Record<Platform, number> = {
|
||||
[Platform.X]: 300, // X post không bao giờ < 300 tokens
|
||||
[Platform.X]: 200, // X post không bao giờ < 300 tokens
|
||||
[Platform.FACEBOOK]: 800, // FB không bao giờ < 800 tokens
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -9,8 +9,10 @@ import {Context, Telegraf} from "telegraf";
|
||||
import {Logger} from "@nestjs/common";
|
||||
import {QuoteWriterService} from "./services/quote-writer.service";
|
||||
import {GenerateQuoteDto} from "./dto/generate-quote.dto";
|
||||
import {TextUtil} from "../../common/utils/text.util";
|
||||
import {isEmpty} from "lodash";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {_getTweetIdFromUrl, rand} from "../../shared/helper";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
|
||||
|
||||
@Processor('comment_writer_queue')
|
||||
@@ -22,8 +24,8 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
private readonly commentWriterService: CommentWriterService,
|
||||
private readonly xreader: XReaderService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
//@InjectQueue('comment_writer_completed_queue') private readonly aiCommentWriteCompletedQueue: Queue,
|
||||
|
||||
private readonly sqsPostService: SqsPostService,
|
||||
private readonly xCache: XCacheService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -47,11 +49,71 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
const topic = summary || title;
|
||||
let pgPostCreateDto!: PostCreateInput;
|
||||
|
||||
console.log(`CommentWriterProcessor_processing_${job.name}`);
|
||||
this.logger.log(`CommentWriterProcessor_processing_${job.name}`);
|
||||
this.logger.debug(job.data)
|
||||
switch (job.name) {
|
||||
case 'generate_comment_twitter': {
|
||||
case 'GENERATE_AUTO_COMMENT_TWITTER': {
|
||||
let {tweetUrl, tweetText, angle, language, tone, x_username} = job.data;
|
||||
if (isEmpty(tweetUrl) && isEmpty(tweetText)) {
|
||||
this.logger.error(`Job thiếu payload`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Job thiếu payload`)
|
||||
return {};
|
||||
}
|
||||
|
||||
const xpost = await this.xreader.readXPost(url);
|
||||
//Kiểm tra đã comment chưa ?
|
||||
const tweetId = _getTweetIdFromUrl(tweetUrl);
|
||||
const hadCmt = await this.xCache.hasComment(tweetId, x_username);
|
||||
if (hadCmt) {
|
||||
this.logger.error(`Đã comment rồi`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Đã comment rồi`)
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
if (isEmpty(tweetText)) {
|
||||
const xpost = await this.xreader.readXPost(tweetUrl);
|
||||
tweetText = xpost.text || '';
|
||||
if (!language) {
|
||||
language = xpost.lang || 'en';
|
||||
}
|
||||
}
|
||||
if (isEmpty(tweetText)) {
|
||||
this.logger.error(`Tweet text = Empty`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `Tweet text = Empty`)
|
||||
return {};
|
||||
}
|
||||
const dto = {
|
||||
originalPost: tweetText,
|
||||
angle,
|
||||
language: language,
|
||||
tone,
|
||||
};
|
||||
this.logger.debug(`==> Bắt đầu viết auto comment: ${tweetUrl}`, dto);
|
||||
|
||||
const res = await this.commentWriterService.generateComment(dto, false);
|
||||
this.logger.debug(`==> Đã viết auto comment xong:`, dto, res);
|
||||
|
||||
await this.sqsPostService.sendMessage(x_username, {
|
||||
// ...res,
|
||||
telegramChatId,
|
||||
content: res.comment,
|
||||
tweetUrl,
|
||||
job_type: 'X_POSTER_REPLY',
|
||||
type: 'X_POSTER_REPLY'
|
||||
},
|
||||
rand(42, 84)
|
||||
);
|
||||
await this.xCache.setAlreadyComment(tweetId, x_username);
|
||||
|
||||
this.logger.log(`Đã gửi comment auto sang queue thành công.`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `
|
||||
Đã gửi comment auto sang queue thành công. M:${res.model}- T:${dto?.tone} A:${dto?.angle} `)
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'generate_comment_twitter': {
|
||||
const xpost: any = await this.xreader.readXPost(url);
|
||||
|
||||
const dto: GenerateCommentDto = {
|
||||
originalPost: xpost.text,
|
||||
@@ -81,7 +143,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
// }, {attempts: 1, backoff: 5000, removeOnComplete: true,});
|
||||
|
||||
await this.bot.telegram.sendMessage(telegramChatId, `
|
||||
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}`);
|
||||
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}\ntransVi: ${aiWriterResult.commentTransVi}`);
|
||||
// const _url = url.indexOf('?s=20') > -1 ? url : `${url}?s=20`;
|
||||
// console.log({_url});
|
||||
|
||||
@@ -144,7 +206,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
|
||||
const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0;
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `
|
||||
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model} }
|
||||
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model}\ndich:${aiWriterResult.commentTransVi}
|
||||
`);
|
||||
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId,
|
||||
@@ -186,7 +248,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
}
|
||||
case 'generate_quote_twitter': {
|
||||
this.logger.debug('===>generate_quote_twitter:', url);
|
||||
const xpost = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
|
||||
const xpost: any = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
|
||||
|
||||
const originalAuthor = `${xpost.author} ${xpost.handle}`;
|
||||
const dto: GenerateQuoteDto = {
|
||||
@@ -284,9 +346,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
|
||||
this.logger.log({aiWriterResult});
|
||||
const sendId = telegramChatId || process.env.TELEGRAM_ADMIN_ID || 0;
|
||||
await this.bot.telegram.sendMessage(sendId, `
|
||||
Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType}
|
||||
`);
|
||||
|
||||
|
||||
//
|
||||
let finalQuote = aiWriterResult.quote;
|
||||
@@ -294,13 +354,18 @@ export class CommentWriterProcessor extends WorkerHost {
|
||||
finalQuote += `\n\nQuote:"${dto.originalPost}\n\n${dto.originalAuthor}`
|
||||
}
|
||||
//xoá url trong bài vì tốn 0,2$ cho bài có url
|
||||
const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
|
||||
// const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
|
||||
|
||||
const X_USERS = process.env.TWITTER_USERNAMES!.split(',');
|
||||
|
||||
await this.bot.telegram.sendMessage(sendId, `
|
||||
Đã viết quote xong ...
|
||||
model: ${aiWriterResult.model}
|
||||
type: ${aiWriterResult.quoteType}
|
||||
dịch: ${aiWriterResult.quoteTransVi}
|
||||
`);
|
||||
await this.bot.telegram.sendMessage(
|
||||
sendId,
|
||||
finalQuoteCleanUrl,
|
||||
finalQuote,
|
||||
{
|
||||
// parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
|
||||
@@ -5,12 +5,16 @@ import {CommentWriterService} from './services/comment-writer.service';
|
||||
import {GenerateContentDto} from './dto/generate-content.dto';
|
||||
import {GenerateCommentDto} from './dto/generate-comment.dto';
|
||||
import {IAIGrokProvider} from "./interfaces/ai-provider.interface";
|
||||
import {AiTranslatorDto} from "./dto/ai-translator.dto";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {GenerateCommentAdapterExtDto} from "./dto/generate-comment-adapter-ext.dto";
|
||||
|
||||
@Controller('content-writer')
|
||||
export class ContentWriterController {
|
||||
constructor(
|
||||
private readonly writerService: ContentWriterService,
|
||||
private readonly commentService: CommentWriterService,
|
||||
private readonly translatorService: TranslatorService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,12 +33,22 @@ export class ContentWriterController {
|
||||
}
|
||||
|
||||
@Post('comment')
|
||||
generateComment(@Body() dto: GenerateCommentDto) {
|
||||
return this.commentService.generateComment(dto);
|
||||
generateComment(@Body() dto: GenerateCommentAdapterExtDto) {
|
||||
const dto0: GenerateCommentDto = {
|
||||
...dto,
|
||||
language: dto.lang,
|
||||
originalPost: dto.tweet_text
|
||||
}
|
||||
return this.commentService.generateComment(dto0);
|
||||
}
|
||||
|
||||
@Post('comment/variants')
|
||||
generateCommentVariants(@Body() dto: GenerateCommentDto) {
|
||||
// return this.commentService.generateVariants(dto, 3);
|
||||
}
|
||||
|
||||
@Post('ai-translate')
|
||||
aiTranslator(@Body() dto: AiTranslatorDto) {
|
||||
return this.translatorService.translator(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {Global, Module} from '@nestjs/common';
|
||||
import {Module} from '@nestjs/common';
|
||||
import {AIService} from "../../shared/ai.service";
|
||||
import {ContentWriterProcessor} from "./content.writer.processor";
|
||||
import {PgPostService} from "../../shared/pg.post.service";
|
||||
import {BullModule} from "@nestjs/bullmq";
|
||||
import {PublishPageService} from "../social-api/publish.page.service";
|
||||
import {SocialModule} from "../social-api/social.module";
|
||||
import {ContentWriterController} from "./content-writer.controller";
|
||||
import {AIProviderFactory} from "./providers/ai-provider.factory";
|
||||
@@ -22,6 +21,8 @@ import {LengthStrategyService} from "./services/length-strategy.service";
|
||||
import {QuoteWriterService} from "./services/quote-writer.service";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {ContentSafetyService} from "./services/content-safety.service";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {GoogleProvider} from "./providers/google.provider";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -43,6 +44,7 @@ import {ContentSafetyService} from "./services/content-safety.service";
|
||||
OpenAIProvider,
|
||||
DeepSeekProvider,
|
||||
GrokProvider,
|
||||
GoogleProvider,
|
||||
AIProviderFactory,
|
||||
ProviderRouterService,
|
||||
CommentWriterProcessor,
|
||||
@@ -51,8 +53,9 @@ import {ContentSafetyService} from "./services/content-safety.service";
|
||||
QuoteWriterService,
|
||||
SqsPostService,
|
||||
ContentSafetyService,
|
||||
TranslatorService,
|
||||
],
|
||||
exports: [GrokProvider, ContentWriterService],
|
||||
exports: [GrokProvider, ContentWriterService, TranslatorService],
|
||||
|
||||
})
|
||||
export class ContentWriterModule {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {AccountTier} from "./enum/account-tier.enum";
|
||||
import {ConfigService} from "@nestjs/config";
|
||||
import {LengthStrategyService} from "./services/length-strategy.service";
|
||||
import {calculateTokenBudget} from "../../common/utils/token-calculator";
|
||||
import {AiTranslatorDto} from "./dto/ai-translator.dto";
|
||||
import {buildReviewerPrompt, LANGUAGE_NAMES} from "./prompts/templates";
|
||||
|
||||
@Injectable()
|
||||
export class ContentWriterService {
|
||||
@@ -33,6 +35,10 @@ export class ContentWriterService {
|
||||
async getGrokAI() {
|
||||
return this.factory.get('grok');
|
||||
}
|
||||
async getGptAI() {
|
||||
return this.factory.get('openai');
|
||||
}
|
||||
|
||||
|
||||
async generate(
|
||||
dto: GenerateContentDto,
|
||||
|
||||
@@ -14,14 +14,20 @@ import {ContentStyle} from "./enum/style.enum";
|
||||
import {ContentTone} from "./enum/tone.enum";
|
||||
import {XStrategy} from "../social-api/x-router.service";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {TranslatorService} from "./services/translator.service";
|
||||
import {CommentWriterService} from "./services/comment-writer.service";
|
||||
import {XReaderService} from "../x-reader/x-reader.service";
|
||||
import {Logger} from "@nestjs/common";
|
||||
|
||||
@Processor('content_writer_queue')
|
||||
export class ContentWriterProcessor extends WorkerHost {
|
||||
private readonly logger = new Logger("ContentWriterProcessor");
|
||||
constructor(
|
||||
private aiService: AIService,
|
||||
private readonly writerService: ContentWriterService,
|
||||
private readonly styleDetectorService: StyleDetectorService,
|
||||
private readonly pgPostService: PgPostService,
|
||||
private readonly translatorService: TranslatorService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
// private readonly managerService: ManagerService,
|
||||
@InjectQueue('content_writer_completed_queue') private readonly fbContentCompletedQueue: Queue,
|
||||
@@ -44,7 +50,8 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
enableReview,
|
||||
language,
|
||||
tone,
|
||||
postLength
|
||||
postLength,
|
||||
style,
|
||||
}
|
||||
const aiWriterResult = await this.writerService.generate(dto, false, 'openai', 'deepseek');
|
||||
// console.log({aiWriterResult});
|
||||
@@ -79,7 +86,6 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'generate_post_telegram': {
|
||||
isAutoPublish = true;
|
||||
const topicLen = topic.length;
|
||||
@@ -161,11 +167,24 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
// let finalContent = aiWriterResult.content;
|
||||
const post = await this.pgPostService.createPost(pgPostCreateDto);
|
||||
if (!isAutoPublish) {
|
||||
let contentTransVi = '';
|
||||
let contentTransModel = '';
|
||||
if (language !== 'vi') {
|
||||
const resTranslate = await this.translatorService.translator({
|
||||
text: pgPostCreateDto.content,
|
||||
target_lang: "vi",
|
||||
target_model: 'google'
|
||||
})
|
||||
contentTransVi = resTranslate.content;
|
||||
contentTransModel = resTranslate.model;
|
||||
}
|
||||
await this.fbContentCompletedQueue.add('generate_post_completed', {
|
||||
id: post.id,
|
||||
name: 'generate_post_completed',
|
||||
needConfirm: 1,
|
||||
content: pgPostCreateDto.content,
|
||||
contentTransVi,
|
||||
contentTransModel,
|
||||
autoPublish: false,
|
||||
telegramChatId,
|
||||
xSubmitProvider: post.id % 2 === 0 ? XStrategy.BROWSER_ONLY : XStrategy.API_ONLY, //cứ 3post api, có 1 post browser
|
||||
@@ -180,7 +199,7 @@ export class ContentWriterProcessor extends WorkerHost {
|
||||
content: pgPostCreateDto.content,
|
||||
autoPublish,
|
||||
telegramChatId,
|
||||
publishTo: ['x', 'fb'],
|
||||
publishTo: [ 'fb'],
|
||||
xSubmitProvider: post.id % 3 === 0 ? XStrategy.API_FIRST : XStrategy.BROWSER_ONLY, //cứ 3post api, có 1 post browser
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// dto/generate-content.dto.ts
|
||||
import {IsOptional, IsString, MaxLength} from 'class-validator';
|
||||
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
|
||||
import * as aiProviderFactory from "../providers/ai-provider.factory";
|
||||
|
||||
export class AiTranslatorDto {
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
text: string; // chủ đề / input thô từ user
|
||||
|
||||
@IsString()
|
||||
target_lang: languagePromptInterface.Language; // 'vi' | 'en' ... default 'en'
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
target_model?: aiProviderFactory.ProviderName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// dto/generate-comment.dto.ts
|
||||
import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator';
|
||||
import { ContentTone } from '../enum/tone.enum';
|
||||
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
|
||||
import {AngleEnum} from "../enum/angle.enum";
|
||||
import {Transform} from "class-transformer";
|
||||
|
||||
export class GenerateCommentAdapterExtDto {
|
||||
@IsString()
|
||||
@MaxLength(5000)
|
||||
tweet_text: string; // nội dung bài X gốc
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => value?.trim().toLowerCase())
|
||||
angle?: AngleEnum; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny"
|
||||
|
||||
@IsString()
|
||||
lang: languagePromptInterface.Language;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value?.trim().toLowerCase())
|
||||
tone?: ContentTone;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
persona?: string; // "crypto trader", "news analyst"...
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {AngleEnum} from "../enum/angle.enum";
|
||||
|
||||
export class GenerateCommentDto {
|
||||
@IsString()
|
||||
@MaxLength(3000)
|
||||
@MaxLength(5000)
|
||||
originalPost: string; // nội dung bài X gốc
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ContentTone} from "./tone.enum";
|
||||
|
||||
export enum AngleEnum {
|
||||
NATURAL = 'natural',
|
||||
AGREE = 'agree',
|
||||
CHALLENGE = 'challenge',
|
||||
ADD_INFO = 'add_info',
|
||||
@@ -33,6 +34,7 @@ export function isEMPATHYToneAngle(angle: AngleEnum): boolean {
|
||||
}
|
||||
|
||||
export const ANGLE_HINTS: Record<AngleEnum, string> = {
|
||||
[AngleEnum.NATURAL]: 'natural reaction.',
|
||||
[AngleEnum.AGREE]: 'I agree and would like to add a small point to support my argument.',
|
||||
[AngleEnum.CHALLENGE]: 'Disagree or add further nuance',
|
||||
[AngleEnum.ADD_INFO]: 'additional useful related information',
|
||||
@@ -61,6 +63,7 @@ export const get_ANGLE_HINTS_TELEGRAM_BUTTON=(tone:ContentTone)=>{
|
||||
return buttons;
|
||||
}
|
||||
export const DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON: Partial<Record<AngleEnum, Object>> = {
|
||||
[AngleEnum.NATURAL]: {text: 'phản ứng tự nhiên'},
|
||||
[AngleEnum.AGREE]: {text: 'Đồng ý'},
|
||||
[AngleEnum.CHALLENGE]: {text: 'Không đồng ý'},
|
||||
[AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'},
|
||||
|
||||
@@ -70,7 +70,8 @@ export function buildCommentPrompt(params: {
|
||||
: '';
|
||||
const budget = calculateLengthBudget(Platform.X, params.language);
|
||||
|
||||
const angleInstruction = getAngleHint(params.angle!, params.language, '- ANGLE:');
|
||||
const angle = params.angle ? params.angle : AngleEnum.NATURAL;
|
||||
const angleInstruction = getAngleHint(angle, params.language, '- ANGLE:');
|
||||
|
||||
console.debug({toneInstruction, angleInstruction});
|
||||
|
||||
@@ -92,5 +93,8 @@ export function buildCommentPrompt(params: {
|
||||
`- Output ONLY the reply text.`,
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
params.angle = angle;
|
||||
params.tone = tone;
|
||||
|
||||
return {system: system, user};
|
||||
}
|
||||
|
||||
@@ -294,11 +294,11 @@ export function getEmpathyAngleInstruction(
|
||||
}
|
||||
|
||||
export function getAngleHint(angle: AngleEnum, language: Language, prefix = '- '): string {
|
||||
if (!angle) return '';
|
||||
if (!angle) return prefix + ANGLE_HINTS[AngleEnum.NATURAL];
|
||||
if (isEMPATHYToneAngle(angle)) {
|
||||
return getEmpathyAngleInstruction(angle, language)
|
||||
}
|
||||
const agHint = ANGLE_HINTS[language];
|
||||
const agHint = ANGLE_HINTS[angle];
|
||||
return agHint
|
||||
? `${prefix} ${agHint}`
|
||||
: ``;
|
||||
|
||||
@@ -4,8 +4,9 @@ import {OpenAIProvider} from './openai.provider';
|
||||
import {DeepSeekProvider} from './deepseek.provider';
|
||||
import {IAIProvider} from '../interfaces/ai-provider.interface';
|
||||
import {GrokProvider} from "./grok.provider";
|
||||
import {GoogleProvider} from "./google.provider";
|
||||
|
||||
export type ProviderName = 'openai' | 'deepseek' | 'grok';
|
||||
export type ProviderName = 'openai' | 'deepseek' | 'grok' | 'google';
|
||||
|
||||
@Injectable()
|
||||
export class AIProviderFactory {
|
||||
@@ -13,6 +14,7 @@ export class AIProviderFactory {
|
||||
private readonly openai: OpenAIProvider,
|
||||
private readonly deepseek: DeepSeekProvider,
|
||||
private readonly grok: GrokProvider,
|
||||
private readonly google: GoogleProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -24,6 +26,8 @@ export class AIProviderFactory {
|
||||
return this.deepseek;
|
||||
case 'grok':
|
||||
return this.grok;
|
||||
case 'google':
|
||||
return this.google;
|
||||
default:
|
||||
throw new Error(`Unknown AI provider: ${name}`);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class DeepSeekProvider implements IAIProvider {
|
||||
baseURL: 'https://api.deepseek.com/v1',
|
||||
});
|
||||
this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-chat');
|
||||
// this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-v4-pro');
|
||||
}
|
||||
|
||||
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// providers/grok.provider.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import OpenAI from 'openai';
|
||||
import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface';
|
||||
import {ChatCompletionTool} from "openai/resources/chat/completions/completions";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class GoogleProvider implements IAIProvider {
|
||||
readonly name = 'google';
|
||||
private readonly logger = new Logger(GoogleProvider.name);
|
||||
private client: OpenAI;
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
// this.client = new OpenAI({
|
||||
// apiKey: this.config.get('XAI_API_KEY'),
|
||||
// baseURL: 'https://api.x.ai/v1',
|
||||
// });
|
||||
this.client = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
|
||||
});
|
||||
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
|
||||
// this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash'); // rẻ
|
||||
this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash-lite'); // rẻ
|
||||
// rẻ
|
||||
|
||||
}
|
||||
|
||||
async getModelName() {
|
||||
return this.defaultModel;
|
||||
}
|
||||
|
||||
async complete(
|
||||
messages: AIMessage[],
|
||||
options: AICompletionOptions = {}
|
||||
|
||||
): Promise<AICompletionResult> {
|
||||
const model = options.model ?? this.defaultModel;
|
||||
const res = await this.client.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
temperature: options.temperature ?? 0.8,
|
||||
max_tokens: options.maxTokens ?? 400,
|
||||
});
|
||||
|
||||
return {
|
||||
content: res.choices[0]?.message?.content?.trim() ?? '',
|
||||
tokensUsed: res.usage?.total_tokens ?? 0,
|
||||
model,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,12 @@ export class GrokProvider implements IAIProvider {
|
||||
private defaultModel: string;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
// this.client = new OpenAI({
|
||||
// apiKey: this.config.get('XAI_API_KEY'),
|
||||
// baseURL: 'https://api.x.ai/v1',
|
||||
// });
|
||||
this.client = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
|
||||
});
|
||||
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
|
||||
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
|
||||
this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
|
||||
this.defaultModel = this.config.get('OPENAI_MODEL', 'x-ai/grok-4.3');
|
||||
|
||||
}
|
||||
|
||||
async getModelName() {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class OpenAIProvider implements IAIProvider {
|
||||
}
|
||||
|
||||
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
|
||||
console.log(`OpenAIProvider_complete`);
|
||||
console.log(`OpenAIProvider_complete_begin...`);
|
||||
|
||||
const model = options.model ?? this.defaultModel;
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,8 @@ import {AIProviderFactory} from '../providers/ai-provider.factory';
|
||||
import {GenerateCommentDto} from '../dto/generate-comment.dto';
|
||||
import {buildCommentPrompt} from '../prompts/comment.templates';
|
||||
import {ProviderRouterService} from "./provider-router.service";
|
||||
import {TranslatorService} from "./translator.service";
|
||||
import {getUuid4} from "../../../shared/helper";
|
||||
|
||||
@Injectable()
|
||||
export class CommentWriterService {
|
||||
@@ -12,10 +14,27 @@ export class CommentWriterService {
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
private translatorService: TranslatorService
|
||||
) {
|
||||
}
|
||||
|
||||
async generateComment(dto: GenerateCommentDto) {
|
||||
// async generateComment() {}
|
||||
|
||||
async generateComment(
|
||||
dto: GenerateCommentDto,
|
||||
allowTranslateToVi = true
|
||||
): Promise<{
|
||||
commentTransVi?: string;
|
||||
commentTransModel?: string;
|
||||
comment: string;
|
||||
tokensUsed: number;
|
||||
tokenTranslatorUsed?: number;
|
||||
model: string;
|
||||
language: "en" | "vi" | "ja" | "ko" | "cn",
|
||||
input?: GenerateCommentDto,
|
||||
uid:string,
|
||||
}> {
|
||||
// const uid = getUuid4();
|
||||
const decision = this.router.route({
|
||||
language: dto.language,
|
||||
contentType: 'comment',
|
||||
@@ -29,13 +48,7 @@ export class CommentWriterService {
|
||||
const provider = this.factory.get(decision.writer);
|
||||
this.logger.log(`==> Comment routing: ${decision.reason} ==>`);
|
||||
// console.log({dto})
|
||||
const {system, user} = buildCommentPrompt({
|
||||
originalPost: dto.originalPost,
|
||||
angle: dto.angle,
|
||||
language: dto.language,
|
||||
persona: dto.persona,
|
||||
tone: dto.tone,
|
||||
});
|
||||
const {system, user} = buildCommentPrompt(dto);
|
||||
|
||||
this.logger.debug({dto, system, user})
|
||||
|
||||
@@ -54,12 +67,35 @@ export class CommentWriterService {
|
||||
|
||||
// Clean output: bỏ quotes nếu AI lỡ wrap
|
||||
const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim();
|
||||
this.logger.debug({
|
||||
post: dto.originalPost,
|
||||
output: cleaned
|
||||
});
|
||||
//transla if language != vi
|
||||
let commentTransVi = '';
|
||||
let commentTransModel = '';
|
||||
let tokenTranslatorUsed = 0;
|
||||
if (allowTranslateToVi && dto.language !== 'vi') {
|
||||
const resT = await this.translatorService.translator({
|
||||
text: cleaned,
|
||||
target_lang: 'vi',
|
||||
target_model: 'google'
|
||||
})
|
||||
commentTransVi = resT.content;
|
||||
commentTransModel = resT.model;
|
||||
tokenTranslatorUsed = resT.tokensUsed;
|
||||
}
|
||||
|
||||
return {
|
||||
commentTransVi,
|
||||
commentTransModel,
|
||||
comment: cleaned,
|
||||
tokensUsed: res.tokensUsed,
|
||||
tokenTranslatorUsed,
|
||||
model: res.model,
|
||||
language: dto.language,
|
||||
input: dto,
|
||||
uid: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import {ProviderName} from '../providers/ai-provider.factory';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||
import {AngleEnum} from "../enum/angle.enum";
|
||||
import {getRandomElement} from "../../../shared/helper";
|
||||
|
||||
interface ProviderPair {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
}
|
||||
export type ContentType = 'post' | 'comment';
|
||||
export type ContentType = 'post' | 'comment' | 'translation';
|
||||
interface RoutingDecision {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
@@ -55,6 +56,24 @@ export class ProviderRouterService {
|
||||
}): RoutingDecision {
|
||||
const { language, contentType, style, tone } = params;
|
||||
|
||||
if(contentType ==='translation') {
|
||||
if (language === 'cn') {
|
||||
// Default EN
|
||||
return {
|
||||
writer: 'deepseek',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'CN default: GPT reliable',
|
||||
};
|
||||
}
|
||||
return {
|
||||
writer: getRandomElement([ 'openai',]),
|
||||
reviewer: 'openai',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN default: GPT reliable',
|
||||
};
|
||||
}
|
||||
|
||||
if (tone === ContentTone.EMPATHETIC) {
|
||||
return {
|
||||
writer: 'openai', // warmest voice, less "AI-ish"
|
||||
@@ -69,7 +88,7 @@ export class ProviderRouterService {
|
||||
if (tone && isEdgyTone(tone)) {
|
||||
if (language === 'en') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: getRandomElement(['openai', 'google', 'grok']),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
@@ -77,14 +96,14 @@ export class ProviderRouterService {
|
||||
}
|
||||
if (tone === ContentTone.SPICY) {
|
||||
return {
|
||||
writer: 'openai',
|
||||
writer: getRandomElement(['deepseek', 'google', 'openai',]),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
writer: 'deepseek',
|
||||
writer: getRandomElement(['deepseek', 'google',]),
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`,
|
||||
@@ -96,9 +115,9 @@ export class ProviderRouterService {
|
||||
// Breaking news EN -> Grok (real-time + X-native)
|
||||
if (style === ContentStyle.BREAKING_NEWS) {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: 'google',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: true,
|
||||
useXEnrichment: false,
|
||||
reason: 'EN breaking news: Grok has real-time X context',
|
||||
};
|
||||
}
|
||||
@@ -106,7 +125,7 @@ export class ProviderRouterService {
|
||||
// Comment EN casual/witty -> Grok
|
||||
if (contentType === 'comment' && tone !== 'professional') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
writer: 'google',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN casual comment: Grok sounds most human on X',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {ContentTone, isEdgyTone} from '../enum/tone.enum';
|
||||
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
|
||||
import {QuoteType} from "../enum/quote-type.enum";
|
||||
import {ContentSafetyService} from "./content-safety.service";
|
||||
import {TranslatorService} from "./translator.service";
|
||||
|
||||
@Injectable()
|
||||
export class QuoteWriterService {
|
||||
@@ -25,10 +26,11 @@ export class QuoteWriterService {
|
||||
private lengthStrategy: LengthStrategyService,
|
||||
private reviewer: ReviewerService,
|
||||
private config: ConfigService,
|
||||
private safety: ContentSafetyService
|
||||
private safety: ContentSafetyService,
|
||||
private translatorService: TranslatorService
|
||||
) {}
|
||||
|
||||
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0) {
|
||||
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0 , allowTranslateToVi =true) {
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
if (_retryCount >= MAX_RETRIES) {
|
||||
@@ -145,6 +147,21 @@ export class QuoteWriterService {
|
||||
);
|
||||
}
|
||||
|
||||
//transla if language != vi
|
||||
let quoteTransVi = '';
|
||||
let quoteTransModel = '';
|
||||
let tokenTranslatorUsed = 0;
|
||||
if (allowTranslateToVi && dto.language !== 'vi') {
|
||||
const resT = await this.translatorService.translator({
|
||||
text: quote,
|
||||
target_lang: 'vi',
|
||||
target_model: 'google'
|
||||
})
|
||||
quoteTransVi = resT.content;
|
||||
quoteTransModel = resT.model;
|
||||
tokenTranslatorUsed = resT.tokensUsed;
|
||||
}
|
||||
|
||||
return {
|
||||
quote,
|
||||
quoteType,
|
||||
@@ -153,6 +170,9 @@ export class QuoteWriterService {
|
||||
reviewNotes,
|
||||
tokensUsed: totalTokens,
|
||||
model: modelUsed,
|
||||
quoteTransVi,
|
||||
quoteTransModel,
|
||||
tokenTranslatorUsed
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ export class StyleDetectorService {
|
||||
}
|
||||
|
||||
detectLanguageFromTelegramAutoContent(text: string): Language {
|
||||
if (/nhật[ _]bản/i.test(text)) return "ja";
|
||||
if (/#hàn_quốc/i.test(text)) return "ko";
|
||||
// if (/nhật[ _]bản/i.test(text)) return "ja";
|
||||
// if (/#hàn_quốc/i.test(text)) return "ko";
|
||||
|
||||
// return getLanguageByJSTTime();
|
||||
//
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {AiTranslatorDto} from "../dto/ai-translator.dto";
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {LANGUAGE_NAMES} from "../prompts/templates";
|
||||
import {Injectable, Logger} from "@nestjs/common";
|
||||
import {AIProviderFactory, ProviderName} from "../providers/ai-provider.factory";
|
||||
import {ProviderRouterService} from "./provider-router.service";
|
||||
|
||||
@Injectable()
|
||||
export class TranslatorService {
|
||||
private readonly logger = new Logger(TranslatorService.name);
|
||||
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
) {
|
||||
}
|
||||
async translator(dto: AiTranslatorDto) {
|
||||
this.logger.log(`Translating ...`);
|
||||
const targetLanguage = LANGUAGE_NAMES[dto.target_lang] || LANGUAGE_NAMES['en'];
|
||||
const systemPrompt =
|
||||
`You are an expert X (Twitter) translator.
|
||||
Rules:
|
||||
- Translate naturally and fluently.
|
||||
- Preserve the original tone, emotion, and internet culture.
|
||||
- Keep the post concise and punchy like an actual X post.
|
||||
- Preserve slang, memes, sarcasm, and crypto terminology.
|
||||
- Do NOT over-formalize the translation.
|
||||
- Keep token names, ticker symbols, usernames, hashtags, and project names unchanged.
|
||||
- Preserve emojis, line breaks, and formatting.
|
||||
- Do not add explanations, notes, or extra commentary.
|
||||
- Avoid robotic or textbook-style translation.
|
||||
- Output ONLY the translated text.`;
|
||||
const USER_PROMPTS_HINT: Record<Language, string> = {
|
||||
'en':'Translate to English:',
|
||||
'cn':'Translate to Chinese:',
|
||||
'vi':'Translate to Vietnamese:',
|
||||
'ja':'Translate to Japanese:',
|
||||
'ko':'Translate to Korean:',
|
||||
}
|
||||
const userPrompt = [
|
||||
`[Target Language: ${targetLanguage}]
|
||||
IMPORTANT: Your previous answer violated language rules.`,
|
||||
USER_PROMPTS_HINT[dto.target_lang],
|
||||
dto.text
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
if(!dto.target_model) {
|
||||
// 🧭 Smart routing
|
||||
const decision = this.router.route({
|
||||
language: dto.target_lang,
|
||||
contentType: 'translation',
|
||||
});
|
||||
dto.target_model = decision.writer;
|
||||
}
|
||||
const provider = this.factory.get(dto.target_model);
|
||||
const draft = await provider.complete(
|
||||
[
|
||||
{role: 'system', content: systemPrompt},
|
||||
{role: 'user', content: userPrompt},
|
||||
],
|
||||
{ temperature: 0.1},
|
||||
);
|
||||
this.logger.debug(`===> ${draft.model} đã dich xong!`);
|
||||
this.logger.debug(`===> ${draft.content}`);
|
||||
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {Context, Telegraf} from "telegraf";
|
||||
import {_JsonToStr} from "../../shared/helper";
|
||||
import {ContentWriterService} from "../content-writer/content-writer.service";
|
||||
import {GenerateContentDto} from "../content-writer/dto/generate-content.dto";
|
||||
import {TranslatorService} from "../content-writer/services/translator.service";
|
||||
|
||||
@Processor('manager_task_queue')
|
||||
export class ManagerProcessor extends WorkerHost {
|
||||
@@ -15,6 +16,7 @@ export class ManagerProcessor extends WorkerHost {
|
||||
constructor(
|
||||
private aiService: AIService,
|
||||
private contentWriterService: ContentWriterService,
|
||||
private translatorService: TranslatorService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
) {
|
||||
super();
|
||||
@@ -24,7 +26,7 @@ export class ManagerProcessor extends WorkerHost {
|
||||
console.log(`ManagerProcessor ==> process => ${job.name}`);
|
||||
console.log('job_data', job.data);
|
||||
|
||||
let {rawData, style, telegramChatId} = job.data;
|
||||
let {rawData, style, telegramChatId, targetLanguage, text} = job.data;
|
||||
|
||||
switch (job.name) {
|
||||
case 'analyze_news':
|
||||
@@ -54,6 +56,23 @@ export class ManagerProcessor extends WorkerHost {
|
||||
|
||||
return {status: 'completed'};
|
||||
}
|
||||
case 'translator_text': {
|
||||
// Gọi AI phân tích
|
||||
const returnQuestion = await this.translatorService.translator({
|
||||
text,
|
||||
target_lang: targetLanguage
|
||||
});
|
||||
// console.log(analysis.result);
|
||||
console.log('telegramChatId:', telegramChatId);
|
||||
await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId,
|
||||
`${returnQuestion.content}`,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
}
|
||||
);
|
||||
|
||||
return {status: 'completed'};
|
||||
}
|
||||
case 'enrich_x_context': {
|
||||
const dto: GenerateContentDto = {
|
||||
topic: rawData,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Injectable} from '@nestjs/common';
|
||||
import {InjectQueue} from '@nestjs/bullmq';
|
||||
import {Queue} from 'bullmq';
|
||||
import {QuoteType} from "../content-writer/enum/quote-type.enum";
|
||||
import {Language} from "../../common/interfaces/language.prompt.interface";
|
||||
|
||||
@Injectable()
|
||||
export class ManagerService {
|
||||
@@ -77,6 +78,18 @@ export class ManagerService {
|
||||
}, {attempts: 1, backoff: 5000})
|
||||
}
|
||||
|
||||
async handleTranslatorText(text: string, targetLanguage: Language, telegramChatId: number = 0) {
|
||||
await this.managerTaskQueue.add('translator_text', {
|
||||
text,
|
||||
targetLanguage,
|
||||
telegramChatId
|
||||
}, {attempts: 1, backoff: 5000});
|
||||
// // 2. Đẩy việc cho AI-C tìm video TikTok
|
||||
// await this.ttQueue.add('find_video', {
|
||||
// keyword: trendData.keyword,
|
||||
// }, {attempts: 2});
|
||||
}
|
||||
|
||||
async handleAskQues(question: string, telegramChatId: number = 0) {
|
||||
await this.managerTaskQueue.add('asking', {
|
||||
rawData: question,
|
||||
@@ -133,6 +146,7 @@ export class ManagerService {
|
||||
telegramChatId: undefined,
|
||||
},
|
||||
) {
|
||||
console.log('manualTriggerWriteWithReview', dto);
|
||||
// 1. Đẩy việc cho AI-B viết bài FB
|
||||
const manualtriggerresult = await this.fbQueue.add('generate_post_ver2', {
|
||||
title: dto.keyword,
|
||||
@@ -159,6 +173,34 @@ export class ManagerService {
|
||||
}
|
||||
}
|
||||
|
||||
async triggerAutoGenerateCommentTwitter(
|
||||
TwitterUrl: string,
|
||||
x_username: string,
|
||||
preferLanguage?: string,
|
||||
telegramChatId?:number
|
||||
) {
|
||||
await this.commentQueue.add('GENERATE_AUTO_COMMENT_TWITTER', {
|
||||
tweetUrl: TwitterUrl,
|
||||
language: preferLanguage,
|
||||
telegramChatId,
|
||||
x_username: x_username || 'realflashkaze',
|
||||
}, {
|
||||
attempts: 1,
|
||||
backoff: 5000,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
|
||||
//console.log(manualtriggerresult);
|
||||
|
||||
return {
|
||||
needsConfirm: 1,
|
||||
summary: '',
|
||||
id: '',
|
||||
}
|
||||
}
|
||||
|
||||
async manualTriggerCommentLinkTwitter(TwitterUrl, preferLanguage = 'en', telegramChatId = '') {
|
||||
await this.commentQueue.add('generate_comment_twitter', {
|
||||
url: TwitterUrl,
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
// post.service.ts
|
||||
import {Injectable} from '@nestjs/common';
|
||||
import {Injectable, Logger} from '@nestjs/common';
|
||||
import {SQS_QUEUES_NAME, SqsService} from './sqs.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class SqsPostService {
|
||||
private readonly logger = new Logger('SqsPostService');
|
||||
|
||||
constructor(private readonly sqs: SqsService) {
|
||||
}
|
||||
|
||||
async sendMessage(username: string, data) {
|
||||
async sendMessage(username: string, data, delaySeconds = 0) {
|
||||
console.log(`SqsPostService -> sendMessage ${username} `);
|
||||
switch (username) {
|
||||
case 'realflashkaze': {
|
||||
return this.postFlashKaze(data)
|
||||
await this.postFlashKaze(data, delaySeconds);
|
||||
console.log(`SqsPostService -> sendMessage done ${username} `);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'echcomvuive': {
|
||||
return this.postEchCom(data)
|
||||
await this.postEchCom(data, delaySeconds);
|
||||
console.log(`SqsPostService -> sendMessage done ${username} `);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.logger.error(`SqsPostService -> K tim thay username=${username} `);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async postFlashKaze(data: any): Promise<any> {
|
||||
async postFlashKaze(data: any, delaySeconds = 0): Promise<any> {
|
||||
console.log(`SqsPostService_postFlashKaze`)
|
||||
return this.sqs.enqueue(
|
||||
SQS_QUEUES_NAME.REALFLASHKAZE!,
|
||||
data,
|
||||
{
|
||||
jobId: `acc1-${Date.now()}`,
|
||||
delaySeconds: Math.floor(Math.random() * 30),
|
||||
delaySeconds: delaySeconds > 0 ? delaySeconds : Math.floor(Math.random() * 30),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async postEchCom(data: any) {
|
||||
async postEchCom(data: any, delaySeconds = 0) {
|
||||
console.log(`SqsPostService_postEchCom`)
|
||||
|
||||
await this.sqs.enqueue(
|
||||
@@ -42,7 +51,7 @@ export class SqsPostService {
|
||||
data,
|
||||
{
|
||||
jobId: `acc2-${Date.now()}`,
|
||||
delaySeconds: Math.floor(Math.random() * 30),
|
||||
delaySeconds: delaySeconds > 0 ? delaySeconds : Math.floor(Math.random() * 30),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,38 +29,15 @@ export class TelegramContentWriterCompletedProcessor extends WorkerHost {
|
||||
console.log('TelegramProcessor_facebook_content_writer_completed_process=>posting')
|
||||
//console.log(fbContent);
|
||||
if (job.name === 'generate_post_completed') {
|
||||
const {id, needConfirm, content, autoPublish, telegramChatId, xSubmitProvider} = job.data;
|
||||
const {
|
||||
id, needConfirm, content, autoPublish, telegramChatId, xSubmitProvider,
|
||||
contentTransVi,
|
||||
contentTransModel,
|
||||
} = job.data;
|
||||
console.log({id, needConfirm, autoPublish, xSubmitProvider})
|
||||
const X_USERS = process.env.TWITTER_USERNAMES!.split(',');
|
||||
console.log({X_USERS});
|
||||
//console.log({job})
|
||||
// if (autoPublish) {
|
||||
// // await this.publishPageService.publishTwitter(id);
|
||||
// // await this.publishPageService.publishToFacebook(id);
|
||||
// const postId = toNumber(id);
|
||||
// await this.bot.telegram.sendMessage(adminChatId, '🤖Đã viết xong tin auto:');
|
||||
//
|
||||
// try {
|
||||
// await Promise.allSettled([
|
||||
// this.publishPageService.publishToFacebook(postId).then(() => {
|
||||
// this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên FB số ${postId} thành công`);
|
||||
// }).catch(err => {
|
||||
// this.bot.telegram.sendMessage(adminChatId, `PublishToFacebook error ${err.message}`);
|
||||
//
|
||||
// }),
|
||||
// this.publishPageService.publishTwitter(postId, xSubmitProvider).then((resp) => {
|
||||
// this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên X số ${postId} thành công via ${xSubmitProvider}, ${resp?.url}`);
|
||||
// }).catch(err => {
|
||||
// this.bot.telegram.sendMessage(adminChatId, `PublishToX error ${err.message}`);
|
||||
// }),
|
||||
// ]
|
||||
// )
|
||||
// } catch (e) {
|
||||
// await this.bot.telegram.sendMessage(adminChatId, `❌ Lỗi đăng bài: ${e.message}`);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId, '🤖Đã viết xong:');
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `🤖Đã viết xong: Dich: ${contentTransVi}`);
|
||||
await this.bot.telegram.sendMessage(telegramChatId || adminChatId,
|
||||
`${content}\n\n`,
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@ import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
QuoteWizard,
|
||||
Comment2Wizard,
|
||||
XImageUploadService,
|
||||
SqsPostService
|
||||
SqsPostService,
|
||||
],
|
||||
})
|
||||
export class TelegramModule implements OnModuleInit{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Action, Command, Ctx, On, Start, Update} from 'nestjs-telegraf';
|
||||
import {Action, Command, Ctx, Hears, Message, On, Start, Update} from 'nestjs-telegraf';
|
||||
import {Context, Scenes} from 'telegraf';
|
||||
import {HttpException, Injectable} from "@nestjs/common";
|
||||
import {ManagerService} from "../manager/manager.service";
|
||||
@@ -18,6 +18,8 @@ import {TextUtil} from "../../common/utils/text.util";
|
||||
import {XStrategy} from "../social-api/x-router.service";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||
import {PgPostService} from "../../shared/pg.post.service";
|
||||
import {Language} from "../../common/interfaces/language.prompt.interface";
|
||||
|
||||
@Injectable()
|
||||
@Update()
|
||||
@@ -30,7 +32,8 @@ export class TelegramUpdates {
|
||||
private readonly trendsService: TrendsService,
|
||||
private readonly xUpload: XImageUploadService,
|
||||
private readonly cacheService: XCacheService,
|
||||
private readonly sqsPostService: SqsPostService
|
||||
private readonly sqsPostService: SqsPostService,
|
||||
private readonly pgPost: PgPostService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -110,14 +113,79 @@ export class TelegramUpdates {
|
||||
async onWizardCommand(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
||||
await ctx.scene.enter(WIZARD_COMMENT_SCENE_ID);
|
||||
}
|
||||
@Command('chatid')
|
||||
async onGetChatId(ctx: Context) {
|
||||
// @ts-ignore
|
||||
ctx.reply(ctx.chat.id)
|
||||
}
|
||||
|
||||
@Command('atc')
|
||||
async onAutoCommentCommand(@Ctx() ctx: Context): Promise<void> {
|
||||
//@ts-ignore
|
||||
let {command, payload} = ctx;
|
||||
if (!payload) {
|
||||
await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} LinkX`);
|
||||
return;
|
||||
}
|
||||
await this.sharedAutoComment(ctx, payload);
|
||||
}
|
||||
@Action('action_auto_comment')
|
||||
async onDownloadAction(@Ctx() ctx: Context) {
|
||||
await ctx.answerCbQuery(); // Tắt trạng thái loading trên nút bấm
|
||||
|
||||
// Ép kiểu để ép TypeScript nhận diện đúng đối tượng tin nhắn chữ
|
||||
// @ts-ignore
|
||||
const message = ctx.callbackQuery.message as any;
|
||||
|
||||
// 💡 LẤY ĐƯỢC LUÔN: Đây chính là nội dung text của tin nhắn chứa nút bấm này!
|
||||
const textInMessage = message.text;
|
||||
|
||||
// await ctx.reply(`Tôi đọc được chữ từ tin nhắn cũ: ${textInMessage}`);
|
||||
|
||||
await this.sharedAutoComment(ctx, textInMessage);
|
||||
}
|
||||
|
||||
private async sharedAutoComment(ctx: Context, linkX:string): Promise<void> {
|
||||
// console.log(ctx);
|
||||
// @ts-ignore
|
||||
if (!linkX) {
|
||||
await ctx.reply(`Thiếu LinkX`);
|
||||
return;
|
||||
}
|
||||
const detectSendLinkX = TextUtil.detectLinkX(linkX);
|
||||
if (detectSendLinkX.hasLinkX) {
|
||||
// @ts-ignore
|
||||
linkX = detectSendLinkX.url;
|
||||
} else {
|
||||
await ctx.reply('Không có link X');
|
||||
return;
|
||||
}
|
||||
const chatId = ctx.chat?.id;
|
||||
|
||||
await this.managerService.triggerAutoGenerateCommentTwitter(
|
||||
linkX,
|
||||
'realflashkaze',
|
||||
'en',
|
||||
chatId,
|
||||
);
|
||||
await ctx.reply('Chờ xử lý ...');
|
||||
}
|
||||
|
||||
|
||||
@Command([
|
||||
'comvi',
|
||||
'cvi',
|
||||
'cvil',
|
||||
'comen',
|
||||
'cen',
|
||||
'comja',
|
||||
'cja',
|
||||
'comjal',
|
||||
'cjal',
|
||||
'comko',
|
||||
'cko',
|
||||
'comcn',
|
||||
'ccn',
|
||||
])
|
||||
async onCommentAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
||||
|
||||
@@ -125,16 +193,19 @@ export class TelegramUpdates {
|
||||
let xpostreader = 'api'
|
||||
// @ts-ignore
|
||||
let {command, payload} = ctx;
|
||||
if (['comvi', 'comvn', 'com_vi'].includes(command)) {
|
||||
if (['cvi', 'cvil', 'comvi', 'comvn', 'com_vi'].includes(command)) {
|
||||
language = 'vi';
|
||||
if (['cvil', 'comvil'].includes(command)) {
|
||||
xpostreader = 'browser'
|
||||
}
|
||||
}
|
||||
if (['comko', 'comkr', 'com_ko'].includes(command)) {
|
||||
language = 'ko';
|
||||
}
|
||||
if (['comja', 'com_ja',].includes(command)) {
|
||||
if (['comja', 'com_ja', 'cja', 'cjal'].includes(command)) {
|
||||
language = 'ja';
|
||||
}
|
||||
if (['comjal', 'comjalong' ].includes(command)) {
|
||||
if (['comjal', 'comjalong', 'cjal'].includes(command)) {
|
||||
language = 'ja';
|
||||
xpostreader = 'browser';
|
||||
}
|
||||
@@ -155,7 +226,7 @@ export class TelegramUpdates {
|
||||
const match = _linkX.match(/status\/(\d+)/);
|
||||
if (match) {
|
||||
//nêu match => get content x
|
||||
const xpost = await this.xReaderService.readXPost(_linkX, xpostreader, chatId);
|
||||
const xpost: any = await this.xReaderService.readXPost(_linkX, xpostreader, chatId);
|
||||
console.log('==> content text:' + xpost.text);
|
||||
payload = xpost.text;
|
||||
tweetId = xpost.tweetId;
|
||||
@@ -176,11 +247,15 @@ export class TelegramUpdates {
|
||||
'quote_vi',
|
||||
'quotevi',
|
||||
'quotevn',
|
||||
'quotevnl',
|
||||
'qvnl',
|
||||
'quote_en',
|
||||
'quoteen',
|
||||
'quote_ja',
|
||||
'quoteja',
|
||||
'quotejal',
|
||||
'qjal',
|
||||
'qja',
|
||||
'quote_jp',
|
||||
'quotejp',
|
||||
'quote_ko',
|
||||
@@ -191,15 +266,17 @@ export class TelegramUpdates {
|
||||
// @ts-ignore
|
||||
let {command, payload} = ctx;
|
||||
let xreaderMode = 'api';
|
||||
if(['quotejal'].includes(command)) {
|
||||
command = 'quote_ja';
|
||||
|
||||
if (['quotevnl', 'qvnl', 'quotejal', 'qjal'].includes(command)) {
|
||||
xreaderMode = 'browser';
|
||||
}
|
||||
if (['quote_jp', 'quotejp', 'quoteja'].includes(command)) {
|
||||
|
||||
if (['quote_jp', 'quotejp', 'quoteja', 'qja', 'quotejal'].includes(command)) {
|
||||
command = 'quote_ja';
|
||||
}
|
||||
if (['quote_vn', 'quotevi', 'quotevn'].includes(command)) {
|
||||
if (['quote_vn', 'quotevi', 'quotevn', 'quotevnl', 'qvnl'].includes(command)) {
|
||||
command = 'quote_vi';
|
||||
|
||||
}
|
||||
if (['quoteen'].includes(command)) {
|
||||
command = 'quote_en';
|
||||
@@ -329,8 +406,13 @@ export class TelegramUpdates {
|
||||
const detectSendLinkX = TextUtil.detectLinkX(payload);
|
||||
if (detectSendLinkX.hasLinkX) {
|
||||
const xpost = await this.xReaderService.readXPost(detectSendLinkX.url);
|
||||
// @ts-ignore
|
||||
payload = xpost.text;
|
||||
}
|
||||
if (!payload) {
|
||||
await ctx.reply(`reader không được`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.scene.enter(WIZARD_WRITER_SCENE_ID, {
|
||||
@@ -354,8 +436,13 @@ export class TelegramUpdates {
|
||||
const postNo = 1 * payload;
|
||||
await ctx.reply(`Đang đẩy bài ${payload} lên Facebook, Twitter`);
|
||||
|
||||
const post = await this.pgPost.getById(postNo);
|
||||
if (!post) {
|
||||
await ctx.reply('No found');
|
||||
return;
|
||||
}
|
||||
await ctx.reply(
|
||||
`🤖 **Vui lòng chọn kênh**`,
|
||||
post.content!,
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
@@ -429,6 +516,45 @@ export class TelegramUpdates {
|
||||
await ctx.reply(`vui lòng đưa chờ phân tích ....`);
|
||||
}
|
||||
|
||||
@Command(['tvi', 'tja', 'ten', 'tko', 'tcn'])
|
||||
async onTranslator(ctx: Context) {
|
||||
// @ts-ignore
|
||||
const {command, payload} = ctx;
|
||||
let lang: Language = 'vi'
|
||||
switch (command) {
|
||||
case 'tvi': {
|
||||
lang = 'vi';
|
||||
break;
|
||||
}
|
||||
case 'tja': {
|
||||
lang = 'ja';
|
||||
break;
|
||||
}
|
||||
case 'ten': {
|
||||
lang = 'en';
|
||||
break;
|
||||
}
|
||||
case 'tko': {
|
||||
lang = 'ko';
|
||||
break;
|
||||
}
|
||||
case 'tcn': {
|
||||
lang = 'cn';
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
await ctx.reply(`vui lòng đưa text, ví dụ: /${command} Hot news today in japan ?`);
|
||||
return;
|
||||
}
|
||||
console.log(ctx.chat);
|
||||
await this.managerService.handleTranslatorText(payload, lang, ctx.chat?.id);
|
||||
await ctx.reply(`vui lòng chờ ....`);
|
||||
}
|
||||
|
||||
|
||||
@Command(['xreader', 'xrbr'])
|
||||
async onXReader(ctx: Context) {
|
||||
// @ts-ignore
|
||||
@@ -483,7 +609,7 @@ export class TelegramUpdates {
|
||||
const callbackData = (ctx.callbackQuery as any).data;
|
||||
const social = callbackData.split('_')[1];
|
||||
const postId = _toNum(callbackData.split('_')[2]);
|
||||
const xUsername = callbackData.split('_')[3];
|
||||
let xUsername = callbackData.split('_')[3];
|
||||
|
||||
await ctx.answerCbQuery(`Đang đẩy bài lên Fb,X ${xUsername}...`);
|
||||
|
||||
@@ -511,6 +637,7 @@ export class TelegramUpdates {
|
||||
case 'facebook':
|
||||
allowFb = 1;
|
||||
publishTo = ['fb'];
|
||||
xUsername = 'realflashkaze';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -818,9 +945,21 @@ export class TelegramUpdates {
|
||||
}
|
||||
}
|
||||
|
||||
@Command('chatid')
|
||||
async onGetChatId(ctx: Context) {
|
||||
// @ts-ignore
|
||||
ctx.reply(ctx.chat.id)
|
||||
// Pattern này sẽ bắt tất cả các link dạng ://x.com... hoặc ://twitter.com...
|
||||
@Hears(/https?:\/\/(www\.)?(x\.com|twitter\.com)\/[a-zA-DR-Z0-9_]+/i)
|
||||
async handleXLink(@Ctx() ctx: Context, @Message('text') text: string) {
|
||||
// text ở đây chính là toàn bộ đoạn tin nhắn chứa link x.com
|
||||
|
||||
// Ví dụ xử lý: Trích xuất phần ID hoặc convert link để preview (như fixupx.com)
|
||||
// const fixedLink = text.replace(/x\.com|twitter\.com/i, 'fixupx.com');
|
||||
|
||||
await ctx.reply(text, {
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '📥 Chạy lệnh auto cmt', callback_data: 'action_auto_comment' }]
|
||||
]
|
||||
}
|
||||
});
|
||||
//return `Tôi đã tìm thấy link X! Đây là bản preview: ${fixedLink}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Inject, Injectable} from "@nestjs/common";
|
||||
import {Cache, CACHE_MANAGER} from "@nestjs/cache-manager";
|
||||
import {_toNum} from "../../shared/helper";
|
||||
|
||||
@Injectable()
|
||||
export class XCacheService {
|
||||
@@ -21,6 +22,15 @@ export class XCacheService {
|
||||
return data;
|
||||
}
|
||||
|
||||
async setAlreadyComment(tweetId: string, xUsername: string) {
|
||||
await this.setCachedKey(`comment_tweetId:${xUsername}:${tweetId}`, 1, 4 * 3600);
|
||||
return {tweetId, xUsername};
|
||||
}
|
||||
async hasComment(tweetId: string, xUsername: string) {
|
||||
const v = await this.getCachedData(`comment_tweetId:${xUsername}:${tweetId}`);
|
||||
return 1 === _toNum(v);
|
||||
}
|
||||
|
||||
async setCacheTweetUrlById(tweetId, tweetUrl) {
|
||||
await this.setCachedKey(`x_tweetId:${tweetId}`, tweetUrl, 24 * 3600);
|
||||
return {tweetId, tweetUrl};
|
||||
@@ -30,6 +40,26 @@ export class XCacheService {
|
||||
return await this.getCachedData(`x_tweetId:${tweetId}`) as string;
|
||||
}
|
||||
|
||||
async setCacheTweetContentById(tweetId, content) {
|
||||
console.log('==> setCacheTweetContentById');
|
||||
await this.setCachedKey(`x_tweet_content:${tweetId}`, content, 12 * 3600);
|
||||
return {tweetId, console};
|
||||
}
|
||||
|
||||
async getCacheTweetContentById(tweetId): Promise<Partial<{
|
||||
stats: any
|
||||
quoted: {}
|
||||
via: string
|
||||
tweetId: any
|
||||
text?: any
|
||||
author?: any
|
||||
handle?: any
|
||||
time?: any
|
||||
lang?: string
|
||||
}> | undefined> {
|
||||
return await this.getCachedData(`x_tweet_content:${tweetId}`);
|
||||
}
|
||||
|
||||
async incrCountCollectNewsapi() {
|
||||
//this.cacheManager.c
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {chromium} from 'playwright';
|
||||
import {Injectable} from "@nestjs/common";
|
||||
import {Injectable, Logger} from "@nestjs/common";
|
||||
import axios from "axios";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
import {InjectBot} from "nestjs-telegraf";
|
||||
@@ -7,13 +7,15 @@ import {Context, Telegraf} from "telegraf";
|
||||
|
||||
@Injectable()
|
||||
export class XReaderService {
|
||||
private logger = new Logger('XReaderService');
|
||||
|
||||
constructor(
|
||||
private readonly cacheService: XCacheService,
|
||||
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||
) {
|
||||
}
|
||||
|
||||
async readXPostViaBrowserV2(url, telegramChatId = 0) {
|
||||
async readXPostViaBrowserV2(url, telegramChatId = 0, useCache = true) {
|
||||
// Normalize URL
|
||||
console.log(`[X] XReaderService -> readXPostViaBrowserV2...`);
|
||||
// @ts-ignore
|
||||
@@ -25,6 +27,22 @@ export class XReaderService {
|
||||
if (!match) throw new Error('URL X không hợp lệ');
|
||||
const tweetId = match[1];
|
||||
console.log({tweetId, url});
|
||||
|
||||
//
|
||||
if (useCache) {
|
||||
this.logger.debug('Find from cache ...')
|
||||
const fCacheData = await this.cacheService.getCacheTweetContentById(tweetId);
|
||||
if (fCacheData) {
|
||||
this.logger.log(`[X] XReaderService ->found data cache, return.`);
|
||||
return {
|
||||
...fCacheData,
|
||||
isCached: true,
|
||||
};
|
||||
}
|
||||
this.logger.log(`[X] XReaderService -> no cache, start browser...`);
|
||||
}
|
||||
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
args: ['--no-sandbox', '--disable-blink-features=AutomationControlled']
|
||||
@@ -85,6 +103,7 @@ export class XReaderService {
|
||||
// Text
|
||||
const textEl = root.querySelector('[data-testid="tweetText"]');
|
||||
const text = textEl?.innerText || '';
|
||||
const lang = textEl?.getAttribute('lang') || '';
|
||||
console.log(`==> 4`);
|
||||
// User info
|
||||
const userEl = root.querySelector('[data-testid="User-Name"]');
|
||||
@@ -120,6 +139,7 @@ export class XReaderService {
|
||||
console.log(`==> 8`);
|
||||
return {
|
||||
text,
|
||||
lang,
|
||||
author,
|
||||
handle,
|
||||
time,
|
||||
@@ -221,6 +241,12 @@ export class XReaderService {
|
||||
|
||||
await browser.close();
|
||||
if (!data) throw new Error(`Không parse được tweet ${tweetId} (có thể bị ẩn/xoá)`);
|
||||
|
||||
if (data) {
|
||||
if (data.text) {
|
||||
await this.cacheService.setCacheTweetContentById(tweetId, data)
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
try {
|
||||
@@ -341,7 +367,7 @@ export class XReaderService {
|
||||
}
|
||||
}
|
||||
|
||||
async readXPost(url, crawlerType: string = 'any', telegramChatId = 0) {
|
||||
async readXPost(url, crawlerType: string = 'any', telegramChatId = 0, useCache=true) {
|
||||
console.log(`[X] XReaderService -> readXPost...`);
|
||||
url = url.replace('twitter.com', 'x.com').split('?')[0];
|
||||
const match = url.match(/status\/(\d+)/);
|
||||
@@ -350,14 +376,14 @@ export class XReaderService {
|
||||
console.log({tweetId, url});
|
||||
await this.cacheService.setCacheTweetUrlById(tweetId, url);
|
||||
if (crawlerType === 'browser') {
|
||||
return await this.readXPostViaBrowserV2(url, telegramChatId);
|
||||
return await this.readXPostViaBrowserV2(url, telegramChatId, useCache);
|
||||
}
|
||||
try {
|
||||
console.log('[X] Thử syndication API...');
|
||||
return await this.readXPostViaApi(url);
|
||||
} catch (err) {
|
||||
console.log(`[X] API fail (${err.message}), fallback sang browser...`);
|
||||
return await this.readXPostViaBrowserV2(url, telegramChatId);
|
||||
return await this.readXPostViaBrowserV2(url, telegramChatId, useCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,3 +67,20 @@ export const getLanguageByJSTTime = () => {
|
||||
|
||||
return 'en';
|
||||
}
|
||||
|
||||
export function getRandomElement<T>(array: T[]): T {
|
||||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
export function getUuid4() {
|
||||
const uuidv4 = require('uuid');
|
||||
|
||||
return uuidv4(); // Generates a random version 4 UUID
|
||||
}
|
||||
export function _getTweetIdFromUrl(xUrl: string) {
|
||||
xUrl = xUrl.replace('twitter.com', 'x.com').split('?')[0];
|
||||
const match = xUrl.match(/status\/(\d+)/);
|
||||
if (!match) throw new Error('getTweetIdFromUrl: URL X không hợp lệ');
|
||||
return match[1];
|
||||
}
|
||||
@@ -13,6 +13,10 @@ export class PgPostService {
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id: number): Promise<Post | null> {
|
||||
return this.post({id})
|
||||
}
|
||||
|
||||
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
|
||||
return this.prisma.post.create({
|
||||
data,
|
||||
|
||||
Reference in New Issue
Block a user