diff --git a/chrom-ext/background.js b/chrom-ext/background.js new file mode 100644 index 0000000..d01afe7 --- /dev/null +++ b/chrom-ext/background.js @@ -0,0 +1,63 @@ +// ===== 1. TẠO CONTEXT MENU ===== +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: 'writeComment', + title: '✍️ Viết comment', + contexts: ['all'], + documentUrlPatterns: ['https://x.com/*', 'https://twitter.com/*'] + }); +}); + +// ===== 2. KHI CLICK VÀO MENU ===== +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === 'writeComment') { + chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => { + // Nếu content script chưa sẵn sàng (hiếm) thì bỏ qua + }); + } +}); + +// ===== 3. NHẬN YÊU CẦU TỪ CONTENT SCRIPT VÀ GỌI API ===== +chrome.runtime.onMessage.addListener((request, sender) => { + if (request.action === 'GENERATE_COMMENT') { + callYourAPI(request.data, sender.tab.id); + } +}); + +async function callYourAPI({ + text, + tone, + angle, + language + }, tabId) { + try { + // ⚠️ THAY BẰNG ENDPOINT CỦA BẠN + const API_URL = 'https://gamerdota0042-himalayas.nord:3000'; + const commentApi='/content-writer/comment' + const API_KEY = 'YOUR_API_KEY_HERE'; // Nên chuyển sang chrome.storage nếu publish + + const res = await fetch(`${API_URL}${commentApi}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` + }, + body: JSON.stringify({ + originalPost: text, + tone: tone, + angle: angle, + language, + }) + }); + + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const data = await res.json(); + // Giả định API trả về: { comment: "..." } + const comment = data.comment || data.text || JSON.stringify(data); + + chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment }); + } catch (err) { + chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message }); + } +} \ No newline at end of file diff --git a/chrom-ext/content.js b/chrom-ext/content.js new file mode 100644 index 0000000..fadd2c5 --- /dev/null +++ b/chrom-ext/content.js @@ -0,0 +1,210 @@ +(() => { + 'use strict'; + + let selectedTweetText = ''; + + // ===== A. LẮNG NGHE CHUỘT PHẢI ĐỂ LẤY TWEET TEXT ===== + document.addEventListener('contextmenu', (e) => { + const article = e.target.closest('article[data-testid="tweet"]'); + if (!article) { + selectedTweetText = ''; + return; + } + const textContainer = article.querySelector('[data-testid="tweetText"]'); + selectedTweetText = textContainer ? textContainer.innerText.trim() : ''; + }); + + // ===== B. LẮNG NGHE MESSAGE TỪ BACKGROUND ===== + chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { + if (req.action === 'OPEN_FORM') { + if (!selectedTweetText) { + alert('Không tìm thấy nội dung tweet. Hãy chuột phải vào vùng văn bản của tweet.'); + return; + } + openModal(selectedTweetText); + } else if (req.action === 'SHOW_RESULT') { + showResult(req.comment); + } else if (req.action === 'SHOW_ERROR') { + showError(req.error); + } + }); + + // ===== C. MỞ MODAL (DÙNG SHADOW DOM ĐỂ TRÁNH CSS XUNG ĐỘT) ===== + function openModal(tweetText) { + removeModal(); // Xóa cũ nếu có + + const host = document.createElement('div'); + host.id = 'x-comment-ai-host'; + Object.assign(host.style, { + position: 'fixed', + top: '0', + left: '0', + width: '100%', + height: '100%', + zIndex: '999999', + pointerEvents: 'none' + }); + document.body.appendChild(host); + + const shadow = host.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = ` + .overlay { + position: fixed; inset: 0; + background: rgba(0,0,0,0.55); + display: flex; align-items: center; justify-content: center; + pointer-events: auto; + } + .modal { + background: #fff; color: #0f1419; + width: 420px; max-width: 92vw; + border-radius: 16px; padding: 24px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + box-shadow: 0 25px 50px rgba(0,0,0,0.25); + display: flex; flex-direction: column; gap: 14px; + animation: popIn 0.2s ease-out; + } + @keyframes popIn { from {opacity:0; transform:scale(0.96)} to {opacity:1; transform:scale(1)} } + h3 { margin: 0; font-size: 20px; } + .tweet-box { + background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px; + padding: 12px; font-size: 14px; line-height: 1.4; + max-height: 120px; overflow-y: auto; color: #333; + } + label { font-size: 13px; font-weight: 700; color: #536471; } + select, button { + width: 100%; padding: 10px 12px; border-radius: 8px; + border: 1px solid #cfd9de; font-size: 14px; outline: none; + } + button.primary { + background: #1d9bf0; color: #fff; border: none; + font-weight: 700; cursor: pointer; margin-top: 4px; + } + button.primary:hover { background: #1a8cd8; } + button.primary:disabled { background: #8ecdf7; cursor: default; } + button.ghost { + background: transparent; color: #536471; border: 1px solid #cfd9de; margin-top: 6px; + } + .status { + display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; + } + .status.ok { background: #e8f6fe; border: 1px solid #b4dffc; color: #0f1419; } + .status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; } + .copy-hint { font-size: 12px; color: #536471; margin-top: 6px; text-align: center; } + `; + + const overlay = document.createElement('div'); + overlay.className = 'overlay'; + overlay.addEventListener('click', (e) => { + if (e.target === overlay) removeModal(); + }); + + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.innerHTML = ` +

