Exempt repeat AI comments on same post

This commit is contained in:
2026-04-07 16:54:24 +02:00
parent 52a21ca089
commit b6f8572aae
3 changed files with 155 additions and 32 deletions

View File

@@ -1171,11 +1171,36 @@ function getAIAutoCommentOldestEventSince(profileNumber, sinceIso) {
`).get(normalizedProfileNumber, sinceIso) || null; `).get(normalizedProfileNumber, sinceIso) || null;
} }
function buildAIAutoCommentRateLimitStatus(profileNumber, settings = getAIAutoCommentRateLimitSettings(), now = new Date()) { function sanitizeAIAutoCommentPostKey(value) {
if (typeof value !== 'string') {
return null;
}
const trimmed = truncateString(value.trim(), 1000);
return trimmed || null;
}
function hasAIAutoCommentEventForPost(profileNumber, postKey) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
const normalizedPostKey = sanitizeAIAutoCommentPostKey(postKey);
if (!normalizedProfileNumber || !normalizedPostKey) {
return false;
}
const row = db.prepare(`
SELECT 1
FROM ai_auto_comment_rate_limit_events
WHERE profile_number = ?
AND post_key = ?
LIMIT 1
`).get(normalizedProfileNumber, normalizedPostKey);
return Boolean(row);
}
function buildAIAutoCommentRateLimitStatus(profileNumber, settings = getAIAutoCommentRateLimitSettings(), now = new Date(), options = {}) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber); const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) { if (!normalizedProfileNumber) {
return null; return null;
} }
const normalizedPostKey = sanitizeAIAutoCommentPostKey(options.postKey);
clearExpiredAIAutoCommentProfileCooldown(normalizedProfileNumber, now); clearExpiredAIAutoCommentProfileCooldown(normalizedProfileNumber, now);
@@ -1192,12 +1217,13 @@ function buildAIAutoCommentRateLimitStatus(profileNumber, settings = getAIAutoCo
const lastEvent = getAIAutoCommentLatestEvent(normalizedProfileNumber); const lastEvent = getAIAutoCommentLatestEvent(normalizedProfileNumber);
const profileState = getAIAutoCommentProfileState(normalizedProfileNumber); const profileState = getAIAutoCommentProfileState(normalizedProfileNumber);
const activeHours = getAIAutoCommentActiveHoursState(settings, now); const activeHours = getAIAutoCommentActiveHoursState(settings, now);
const samePostExempt = Boolean(normalizedPostKey && hasAIAutoCommentEventForPost(normalizedProfileNumber, normalizedPostKey));
let blocked = false; let blocked = false;
let blockedReason = null; let blockedReason = null;
let blockedUntil = null; let blockedUntil = null;
if (settings.enabled) { if (settings.enabled && !samePostExempt) {
const blockingCandidates = []; const blockingCandidates = [];
const addBlockingCandidate = (reason, untilIso) => { const addBlockingCandidate = (reason, untilIso) => {
if (!untilIso) { if (!untilIso) {
@@ -1282,6 +1308,7 @@ function buildAIAutoCommentRateLimitStatus(profileNumber, settings = getAIAutoCo
blocked, blocked,
blocked_reason: blockedReason, blocked_reason: blockedReason,
blocked_until: blockedUntil, blocked_until: blockedUntil,
same_post_exempt: samePostExempt,
cooldown_until: profileState?.cooldown_until || null, cooldown_until: profileState?.cooldown_until || null,
cooldown_reason: profileState?.cooldown_reason || null, cooldown_reason: profileState?.cooldown_reason || null,
usage: { usage: {
@@ -1313,15 +1340,18 @@ function listAIAutoCommentRateLimitStatuses(settings = getAIAutoCommentRateLimit
)); ));
} }
function checkAIAutoCommentActionAvailability(profileNumber, settings = getAIAutoCommentRateLimitSettings()) { function checkAIAutoCommentActionAvailability(profileNumber, settings = getAIAutoCommentRateLimitSettings(), postKey = null) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber); const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) { if (!normalizedProfileNumber) {
return { ok: false, status: null }; return { ok: false, status: null };
} }
const normalizedPostKey = sanitizeAIAutoCommentPostKey(postKey);
const check = db.transaction(() => { const check = db.transaction(() => {
purgeOldAIAutoCommentRateLimitEvents(); purgeOldAIAutoCommentRateLimitEvents();
const currentStatus = buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, settings, new Date()); const currentStatus = buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, settings, new Date(), {
postKey: normalizedPostKey
});
if (!currentStatus || currentStatus.blocked) { if (!currentStatus || currentStatus.blocked) {
return { return {
ok: false, ok: false,
@@ -1345,11 +1375,12 @@ function checkAIAutoCommentActionAvailability(profileNumber, settings = getAIAut
return check(); return check();
} }
function recordAIAutoCommentAction(profileNumber, settings = getAIAutoCommentRateLimitSettings(), occurredAt = new Date()) { function recordAIAutoCommentAction(profileNumber, settings = getAIAutoCommentRateLimitSettings(), occurredAt = new Date(), postKey = null) {
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber); const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
if (!normalizedProfileNumber) { if (!normalizedProfileNumber) {
return null; return null;
} }
const normalizedPostKey = sanitizeAIAutoCommentPostKey(postKey);
const eventDate = occurredAt instanceof Date && !Number.isNaN(occurredAt.getTime()) const eventDate = occurredAt instanceof Date && !Number.isNaN(occurredAt.getTime())
? occurredAt ? occurredAt
@@ -1357,12 +1388,19 @@ function recordAIAutoCommentAction(profileNumber, settings = getAIAutoCommentRat
const record = db.transaction(() => { const record = db.transaction(() => {
purgeOldAIAutoCommentRateLimitEvents(eventDate); purgeOldAIAutoCommentRateLimitEvents(eventDate);
if (normalizedPostKey && hasAIAutoCommentEventForPost(normalizedProfileNumber, normalizedPostKey)) {
return buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, settings, eventDate, {
postKey: normalizedPostKey
});
}
db.prepare(` db.prepare(`
INSERT INTO ai_auto_comment_rate_limit_events (profile_number, created_at) INSERT INTO ai_auto_comment_rate_limit_events (profile_number, post_key, created_at)
VALUES (?, ?) VALUES (?, ?, ?)
`).run(normalizedProfileNumber, eventDate.toISOString()); `).run(normalizedProfileNumber, normalizedPostKey, eventDate.toISOString());
return buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, settings, eventDate); return buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, settings, eventDate, {
postKey: normalizedPostKey
});
}); });
return record(); return record();
@@ -2159,6 +2197,7 @@ db.exec(`
CREATE TABLE IF NOT EXISTS ai_auto_comment_rate_limit_events ( CREATE TABLE IF NOT EXISTS ai_auto_comment_rate_limit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_number INTEGER NOT NULL, profile_number INTEGER NOT NULL,
post_key TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );
`); `);
@@ -2655,8 +2694,13 @@ ensureColumn('ai_auto_comment_rate_limit_settings', 'burst_limit', 'burst_limit
ensureColumn('ai_auto_comment_rate_limit_settings', 'cooldown_minutes', 'cooldown_minutes INTEGER NOT NULL DEFAULT 15'); ensureColumn('ai_auto_comment_rate_limit_settings', 'cooldown_minutes', 'cooldown_minutes INTEGER NOT NULL DEFAULT 15');
ensureColumn('ai_auto_comment_rate_limit_settings', 'active_hours_start', 'active_hours_start TEXT'); ensureColumn('ai_auto_comment_rate_limit_settings', 'active_hours_start', 'active_hours_start TEXT');
ensureColumn('ai_auto_comment_rate_limit_settings', 'active_hours_end', 'active_hours_end TEXT'); ensureColumn('ai_auto_comment_rate_limit_settings', 'active_hours_end', 'active_hours_end TEXT');
ensureColumn('ai_auto_comment_rate_limit_events', 'post_key', 'post_key TEXT');
ensureColumn('ai_auto_comment_rate_limit_profile_state', 'cooldown_until', 'cooldown_until DATETIME'); ensureColumn('ai_auto_comment_rate_limit_profile_state', 'cooldown_until', 'cooldown_until DATETIME');
ensureColumn('ai_auto_comment_rate_limit_profile_state', 'cooldown_reason', 'cooldown_reason TEXT'); ensureColumn('ai_auto_comment_rate_limit_profile_state', 'cooldown_reason', 'cooldown_reason TEXT');
db.exec(`
CREATE INDEX IF NOT EXISTS idx_ai_auto_comment_rate_limit_events_profile_post
ON ai_auto_comment_rate_limit_events(profile_number, post_key);
`);
ensureColumn('ai_credentials', 'is_active', 'is_active INTEGER DEFAULT 1'); ensureColumn('ai_credentials', 'is_active', 'is_active INTEGER DEFAULT 1');
ensureColumn('ai_credentials', 'priority', 'priority INTEGER DEFAULT 0'); ensureColumn('ai_credentials', 'priority', 'priority INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'base_url', 'base_url TEXT'); ensureColumn('ai_credentials', 'base_url', 'base_url TEXT');
@@ -7294,6 +7338,26 @@ app.get('/api/ai-settings', (req, res) => {
} }
}); });
app.get('/api/ai/auto-comment-rate-limit-status', (req, res) => {
try {
const profileNumber = sanitizeProfileNumber(req.query.profileNumber);
if (!profileNumber) {
return res.status(400).json({ error: 'profileNumber is required' });
}
const status = buildAIAutoCommentRateLimitStatus(
profileNumber,
getAIAutoCommentRateLimitSettings(),
new Date(),
{ postKey: req.query.postKey }
);
res.json({ status });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.put('/api/ai-settings', (req, res) => { app.put('/api/ai-settings', (req, res) => {
try { try {
const { active_credential_id, prompt_prefix, enabled, rate_limit_settings } = req.body; const { active_credential_id, prompt_prefix, enabled, rate_limit_settings } = req.body;
@@ -7933,10 +7997,12 @@ app.post('/api/ai/generate-comment', async (req, res) => {
}; };
let autoCommentRateLimitStatus = null; let autoCommentRateLimitStatus = null;
let autoCommentRateLimitGlobalStatus = null;
try { try {
const { postText, profileNumber, preferredCredentialId } = requestBody; const { postText, profileNumber, preferredCredentialId, postKey } = requestBody;
const normalizedProfileNumber = sanitizeProfileNumber(profileNumber); const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
const normalizedPostKey = sanitizeAIAutoCommentPostKey(postKey);
if (!postText) { if (!postText) {
return respondWithTrackedError(400, 'postText is required'); return respondWithTrackedError(400, 'postText is required');
@@ -7975,8 +8041,16 @@ app.post('/api/ai/generate-comment', async (req, res) => {
} }
const limitCheckStartedMs = timingStart(); const limitCheckStartedMs = timingStart();
const availability = checkAIAutoCommentActionAvailability(normalizedProfileNumber, autoCommentRateLimitSettings); const availability = checkAIAutoCommentActionAvailability(
normalizedProfileNumber,
autoCommentRateLimitSettings,
normalizedPostKey
);
autoCommentRateLimitStatus = availability.status || null; autoCommentRateLimitStatus = availability.status || null;
autoCommentRateLimitGlobalStatus = buildAIAutoCommentRateLimitStatus(
normalizedProfileNumber,
autoCommentRateLimitSettings
);
if (!availability.ok && autoCommentRateLimitStatus && autoCommentRateLimitStatus.blocked) { if (!availability.ok && autoCommentRateLimitStatus && autoCommentRateLimitStatus.blocked) {
timingEnd('profileLimitCheckMs', limitCheckStartedMs); timingEnd('profileLimitCheckMs', limitCheckStartedMs);
const blockedUntilText = autoCommentRateLimitStatus.blocked_until const blockedUntilText = autoCommentRateLimitStatus.blocked_until
@@ -8079,7 +8153,12 @@ app.post('/api/ai/generate-comment', async (req, res) => {
autoCommentRateLimitStatus = recordAIAutoCommentAction( autoCommentRateLimitStatus = recordAIAutoCommentAction(
normalizedProfileNumber, normalizedProfileNumber,
autoCommentRateLimitSettings, autoCommentRateLimitSettings,
new Date() new Date(),
normalizedPostKey
);
autoCommentRateLimitGlobalStatus = buildAIAutoCommentRateLimitStatus(
normalizedProfileNumber,
autoCommentRateLimitSettings
); );
} }
@@ -8097,7 +8176,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
usedCredential: credential.name, usedCredential: credential.name,
usedCredentialId: credential.id, usedCredentialId: credential.id,
attempts: attemptDetails, attempts: attemptDetails,
autoCommentRateLimit: autoCommentRateLimitStatus autoCommentRateLimit: autoCommentRateLimitStatus,
autoCommentRateLimitGlobal: autoCommentRateLimitGlobalStatus
}, },
totalDurationMs: backendTimings.totalMs totalDurationMs: backendTimings.totalMs
}); });
@@ -8112,6 +8192,7 @@ app.post('/api/ai/generate-comment', async (req, res) => {
attempts: attemptDetails, attempts: attemptDetails,
rateLimitInfo: rateInfo || null, rateLimitInfo: rateInfo || null,
autoCommentRateLimitStatus, autoCommentRateLimitStatus,
autoCommentRateLimitGlobalStatus,
traceId, traceId,
flowId, flowId,
timings: { timings: {
@@ -8129,6 +8210,7 @@ app.post('/api/ai/generate-comment', async (req, res) => {
if (cooldownDecision) { if (cooldownDecision) {
setAIAutoCommentProfileCooldown(normalizedProfileNumber, cooldownDecision); setAIAutoCommentProfileCooldown(normalizedProfileNumber, cooldownDecision);
autoCommentRateLimitStatus = buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, autoCommentRateLimitSettings); autoCommentRateLimitStatus = buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, autoCommentRateLimitSettings);
autoCommentRateLimitGlobalStatus = buildAIAutoCommentRateLimitStatus(normalizedProfileNumber, autoCommentRateLimitSettings);
} }
} }
credentialTimingDetails.push({ credentialTimingDetails.push({
@@ -8170,7 +8252,8 @@ app.post('/api/ai/generate-comment', async (req, res) => {
attempts: error && error.attempts ? error.attempts : null, attempts: error && error.attempts ? error.attempts : null,
responseMeta: { responseMeta: {
attempts: error && error.attempts ? error.attempts : null, attempts: error && error.attempts ? error.attempts : null,
autoCommentRateLimit: autoCommentRateLimitStatus autoCommentRateLimit: autoCommentRateLimitStatus,
autoCommentRateLimitGlobal: autoCommentRateLimitGlobalStatus
} }
} }
); );

View File

@@ -1,7 +1,7 @@
// Facebook Post Tracker Extension // Facebook Post Tracker Extension
// Uses API_BASE_URL from config.js // Uses API_BASE_URL from config.js
const EXTENSION_VERSION = '1.2.2'; const EXTENSION_VERSION = '1.2.3';
const PROCESSED_ATTR = 'data-fb-tracker-processed'; const PROCESSED_ATTR = 'data-fb-tracker-processed';
const PENDING_ATTR = 'data-fb-tracker-pending'; const PENDING_ATTR = 'data-fb-tracker-pending';
const DIALOG_ROOT_SELECTOR = '[role="dialog"], [data-pagelet*="Modal"], [data-pagelet="StoriesRecentStoriesFeedSection"]'; const DIALOG_ROOT_SELECTOR = '[role="dialog"], [data-pagelet*="Modal"], [data-pagelet="StoriesRecentStoriesFeedSection"]';
@@ -6509,7 +6509,8 @@ async function generateAIComment(postText, profileNumber, options = {}) {
maxAttempts = 3, maxAttempts = 3,
flowId = null, flowId = null,
source = 'extension-ai-button', source = 'extension-ai-button',
returnMeta = false returnMeta = false,
postKey = null
} = options; } = options;
const normalizedFlowId = typeof flowId === 'string' && flowId.trim() const normalizedFlowId = typeof flowId === 'string' && flowId.trim()
? flowId.trim() ? flowId.trim()
@@ -6524,6 +6525,9 @@ async function generateAIComment(postText, profileNumber, options = {}) {
if (typeof preferredCredentialId === 'number' && !Number.isNaN(preferredCredentialId)) { if (typeof preferredCredentialId === 'number' && !Number.isNaN(preferredCredentialId)) {
basePayload.preferredCredentialId = preferredCredentialId; basePayload.preferredCredentialId = preferredCredentialId;
} }
if (typeof postKey === 'string' && postKey.trim()) {
basePayload.postKey = postKey.trim();
}
const requestAttempts = []; const requestAttempts = [];
let lastError = null; let lastError = null;
@@ -6605,7 +6609,8 @@ async function generateAIComment(postText, profileNumber, options = {}) {
flowId: effectiveFlowId, flowId: effectiveFlowId,
requestAttempts, requestAttempts,
backendTimings: data.timings && data.timings.backend ? data.timings.backend : null, backendTimings: data.timings && data.timings.backend ? data.timings.backend : null,
autoCommentRateLimitStatus: data.autoCommentRateLimitStatus || null autoCommentRateLimitStatus: data.autoCommentRateLimitStatus || null,
autoCommentRateLimitGlobalStatus: data.autoCommentRateLimitGlobalStatus || null
}; };
return returnMeta ? result : sanitizedComment; return returnMeta ? result : sanitizedComment;
} }
@@ -6664,6 +6669,27 @@ async function generateAIComment(postText, profileNumber, options = {}) {
throw finalError; throw finalError;
} }
async function fetchAIAutoCommentRateLimitStatus(profileNumber, options = {}) {
const normalizedProfile = parseInt(profileNumber, 10);
if (!normalizedProfile) {
return null;
}
const params = new URLSearchParams();
params.set('profileNumber', String(normalizedProfile));
if (typeof options.postKey === 'string' && options.postKey.trim()) {
params.set('postKey', options.postKey.trim());
}
const response = await backendFetch(`${API_URL}/ai/auto-comment-rate-limit-status?${params.toString()}`);
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.error || 'Failed to fetch AI rate limit status');
}
return data && data.status ? data.status : null;
}
async function handleSelectionAIRequest(selectionText, sendResponse) { async function handleSelectionAIRequest(selectionText, sendResponse) {
try { try {
const normalizedSelection = normalizeSelectionText(selectionText); const normalizedSelection = normalizeSelectionText(selectionText);
@@ -6869,6 +6895,20 @@ async function addAICommentButton(container, postElement) {
: 'Generiere automatisch einen passenden Kommentar'; : 'Generiere automatisch einen passenden Kommentar';
}; };
const getCurrentPostKey = () => {
const raw = encodedPostUrl || (container && container.getAttribute('data-post-url'));
if (!raw) {
return null;
}
try {
const decoded = decodeURIComponent(raw);
return normalizeFacebookPostUrl(decoded) || decoded;
} catch (error) {
console.warn('[FB Tracker] Konnte Post-Key nicht lesen:', error);
return null;
}
};
const buildBlockedButtonTitle = (status) => { const buildBlockedButtonTitle = (status) => {
if (!status || !status.blocked) { if (!status || !status.blocked) {
return getDefaultButtonTitle(); return getDefaultButtonTitle();
@@ -7024,8 +7064,10 @@ async function addAICommentButton(container, postElement) {
applyAvailabilityState(null); applyAvailabilityState(null);
return null; return null;
} }
const settings = await fetchAISettings(forceRefresh); const status = await fetchAIAutoCommentRateLimitStatus(profileNumber, {
const status = getAIAutoCommentRateLimitStatusFromSettings(settings, profileNumber); forceRefresh,
postKey: getCurrentPostKey()
});
applyAvailabilityState(status); applyAvailabilityState(status);
return status; return status;
} catch (error) { } catch (error) {
@@ -7040,7 +7082,7 @@ async function addAICommentButton(container, postElement) {
return await rateLimitRefreshPromise; return await rateLimitRefreshPromise;
}; };
const handleSharedAISettingsUpdate = async (settings) => { const handleSharedAISettingsUpdate = async () => {
if (!wrapper.isConnected) { if (!wrapper.isConnected) {
clearBlockedCountdown(); clearBlockedCountdown();
aiAvailabilitySubscribers.delete(handleSharedAISettingsUpdate); aiAvailabilitySubscribers.delete(handleSharedAISettingsUpdate);
@@ -7048,13 +7090,7 @@ async function addAICommentButton(container, postElement) {
} }
try { try {
const profileNumber = await fetchBackendProfileNumber(); await refreshAvailabilityState(true);
if (!profileNumber) {
applyAvailabilityState(null);
return;
}
const status = getAIAutoCommentRateLimitStatusFromSettings(settings, profileNumber);
applyAvailabilityState(status);
} catch (error) { } catch (error) {
console.warn('[FB Tracker] Failed to apply shared AI settings update:', error); console.warn('[FB Tracker] Failed to apply shared AI settings update:', error);
} }
@@ -7841,7 +7877,8 @@ async function addAICommentButton(container, postElement) {
preferredCredentialId, preferredCredentialId,
flowId: flowTrace.flowId, flowId: flowTrace.flowId,
source: flowTrace.source, source: flowTrace.source,
returnMeta: true returnMeta: true,
postKey: getCurrentPostKey()
}); });
endPhase('aiRequestMs', { endPhase('aiRequestMs', {
traceId: aiResult.traceId || null, traceId: aiResult.traceId || null,
@@ -7849,9 +7886,12 @@ async function addAICommentButton(container, postElement) {
}); });
mergeTraceInfo(aiResult); mergeTraceInfo(aiResult);
if (aiResult.autoCommentRateLimitStatus) { if (aiResult.autoCommentRateLimitStatus) {
const nextSettings = upsertAIAutoCommentRateLimitStatus(aiSettingsCache.data, aiResult.autoCommentRateLimitStatus);
applyAISettingsSnapshot(nextSettings, { timestamp: Date.now(), broadcast: true });
applyAvailabilityState(aiResult.autoCommentRateLimitStatus); applyAvailabilityState(aiResult.autoCommentRateLimitStatus);
const globalRateLimitStatus = aiResult.autoCommentRateLimitGlobalStatus || null;
if (globalRateLimitStatus) {
const nextSettings = upsertAIAutoCommentRateLimitStatus(aiSettingsCache.data, globalRateLimitStatus);
applyAISettingsSnapshot(nextSettings, { timestamp: Date.now(), broadcast: true });
}
} else { } else {
void refreshAvailabilityState(true, profileNumber); void refreshAvailabilityState(true, profileNumber);
} }
@@ -7969,7 +8009,7 @@ async function addAICommentButton(container, postElement) {
if (error && error.status === 429) { if (error && error.status === 429) {
const syncedStatus = await refreshAvailabilityState(true); const syncedStatus = await refreshAvailabilityState(true);
if (syncedStatus) { if (syncedStatus && !syncedStatus.same_post_exempt) {
const nextSettings = upsertAIAutoCommentRateLimitStatus(aiSettingsCache.data, syncedStatus); const nextSettings = upsertAIAutoCommentRateLimitStatus(aiSettingsCache.data, syncedStatus);
applyAISettingsSnapshot(nextSettings, { timestamp: Date.now(), broadcast: true }); applyAISettingsSnapshot(nextSettings, { timestamp: Date.now(), broadcast: true });
} }

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Facebook Post Tracker", "name": "Facebook Post Tracker",
"version": "1.2.2", "version": "1.2.3",
"description": "Track Facebook posts across multiple profiles", "description": "Track Facebook posts across multiple profiles",
"permissions": [ "permissions": [
"storage", "storage",