api usage cooldown

This commit is contained in:
MDeeApp
2025-10-05 20:12:13 +02:00
parent 1b0389b63d
commit 3bcc7b08b4
4 changed files with 1266 additions and 141 deletions

View File

@@ -17,6 +17,54 @@ const FEED_HOME_PATHS = ['/', '/home.php'];
const sessionSearchRecordedUrls = new Set();
const sessionSearchInfoCache = new Map();
const trackerElementsByPost = new WeakMap();
function getTrackerElementForPost(postElement) {
if (!postElement) {
return null;
}
const tracker = trackerElementsByPost.get(postElement);
if (tracker && tracker.isConnected) {
return tracker;
}
if (tracker) {
trackerElementsByPost.delete(postElement);
}
return null;
}
function setTrackerElementForPost(postElement, trackerElement) {
if (!postElement) {
return;
}
if (trackerElement && trackerElement.isConnected) {
trackerElementsByPost.set(postElement, trackerElement);
} else {
trackerElementsByPost.delete(postElement);
}
}
function clearTrackerElementForPost(postElement, trackerElement = null) {
if (!postElement) {
return;
}
if (!trackerElementsByPost.has(postElement)) {
return;
}
const current = trackerElementsByPost.get(postElement);
if (trackerElement && current && current !== trackerElement) {
return;
}
trackerElementsByPost.delete(postElement);
}
const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache
const aiCredentialCache = {
data: null,
@@ -1600,9 +1648,22 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
// Normalize to top-level post container if nested element passed
postElement = ensurePrimaryPostElement(postElement);
const existingUI = postElement.querySelector('.fb-tracker-ui');
let existingUI = getTrackerElementForPost(postElement);
if (!existingUI) {
existingUI = postElement.querySelector('.fb-tracker-ui');
if (existingUI && existingUI.isConnected) {
setTrackerElementForPost(postElement, existingUI);
}
}
if (existingUI && !existingUI.isConnected) {
clearTrackerElementForPost(postElement, existingUI);
existingUI = null;
}
if (existingUI) {
console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element');
postElement.setAttribute(PROCESSED_ATTR, '1');
return;
}
@@ -1617,6 +1678,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
if (!postUrlData.url) {
console.log('[FB Tracker] Post #' + postNum + ' - No URL found:', postElement);
postElement.removeAttribute(PROCESSED_ATTR);
clearTrackerElementForPost(postElement);
return;
}
@@ -1628,6 +1690,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
if (existingEntry && existingEntry.element && existingEntry.element !== postElement) {
if (document.body.contains(existingEntry.element)) {
existingEntry.element.removeAttribute(PROCESSED_ATTR);
clearTrackerElementForPost(existingEntry.element);
} else {
processedPostUrls.delete(encodedUrl);
}
@@ -2149,6 +2212,7 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
: null,
hidden: false
});
setTrackerElementForPost(postElement, container);
}
// Monitor if the UI gets removed and re-insert it
@@ -2163,6 +2227,9 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
} else if (postElement.parentElement) {
postElement.parentElement.appendChild(container);
}
if (container.isConnected) {
setTrackerElementForPost(postElement, container);
}
}
});
@@ -2271,10 +2338,29 @@ function findPosts() {
continue;
}
const existingTracker = container.querySelector('.fb-tracker-ui');
let existingTracker = getTrackerElementForPost(container);
if (!existingTracker) {
existingTracker = container.querySelector('.fb-tracker-ui');
if (existingTracker && existingTracker.isConnected) {
setTrackerElementForPost(container, existingTracker);
}
}
if (existingTracker && !existingTracker.isConnected) {
clearTrackerElementForPost(container, existingTracker);
existingTracker = null;
}
const alreadyProcessed = container.getAttribute(PROCESSED_ATTR) === '1';
const trackerDialogRoot = existingTracker ? existingTracker.closest(DIALOG_ROOT_SELECTOR) : null;
const trackerInSameDialog = Boolean(existingTracker && existingTracker.isConnected && trackerDialogRoot === dialogRoot);
const trackerInSameDialog = Boolean(
existingTracker
&& existingTracker.isConnected
&& (
trackerDialogRoot === dialogRoot
|| (dialogRoot && dialogRoot.contains(existingTracker))
)
);
if (isInDialog) {
if (trackerInSameDialog) {
@@ -2284,6 +2370,7 @@ function findPosts() {
if (existingTracker && existingTracker.isConnected) {
existingTracker.remove();
clearTrackerElementForPost(container, existingTracker);
}
if (alreadyProcessed) {
@@ -2477,6 +2564,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const existingUI = postContainer.querySelector('.fb-tracker-ui');
if (existingUI) {
existingUI.remove();
clearTrackerElementForPost(postContainer, existingUI);
console.log('[FB Tracker] Removed existing UI');
}
@@ -2805,12 +2893,16 @@ function findAndClickCommentButton(postElement) {
/**
* Find comment input field on current page
*/
function findCommentInput(postElement) {
if (!postElement) {
function findCommentInput(postElement, options = {}) {
const {
preferredRoot = null,
includeParents = true
} = options;
if (!postElement && !preferredRoot) {
return null;
}
// Try multiple selectors for comment input
const selectors = [
'div[contenteditable="true"][role="textbox"]',
'div[aria-label*="Kommentar"][contenteditable="true"]',
@@ -2818,24 +2910,48 @@ function findCommentInput(postElement) {
'div[aria-label*="Write a comment"][contenteditable="true"]'
];
for (const selector of selectors) {
const input = postElement.querySelector(selector);
const searchInRoot = (root) => {
if (!root) {
return null;
}
for (const selector of selectors) {
const input = root.querySelector(selector);
if (input && isElementVisible(input)) {
return input;
}
}
return null;
};
const roots = [];
if (postElement) {
roots.push(postElement);
}
if (preferredRoot && preferredRoot.isConnected && !roots.includes(preferredRoot)) {
roots.push(preferredRoot);
}
for (const root of roots) {
const input = searchInRoot(root);
if (input) {
return input;
}
}
// Search in parent containers
let parent = postElement;
for (let i = 0; i < 3; i++) {
parent = parent.parentElement;
if (!parent) break;
for (const selector of selectors) {
const input = parent.querySelector(selector);
if (includeParents && postElement) {
let parent = postElement.parentElement;
for (let i = 0; i < 3 && parent; i++) {
if (preferredRoot && !preferredRoot.contains(parent)) {
parent = parent.parentElement;
continue;
}
const input = searchInRoot(parent);
if (input) {
return input;
}
parent = parent.parentElement;
}
}
@@ -2879,11 +2995,15 @@ async function waitForCommentInput(postElement, options = {}) {
encodedPostUrl = null,
timeout = 6000,
interval = 200,
context = null
context = null,
preferredRoot: rawPreferredRoot = null
} = options;
const deadline = Date.now() + Math.max(timeout, 0);
let attempts = 0;
const preferredRoot = rawPreferredRoot && rawPreferredRoot.isConnected
? rawPreferredRoot
: null;
const findByEncodedUrl = () => {
if (context && context.cancelled) {
@@ -2900,15 +3020,19 @@ async function waitForCommentInput(postElement, options = {}) {
continue;
}
if (preferredRoot && !preferredRoot.contains(tracker)) {
continue;
}
const trackerContainer = tracker.closest('div[aria-posinset], article[role="article"], article');
if (trackerContainer) {
const input = findCommentInput(trackerContainer);
const input = findCommentInput(trackerContainer, { preferredRoot });
if (isElementVisible(input)) {
return input;
}
}
const dialogRoot = tracker.closest(DIALOG_ROOT_SELECTOR);
const dialogRoot = preferredRoot || tracker.closest(DIALOG_ROOT_SELECTOR);
if (dialogRoot) {
const dialogInput = dialogRoot.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
@@ -2927,7 +3051,7 @@ async function waitForCommentInput(postElement, options = {}) {
attempts++;
let input = findCommentInput(postElement);
let input = findCommentInput(postElement, { preferredRoot });
if (isElementVisible(input)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (post context)');
@@ -2943,7 +3067,8 @@ async function waitForCommentInput(postElement, options = {}) {
return input;
}
const dialogRootFromPost = postElement ? postElement.closest(DIALOG_ROOT_SELECTOR) : null;
const dialogRootFromPost = preferredRoot
|| (postElement ? postElement.closest(DIALOG_ROOT_SELECTOR) : null);
if (dialogRootFromPost) {
const dialogInput = dialogRootFromPost.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
@@ -2954,18 +3079,32 @@ async function waitForCommentInput(postElement, options = {}) {
}
}
const fallbackDialog = document.querySelector(DIALOG_ROOT_SELECTOR);
if (fallbackDialog && fallbackDialog !== dialogRootFromPost) {
const dialogInput = fallbackDialog.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (fallback dialog)');
if (!preferredRoot) {
const fallbackDialog = document.querySelector(DIALOG_ROOT_SELECTOR);
if (fallbackDialog && fallbackDialog !== dialogRootFromPost) {
const dialogInput = fallbackDialog.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (fallback dialog)');
}
return dialogInput;
}
return dialogInput;
}
}
const globalInputs = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"]')).filter(isElementVisible);
if (preferredRoot) {
const scopedInputs = globalInputs.filter(input => preferredRoot.contains(input));
if (scopedInputs.length > 0) {
const lastInput = scopedInputs[scopedInputs.length - 1];
if (lastInput) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (preferred root fallback)');
}
return lastInput;
}
}
}
if (globalInputs.length > 0) {
const lastInput = globalInputs[globalInputs.length - 1];
if (lastInput) {
@@ -3235,7 +3374,11 @@ async function addAICommentButton(container, postElement) {
});
button.addEventListener('pointerdown', () => {
cacheSelectionForPost(postElement);
const contextElement = container.closest('div[aria-posinset], article[role="article"], article');
const normalized = contextElement ? ensurePrimaryPostElement(contextElement) : null;
const fallbackNormalized = postElement ? ensurePrimaryPostElement(postElement) : null;
const target = normalized || fallbackNormalized || contextElement || postElement || container;
cacheSelectionForPost(target);
});
button.dataset.aiState = 'idle';
@@ -3469,9 +3612,38 @@ async function addAICommentButton(container, postElement) {
button.textContent = '⏳ Generiere...';
try {
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article');
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
const selectionKeys = [];
if (postContext) {
selectionKeys.push(postContext);
}
if (postElement && postElement !== postContext) {
selectionKeys.push(postElement);
}
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
selectionKeys.push(contextCandidate);
}
const resolveRecentSelection = () => {
for (const key of selectionKeys) {
if (!key) {
continue;
}
const entry = postSelectionCache.get(key);
if (entry && Date.now() - entry.timestamp < LAST_SELECTION_MAX_AGE) {
return entry;
}
}
return null;
};
let postText = '';
const cachedSelection = postSelectionCache.get(postElement);
if (cachedSelection && Date.now() - cachedSelection.timestamp < LAST_SELECTION_MAX_AGE) {
const cachedSelection = resolveRecentSelection();
if (cachedSelection) {
console.log('[FB Tracker] Using cached selection text');
postText = cachedSelection.text;
}
@@ -3479,22 +3651,25 @@ async function addAICommentButton(container, postElement) {
throwIfCancelled();
if (!postText) {
postText = getSelectedTextFromPost(postElement);
if (postText) {
console.log('[FB Tracker] Using active selection text');
const selectionSource = postContext || postElement;
if (selectionSource) {
postText = getSelectedTextFromPost(selectionSource);
if (postText) {
console.log('[FB Tracker] Using active selection text');
}
}
}
if (!postText) {
const latestCached = postSelectionCache.get(postElement);
if (latestCached && Date.now() - latestCached.timestamp < LAST_SELECTION_MAX_AGE) {
const latestCached = resolveRecentSelection();
if (latestCached) {
console.log('[FB Tracker] Using latest cached selection after check');
postText = latestCached.text;
}
}
if (!postText) {
postText = extractPostText(postElement);
postText = extractPostText(postContext);
if (postText) {
console.log('[FB Tracker] Fallback to DOM extraction');
}
@@ -3504,7 +3679,11 @@ async function addAICommentButton(container, postElement) {
throw new Error('Konnte Post-Text nicht extrahieren');
}
postSelectionCache.delete(postElement);
selectionKeys.forEach((key) => {
if (key) {
postSelectionCache.delete(key);
}
});
throwIfCancelled();
@@ -3523,32 +3702,45 @@ async function addAICommentButton(container, postElement) {
console.log('[FB Tracker] Generated comment:', comment);
let commentInput = findCommentInput(postElement);
const dialogRoot = container.closest(DIALOG_ROOT_SELECTOR)
|| (postContext && postContext.closest(DIALOG_ROOT_SELECTOR));
let commentInput = findCommentInput(postContext, { preferredRoot: dialogRoot });
let waitedForInput = false;
if (!commentInput) {
console.log('[FB Tracker] Comment input not found, trying to click comment button');
const buttonClicked = findAndClickCommentButton(postElement);
let buttonClicked = findAndClickCommentButton(postContext);
if (!buttonClicked && dialogRoot) {
const dialogCommentButton = dialogRoot.querySelector('[data-ad-rendering-role="comment_button"], [aria-label*="Kommentieren"], [aria-label*="Comment"]');
if (dialogCommentButton && isElementVisible(dialogCommentButton)) {
dialogCommentButton.click();
buttonClicked = true;
}
}
updateProcessingText(buttonClicked ? '⏳ Öffne Kommentare...' : '⏳ Suche Kommentarfeld...');
waitedForInput = true;
commentInput = await waitForCommentInput(postElement, {
commentInput = await waitForCommentInput(postContext, {
encodedPostUrl,
timeout: buttonClicked ? 8000 : 5000,
interval: 250,
context: aiContext
context: aiContext,
preferredRoot: dialogRoot
});
}
if (!commentInput && !waitedForInput) {
updateProcessingText('⏳ Suche Kommentarfeld...');
waitedForInput = true;
commentInput = await waitForCommentInput(postElement, {
commentInput = await waitForCommentInput(postContext, {
encodedPostUrl,
timeout: 4000,
interval: 200,
context: aiContext
context: aiContext,
preferredRoot: dialogRoot
});
}