✍️ Viết Comment AI

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + `; + + overlay.appendChild(modal); + shadow.appendChild(style); + shadow.appendChild(overlay); + + // Điền text tweet + shadow.getElementById('ai-tweet').textContent = tweetText; + + // Events + shadow.getElementById('ai-close').addEventListener('click', removeModal); + + shadow.getElementById('ai-run').addEventListener('click', () => { + const language = shadow.getElementById('ai-language').value; + const tone = shadow.getElementById('ai-tone').value; + const angle = shadow.getElementById('ai-angle').value; + const status = shadow.getElementById('ai-status'); + const btn = shadow.getElementById('ai-run'); + + btn.disabled = true; + status.style.display = 'block'; + status.className = 'status ok'; + status.innerHTML = '⏳ Đang tạo comment...'; + + chrome.runtime.sendMessage({ + action: 'GENERATE_COMMENT', + data: { text: tweetText, tone, angle, language } + }); + }); + } + + // ===== D. HIỂN THỊ KẾT QUẢ / LỖI ===== + function showResult(comment) { + const host = document.getElementById('x-comment-ai-host'); + if (!host) return; + const s = host.shadowRoot; + const status = s.getElementById('ai-status'); + const btn = s.getElementById('ai-run'); + const hint = s.getElementById('ai-hint'); + + status.style.display = 'block'; + status.className = 'status ok'; + status.textContent = comment; + + hint.style.display = 'block'; + if (btn) btn.disabled = false; + } + + function showError(msg) { + const host = document.getElementById('x-comment-ai-host'); + if (!host) return; + const s = host.shadowRoot; + const status = s.getElementById('ai-status'); + const btn = s.getElementById('ai-run'); + + status.style.display = 'block'; + status.className = 'status err'; + status.textContent = 'Lỗi: ' + msg; + + if (btn) btn.disabled = false; + } + + function removeModal() { + const el = document.getElementById('x-comment-ai-host'); + if (el) el.remove(); + } +})(); \ No newline at end of file diff --git a/chrom-ext/icons/icon.webp b/chrom-ext/icons/icon.webp new file mode 100644 index 0000000..d996ee3 Binary files /dev/null and b/chrom-ext/icons/icon.webp differ diff --git a/chrom-ext/manifest.json b/chrom-ext/manifest.json new file mode 100644 index 0000000..14fcb30 --- /dev/null +++ b/chrom-ext/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 3, + "name": "X Comment AI", + "version": "1.0.0", + "description": "Tạo comment cho X.com bằng AI của bạn", + "permissions": [ + "contextMenus", + "storage" + ], + "host_permissions": [ + "https://x.com/*", + "https://twitter.com/*", + "https://api.yoursite.com/*" + ], + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [ + "https://x.com/*", + "https://twitter.com/*" + ], + "js": [ + "content.js" + ], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icons/icon.webp", + "48": "icons/icon.webp", + "128": "icons/icon.webp" + } +} \ No newline at end of file diff --git a/src/sqs-module/sqs.poster.worker.ts b/src/sqs-module/sqs.poster.worker.ts index 8c5a1ee..f77b859 100644 --- a/src/sqs-module/sqs.poster.worker.ts +++ b/src/sqs-module/sqs.poster.worker.ts @@ -103,7 +103,10 @@ export class SqsPosterWorker { // await postToX(data.content); // giả lập delay - await this.sleep(rand(7, 10) * 1000); //nghỉ 10s + const sleepS = rand(12, 21) * 1000; + this.logger.log(`Nghỉ ${sleepS} second`); + + await this.sleep(sleepS); } private sleep(ms: number) { diff --git a/src/x-poster/x-browser.service.ts b/src/x-poster/x-browser.service.ts index c09fb37..c8ff167 100644 --- a/src/x-poster/x-browser.service.ts +++ b/src/x-poster/x-browser.service.ts @@ -358,7 +358,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { // await page.mouse.click(btnBox?.x + btnBox.width / 2, btnBox.y + btnBox.height / 2); await page.keyboard.press('Control+Enter'); this.logger.debug('Nhấn Control+Enter done ...'); - await page.waitForTimeout(5000); + await page.waitForTimeout(10000); // Chờ request CreateTweet hoàn tất // await page.waitForResponse( @@ -484,7 +484,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { this.logger.debug('❌ Nut click khong duoc, thử dùng bàn phím Control+Enter'); await page.keyboard.press('Control+Enter'); }); - await page.waitForTimeout(rand(4000, 6000)); + await page.waitForTimeout(rand(6000, 10000)); this.logger.debug('✅ Quoted thành công'); } else { @@ -581,8 +581,8 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { } await btn.click(); - this.logger.debug(`nhấn nút gửi ...`) - await page.waitForTimeout(3000); + this.logger.debug(`Đã nhấn nút gửi ...`) + await page.waitForTimeout(10000); this.logger.debug('✅ Reply OK'); return {success: true, error: ''};