aktueller stand

This commit is contained in:
2025-12-29 19:45:08 +01:00
parent fde5ab91c8
commit 677eac2632
6 changed files with 1888 additions and 272 deletions

View File

@@ -42,6 +42,32 @@ function isOnReelsPage() {
}
}
function maybeRedirectPageReelsToMain() {
try {
const { location } = window;
const pathname = location && location.pathname;
if (typeof pathname !== 'string') {
return false;
}
const match = pathname.match(/^\/([^/]+)\/reels\/?$/i);
if (!match) {
return false;
}
const pageSlug = match[1];
if (!pageSlug) {
return false;
}
const targetUrl = `${location.origin}/${pageSlug}/`;
if (location.href === targetUrl) {
return false;
}
location.replace(targetUrl);
return true;
} catch (error) {
return false;
}
}
let debugLoggingEnabled = false;
const originalConsoleLog = console.log.bind(console);
@@ -705,6 +731,140 @@ function expandPhotoUrlHostVariants(url) {
}
}
async function fetchPostByUrl(url) {
const normalizedUrl = normalizeFacebookPostUrl(url);
if (!normalizedUrl) {
return null;
}
const response = await backendFetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(normalizedUrl)}`);
if (!response.ok) {
return null;
}
const data = await response.json();
return data && data.id ? data : null;
}
async function fetchPostById(postId) {
if (!postId) {
return null;
}
try {
const response = await backendFetch(`${API_URL}/posts`);
if (!response.ok) {
return null;
}
const posts = await response.json();
if (!Array.isArray(posts)) {
return null;
}
return posts.find(post => post && post.id === postId) || null;
} catch (error) {
return null;
}
}
async function buildSimilarityPayload(postElement) {
let postText = null;
try {
postText = extractPostText(postElement) || null;
} catch (error) {
console.debug('[FB Tracker] Failed to extract post text for similarity:', error);
}
const imageInfo = await getFirstPostImageInfo(postElement);
return {
postText,
firstImageHash: imageInfo.hash,
firstImageUrl: imageInfo.url
};
}
async function findSimilarPost({ url, postText, firstImageHash }) {
if (!url) {
return null;
}
if (!postText && !firstImageHash) {
return null;
}
try {
const payload = {
url,
post_text: postText || null,
first_image_hash: firstImageHash || null
};
const response = await backendFetch(`${API_URL}/posts/similar`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
return null;
}
const data = await response.json();
return data && data.match ? data : null;
} catch (error) {
console.warn('[FB Tracker] Similarity check failed:', error);
return null;
}
}
function shortenInline(text, maxLength = 64) {
if (!text) {
return '';
}
if (text.length <= maxLength) {
return text;
}
return `${text.slice(0, maxLength - 3)}...`;
}
function formatSimilarityLabel(similarity) {
if (!similarity || !similarity.match) {
return '';
}
const match = similarity.match;
const base = match.title || match.created_by_name || match.url || 'Beitrag';
const details = [];
if (similarity.similarity && typeof similarity.similarity.text === 'number') {
details.push(`Text ${Math.round(similarity.similarity.text * 100)}%`);
}
if (similarity.similarity && typeof similarity.similarity.image_distance === 'number') {
details.push(`Bild Δ${similarity.similarity.image_distance}`);
}
const detailText = details.length ? ` (${details.join(', ')})` : '';
return `Ähnlich zu: ${shortenInline(base, 64)}${detailText}`;
}
async function attachUrlToExistingPost(postId, urls, payload = {}) {
if (!postId) {
return false;
}
try {
const body = {
urls: Array.isArray(urls) ? urls : [],
skip_content_key_check: true
};
if (payload.firstImageHash) {
body.first_image_hash = payload.firstImageHash;
}
if (payload.firstImageUrl) {
body.first_image_url = payload.firstImageUrl;
}
const response = await backendFetch(`${API_URL}/posts/${postId}/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
return response.ok;
} catch (error) {
console.warn('[FB Tracker] Failed to attach URL to existing post:', error);
return false;
}
}
// Check if post is already tracked (checks all URL candidates to avoid duplicates)
async function checkPostStatus(postUrl, allUrlCandidates = []) {
try {
@@ -880,15 +1040,20 @@ async function persistAlternatePostUrls(postId, urls = []) {
}
// Add post to tracking
async function markPostChecked(postId, profileNumber) {
async function markPostChecked(postId, profileNumber, options = {}) {
try {
const ignoreOrder = options && options.ignoreOrder === true;
const returnError = options && options.returnError === true;
console.log('[FB Tracker] Marking post as checked:', postId, 'Profile:', profileNumber);
const response = await backendFetch(`${API_URL}/posts/${postId}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ profile_number: profileNumber })
body: JSON.stringify({
profile_number: profileNumber,
ignore_order: ignoreOrder
})
});
if (response.ok) {
@@ -898,15 +1063,21 @@ async function markPostChecked(postId, profileNumber) {
}
if (response.status === 409) {
console.log('[FB Tracker] Post already checked by this profile');
return null;
const payload = await response.json().catch(() => ({}));
const message = payload && payload.error ? payload.error : 'Beitrag kann aktuell nicht bestätigt werden.';
console.log('[FB Tracker] Post check blocked:', message);
return returnError ? { error: message, status: response.status } : null;
}
console.error('[FB Tracker] Failed to mark post as checked:', response.status);
return null;
return returnError
? { error: 'Beitrag konnte nicht bestätigt werden.', status: response.status }
: null;
} catch (error) {
console.error('[FB Tracker] Error marking post as checked:', error);
return null;
return (options && options.returnError)
? { error: 'Beitrag konnte nicht bestätigt werden.', status: 0 }
: null;
}
}
@@ -920,7 +1091,9 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
}
let postText = null;
if (options && options.postElement) {
if (options && typeof options.postText === 'string') {
postText = options.postText;
} else if (options && options.postElement) {
try {
postText = extractPostText(options.postElement) || null;
} catch (error) {
@@ -967,6 +1140,13 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
payload.post_text = postText;
}
if (options && typeof options.firstImageHash === 'string' && options.firstImageHash.trim()) {
payload.first_image_hash = options.firstImageHash.trim();
}
if (options && typeof options.firstImageUrl === 'string' && options.firstImageUrl.trim()) {
payload.first_image_url = options.firstImageUrl.trim();
}
const response = await backendFetch(`${API_URL}/posts`, {
method: 'POST',
headers: {
@@ -1667,6 +1847,18 @@ async function captureElementScreenshot(element) {
const endY = Math.min(documentHeight, elementBottom + verticalMargin);
const baseDocTop = Math.max(0, elementTop - verticalMargin);
const restoreScrollPosition = () => {
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
if (document.documentElement) {
document.documentElement.scrollTop = originalScrollY;
document.documentElement.scrollLeft = originalScrollX;
}
if (document.body) {
document.body.scrollTop = originalScrollY;
document.body.scrollLeft = originalScrollX;
}
};
try {
let iteration = 0;
let targetScroll = startY;
@@ -1723,7 +1915,9 @@ async function captureElementScreenshot(element) {
}
}
} finally {
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
restoreScrollPosition();
await delay(0);
restoreScrollPosition();
}
if (!segments.length) {
@@ -1845,6 +2039,125 @@ async function maybeDownscaleScreenshot(imageData) {
}
}
function isLikelyPostImage(img) {
if (!img) {
return false;
}
const src = img.currentSrc || img.src || '';
if (!src) {
return false;
}
if (src.startsWith('data:')) {
return false;
}
const lowerSrc = src.toLowerCase();
if (lowerSrc.includes('emoji') || lowerSrc.includes('static.xx') || lowerSrc.includes('sprite')) {
return false;
}
const width = img.naturalWidth || img.width || 0;
const height = img.naturalHeight || img.height || 0;
if (width < 120 || height < 120) {
return false;
}
return true;
}
function waitForImageLoad(img, timeoutMs = 1500) {
return new Promise((resolve) => {
if (!img) {
resolve(false);
return;
}
if (img.complete && img.naturalWidth > 0) {
resolve(true);
return;
}
let resolved = false;
const finish = (value) => {
if (resolved) return;
resolved = true;
resolve(value);
};
const timer = setTimeout(() => finish(false), timeoutMs);
img.addEventListener('load', () => {
clearTimeout(timer);
finish(true);
}, { once: true });
img.addEventListener('error', () => {
clearTimeout(timer);
finish(false);
}, { once: true });
});
}
function buildDHashFromPixels(imageData) {
if (!imageData || !imageData.data) {
return null;
}
const { data } = imageData;
const bits = [];
for (let y = 0; y < 8; y += 1) {
for (let x = 0; x < 8; x += 1) {
const leftIndex = ((y * 9) + x) * 4;
const rightIndex = ((y * 9) + x + 1) * 4;
const left = 0.299 * data[leftIndex] + 0.587 * data[leftIndex + 1] + 0.114 * data[leftIndex + 2];
const right = 0.299 * data[rightIndex] + 0.587 * data[rightIndex + 1] + 0.114 * data[rightIndex + 2];
bits.push(left > right ? 1 : 0);
}
}
let hex = '';
for (let i = 0; i < bits.length; i += 4) {
const value = (bits[i] << 3) | (bits[i + 1] << 2) | (bits[i + 2] << 1) | bits[i + 3];
hex += value.toString(16);
}
return hex.padStart(16, '0');
}
async function computeDHashFromUrl(imageUrl) {
if (!imageUrl) {
return null;
}
try {
const response = await fetch(imageUrl);
if (!response.ok) {
return null;
}
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
const canvas = document.createElement('canvas');
canvas.width = 9;
canvas.height = 8;
const ctx = canvas.getContext('2d');
if (!ctx) {
return null;
}
ctx.drawImage(bitmap, 0, 0, 9, 8);
const imageData = ctx.getImageData(0, 0, 9, 8);
return buildDHashFromPixels(imageData);
} catch (error) {
return null;
}
}
async function getFirstPostImageInfo(postElement) {
if (!postElement) {
return { hash: null, url: null };
}
const images = Array.from(postElement.querySelectorAll('img')).filter(isLikelyPostImage);
for (const img of images.slice(0, 5)) {
const loaded = await waitForImageLoad(img);
if (!loaded) {
continue;
}
const src = img.currentSrc || img.src;
const hash = await computeDHashFromUrl(src);
if (hash) {
return { hash, url: src };
}
}
return { hash: null, url: null };
}
function getStickyHeaderHeight() {
try {
const banner = document.querySelector('[role="banner"], header[role="banner"]');
@@ -1939,6 +2252,9 @@ function extractDeadlineFromPostText(postElement) {
const extractTimeAfterIndex = (text, index) => {
const tail = text.slice(index, index + 80);
if (/^\s*(?:-||—|bis)\s*\d{1,2}\.\d{1,2}\./i.test(tail)) {
return null;
}
const timeMatch = /^\s*(?:\(|\[)?\s*(?:[,;:-]|\b)?\s*(?:um|ab|bis|gegen|spätestens)?\s*(?:den|dem|am)?\s*(?:ca\.?)?\s*(\d{1,2})(?:[:.](\d{2}))?\s*(?:uhr|h)?\s*(?:\)|\])?/i.exec(tail);
if (!timeMatch) {
return null;
@@ -1950,6 +2266,9 @@ function extractDeadlineFromPostText(postElement) {
if (Number.isNaN(hour) || Number.isNaN(minute)) {
return null;
}
if (hour === 24 && minute === 0) {
return { hour: 23, minute: 59 };
}
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
return null;
}
@@ -1963,6 +2282,22 @@ function extractDeadlineFromPostText(postElement) {
};
const foundDates = [];
const rangePattern = /\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\s*(?:-||—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i;
const rangeMatch = rangePattern.exec(fullText);
if (rangeMatch) {
const endDay = parseInt(rangeMatch[4], 10);
const endMonth = parseInt(rangeMatch[5], 10);
let endYear = parseInt(rangeMatch[6], 10);
if (endYear < 100) {
endYear += 2000;
}
if (endMonth >= 1 && endMonth <= 12 && endDay >= 1 && endDay <= 31) {
const endDate = new Date(endYear, endMonth - 1, endDay, 0, 0, 0, 0);
if (endDate.getMonth() === endMonth - 1 && endDate.getDate() === endDay && endDate > today) {
return toDateTimeLocalString(endDate);
}
}
}
for (const pattern of patterns) {
let match;
@@ -2404,7 +2739,19 @@ async function renderTrackedStatus({
: null;
const isExpired = postData.deadline_at ? new Date(postData.deadline_at) < new Date() : false;
const canCurrentProfileCheck = postData.next_required_profile === profileNumber;
const requiredProfiles = Array.isArray(postData.required_profiles) && postData.required_profiles.length
? postData.required_profiles
.map((value) => {
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed)) {
return null;
}
return Math.min(5, Math.max(1, parsed));
})
.filter(Boolean)
: Array.from({ length: Math.max(1, Math.min(5, parseInt(postData.target_count, 10) || 1)) }, (_, index) => index + 1);
const isCurrentProfileRequired = requiredProfiles.includes(profileNumber);
const canCurrentProfileCheck = isCurrentProfileRequired && postData.next_required_profile === profileNumber;
const isCurrentProfileDone = checks.some(check => check.profile_number === profileNumber);
if (isFeedHome && isCurrentProfileDone) {
@@ -2434,16 +2781,23 @@ async function renderTrackedStatus({
${lastCheck ? `<div style="color: #65676b; font-size: 12px; white-space: nowrap;">Letzte: ${lastCheck}</div>` : ''}
`;
if (canCurrentProfileCheck && !isExpired && !completed) {
if (!isExpired && !completed && !isCurrentProfileDone && isCurrentProfileRequired) {
const checkButtonEnabled = canCurrentProfileCheck;
const buttonColor = checkButtonEnabled ? '#42b72a' : '#f39c12';
const cursorStyle = 'pointer';
const buttonTitle = checkButtonEnabled
? 'Beitrag bestätigen'
: 'Wartet auf vorherige Profile';
statusHtml += `
<button class="fb-tracker-check-btn" style="
<button class="fb-tracker-check-btn" title="${buttonTitle}" style="
padding: 4px 12px;
background-color: #42b72a;
background-color: ${buttonColor};
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
cursor: ${cursorStyle};
font-size: 13px;
white-space: nowrap;
margin-left: 8px;
@@ -2524,9 +2878,9 @@ async function renderTrackedStatus({
checkBtn.disabled = true;
checkBtn.textContent = 'Wird bestätigt...';
const result = await markPostChecked(postData.id, profileNumber);
const result = await markPostChecked(postData.id, profileNumber, { returnError: true });
if (result) {
if (result && !result.error) {
await renderTrackedStatus({
container,
postElement,
@@ -2540,8 +2894,13 @@ async function renderTrackedStatus({
});
} else {
checkBtn.disabled = false;
checkBtn.textContent = 'Fehler - Erneut versuchen';
checkBtn.style.backgroundColor = '#e74c3c';
checkBtn.textContent = '✓ Bestätigen';
if (result && result.error) {
showToast(result.error, 'error');
} else {
checkBtn.textContent = 'Fehler - Erneut versuchen';
checkBtn.style.backgroundColor = '#e74c3c';
}
}
});
}
@@ -2772,6 +3131,36 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
font-size: 13px;
max-width: 160px;
" />
<div class="fb-tracker-similarity" style="
display: none;
align-items: center;
gap: 8px;
padding: 4px 8px;
border: 1px solid #f0c36d;
background: #fff6d5;
border-radius: 6px;
font-size: 12px;
color: #6b5b00;
flex-basis: 100%;
">
<span class="fb-tracker-similarity__text"></span>
<button class="fb-tracker-merge-btn" type="button" style="
border: 1px solid #caa848;
background: white;
color: #7a5d00;
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
">Mergen</button>
<a class="fb-tracker-similarity-link" href="#" target="_blank" rel="noopener" style="
color: #7a5d00;
text-decoration: none;
font-weight: 600;
display: none;
">Öffnen</a>
</div>
<button class="fb-tracker-add-btn" style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
@@ -2792,8 +3181,109 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
const selectElement = container.querySelector(`#${selectId}`);
const deadlineInput = container.querySelector(`#${deadlineId}`);
selectElement.value = '2';
const similarityBox = container.querySelector('.fb-tracker-similarity');
const similarityText = container.querySelector('.fb-tracker-similarity__text');
const mergeButton = container.querySelector('.fb-tracker-merge-btn');
const similarityLink = container.querySelector('.fb-tracker-similarity-link');
const similarityPayloadPromise = buildSimilarityPayload(postElement);
let similarityPayload = null;
let similarityMatch = null;
const mainLinkUrl = postUrlData.mainUrl;
const resolveSimilarityPayload = async () => {
if (!similarityPayload) {
similarityPayload = await similarityPayloadPromise;
}
return similarityPayload;
};
if (similarityBox && similarityText && mergeButton) {
(async () => {
const payload = await resolveSimilarityPayload();
const similarity = await findSimilarPost({
url: postUrlData.url,
postText: payload.postText,
firstImageHash: payload.firstImageHash
});
if (!similarity || !similarity.match) {
return;
}
similarityMatch = similarity.match;
similarityText.textContent = formatSimilarityLabel(similarity);
similarityBox.style.display = 'flex';
if (!addButton.disabled) {
addButton.textContent = 'Neu speichern';
}
if (similarityLink) {
similarityLink.style.display = 'inline';
similarityLink.textContent = 'Öffnen';
if (similarityMatch.url) {
similarityLink.href = similarityMatch.url;
similarityLink.dataset.ready = '1';
} else {
similarityLink.href = '#';
similarityLink.dataset.ready = '0';
similarityLink.dataset.postId = similarityMatch.id;
}
}
})();
mergeButton.addEventListener('click', async () => {
if (!similarityMatch) {
return;
}
mergeButton.disabled = true;
const previousLabel = mergeButton.textContent;
mergeButton.textContent = 'Mergen...';
const payload = await resolveSimilarityPayload();
const urlCandidates = [postUrlData.url, ...postUrlData.allCandidates];
const uniqueUrls = Array.from(new Set(urlCandidates.filter(Boolean)));
const attached = await attachUrlToExistingPost(similarityMatch.id, uniqueUrls, payload);
if (attached) {
const updatedPost = await fetchPostByUrl(similarityMatch.url);
if (updatedPost) {
await renderTrackedStatus({
container,
postElement,
postData: updatedPost,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
return;
}
}
mergeButton.disabled = false;
mergeButton.textContent = previousLabel;
});
}
if (similarityLink && !similarityLink.dataset.bound) {
similarityLink.dataset.bound = '1';
similarityLink.addEventListener('click', async (event) => {
if (similarityLink.dataset.ready === '1') {
return;
}
event.preventDefault();
const postId = similarityLink.dataset.postId;
if (!postId) {
return;
}
const resolved = await fetchPostById(postId);
if (resolved && resolved.url) {
similarityLink.href = resolved.url;
similarityLink.dataset.ready = '1';
window.open(resolved.url, '_blank', 'noopener');
}
});
}
if (mainLinkUrl) {
const mainLinkButton = document.createElement('button');
mainLinkButton.className = 'fb-tracker-mainlink-btn';
@@ -2838,11 +3328,15 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
await delay(220);
const deadlineValue = deadlineInput ? deadlineInput.value : '';
const payload = await resolveSimilarityPayload();
const result = await addPostToTracking(postUrlData.url, targetCount, profileNumber, {
postElement,
deadline: deadlineValue,
candidates: postUrlData.allCandidates
candidates: postUrlData.allCandidates,
postText: payload.postText,
firstImageHash: payload.firstImageHash,
firstImageUrl: payload.firstImageUrl
});
if (result) {
@@ -3236,6 +3730,10 @@ let globalPostCounter = 0;
// Find all Facebook posts on the page
function findPosts() {
if (maybeRedirectPageReelsToMain()) {
return;
}
console.log('[FB Tracker] Scanning for posts...');
const postContainers = findPostContainers();
@@ -3343,6 +3841,7 @@ function findPosts() {
// Initialize
console.log('[FB Tracker] Initializing...');
maybeRedirectPageReelsToMain();
// Run multiple times to catch loading posts
setTimeout(findPosts, 2000);
@@ -4055,6 +4554,32 @@ function extractPostText(postElement) {
}
};
const hasEmojiChars = (text) => /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\u2600-\u27BF]/.test(text);
const injectEmojiLabels = (root) => {
if (!root || typeof root.querySelectorAll !== 'function') {
return;
}
const emojiNodes = root.querySelectorAll('img[alt], [role="img"][aria-label]');
emojiNodes.forEach((node) => {
const label = node.getAttribute('alt') || node.getAttribute('aria-label');
if (!label || !hasEmojiChars(label)) {
return;
}
const textNode = node.ownerDocument.createTextNode(label);
node.replaceWith(textNode);
});
};
const getTextWithEmojis = (element) => {
if (!element) {
return '';
}
const clone = element.cloneNode(true);
injectEmojiLabels(clone);
return clone.innerText || clone.textContent || '';
};
const SKIP_TEXT_CONTAINERS_SELECTOR = [
'div[role="textbox"]',
'[contenteditable="true"]',
@@ -4177,7 +4702,7 @@ function extractPostText(postElement) {
logPostText('Selector result skipped (inside disallowed region)', selector, makeSnippet(element.innerText || element.textContent || ''));
continue;
}
tryAddCandidate(element.innerText || element.textContent || '', element, { selector });
tryAddCandidate(getTextWithEmojis(element), element, { selector });
}
}
@@ -4200,11 +4725,12 @@ function extractPostText(postElement) {
const clone = postElement.cloneNode(true);
const elementsToRemove = clone.querySelectorAll(SKIP_TEXT_CONTAINERS_SELECTOR);
elementsToRemove.forEach((node) => node.remove());
injectEmojiLabels(clone);
const cloneText = clone.innerText || clone.textContent || '';
fallbackText = cleanCandidate(cloneText);
logPostText('Fallback extracted from clone', makeSnippet(fallbackText || cloneText));
} catch (error) {
const allText = postElement.innerText || postElement.textContent || '';
const allText = getTextWithEmojis(postElement);
fallbackText = cleanCandidate(allText);
logPostText('Fallback extracted from original element', makeSnippet(fallbackText || allText));
}
@@ -5071,19 +5597,19 @@ async function addAICommentButton(container, postElement) {
let postId = container.dataset.postId || '';
if (postId) {
latestData = await markPostChecked(postId, effectiveProfile);
latestData = await markPostChecked(postId, effectiveProfile, { ignoreOrder: true });
if (!latestData && decodedUrl) {
const refreshed = await checkPostStatus(decodedUrl);
if (refreshed && refreshed.id) {
container.dataset.postId = refreshed.id;
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
latestData = await markPostChecked(refreshed.id, effectiveProfile, { ignoreOrder: true }) || refreshed;
}
}
} else if (decodedUrl) {
const refreshed = await checkPostStatus(decodedUrl);
if (refreshed && refreshed.id) {
container.dataset.postId = refreshed.id;
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
latestData = await markPostChecked(refreshed.id, effectiveProfile, { ignoreOrder: true }) || refreshed;
}
}