Skip to content

Commit d85703b

Browse files
authored
Rework URL checking logic and disable loading user-uploaded content (#203)
* Rework URL checking logic and disable loading user uploaded content - Reworked the emote URL existence check to optimize the amount of requests sent - Reverted RC emote loading location to a folder located on files.riichi.moe and removed dependency on third party website (please upload new RC emotes somewhere and ask repomeidos to upload them to files.riichi.moe) - Removed custom emote loading (do not trust user uploads to avoid daily dose-like incidents :^)) * Code refactor and bump version to 1.3.10
1 parent fb1eb86 commit d85703b

File tree

1 file changed

+162
-169
lines changed

1 file changed

+162
-169
lines changed
Lines changed: 162 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// ==UserScript==
22
// @name /mjg/ Emote Replacer
33
// @namespace http://repo.riichi.moe/
4-
// @version 1.3.9
4+
// @namespace http://repo.riichi.moe/
5+
// @version 1.3.10
56
// @description Detects emote strings in imageless posts in /mjg/ threads, and displays them as fake images posts.
67
// @icon https://files.catbox.moe/3sh459.png
78
// @author Ling and Anon
@@ -20,40 +21,148 @@
2021
// Sources for MJS and RC emotes
2122
const EMOTE_BASE_URLS = [
2223
'https://files.riichi.moe/mjg/game%20resources%20and%20tools/Mahjong%20Soul/game%20files/emotes/',
23-
'https://files.riichi.moe/mjg/game%20resources%20and%20tools/Mahjong%20Soul/emote%20edits/',
24-
'https://tanoshii.moe/images/riichi_city_emotes/'
24+
'https://files.riichi.moe/mjg/game%20resources%20and%20tools/Riichi%20City/emotes_new/'
2525
];
26-
27-
const EMOTE_REGEX = /\b(([a-zA-Z0-9\-\.]+-\d+[a-zA-Z]{0,10}|mooncakes\/\d)\.(?:png|jpg|jpeg|gif))\b/i;
26+
const EMOTE_REGEX = /\b(([a-zA-Z0-9\&\-\.]+-\d+[a-z]{0,4}|mooncakes\/\d)\.(?:png|jpg|jpeg|gif))\b/i;
2827
const PROCESSED_MARKER = 'data-mjg-emote-processed'; // Values: 'true' (success), 'has-file', 'no-message', 'limit-not-reached', 'emote-not-found', 'checking'
2928

30-
// --- Helper: Check if remote image exists (tries multiple URLs) ---
31-
function checkImageExists(urls) {
32-
return new Promise(async resolve => {
33-
for (const url of urls) {
34-
try {
35-
const img = new Image();
36-
const result = await new Promise(resolve => {
37-
img.onload = () => resolve(true); // Image loaded successfully
38-
img.onerror = () => resolve(false); // Image failed to load (404, CORS block, invalid etc.)
39-
img.onabort = () => resolve(false); // Handle aborts as well
40-
try {
41-
img.src = url;
42-
} catch (e) {
43-
console.error("/mjg/ Emote Replacer: Error synchronously thrown while setting src for " + url, e);
44-
resolve(false);
29+
// --- Main Execution ---
30+
if (isMjgThread()) {
31+
requestAnimationFrame(() => {
32+
initialScan();
33+
observeNewPosts();
34+
});
35+
}
36+
37+
// --- Check if this is the correct type of thread ---
38+
function isMjgThread() {
39+
const opSubjectElement = document.querySelector('.opContainer .postInfo .subject, .opContainer .postInfoM .subject');
40+
if (!opSubjectElement) return false;
41+
const subjectText = opSubjectElement.textContent.toLowerCase();
42+
return subjectText.includes('mjg') || subjectText.includes('mahjong');
43+
}
44+
45+
// --- Initial Scan ---
46+
function initialScan() {
47+
const posts = document.querySelectorAll('.postContainer.replyContainer .post.reply');
48+
posts.forEach(post => {
49+
processPost(post).catch(e => {
50+
console.error("/mjg/ Emote Replacer: Error during async processPost in initial scan:", post?.id, e);
51+
// Mark post to avoid retrying on error.
52+
if (post) post.setAttribute(PROCESSED_MARKER, 'error');
53+
});
54+
});
55+
}
56+
57+
// --- Observe for new posts ---
58+
function observeNewPosts() {
59+
const threadElement = document.querySelector('.thread');
60+
if (!threadElement) {
61+
console.error('/mjg/ Emote Replacer: Could not find thread element to observe.');
62+
return;
63+
}
64+
const observer = new MutationObserver(mutations => {
65+
let postsToProcess = new Set();
66+
mutations.forEach(mutation => {
67+
mutation.addedNodes.forEach(node => {
68+
if (node.nodeType === Node.ELEMENT_NODE) {
69+
if (node.matches('.postContainer.replyContainer')) {
70+
const postElement = node.querySelector('.post.reply');
71+
if (postElement) postsToProcess.add(postElement);
72+
} else {
73+
node.querySelectorAll('.postContainer.replyContainer .post.reply').forEach(postElement => {
74+
postsToProcess.add(postElement);
75+
});
4576
}
46-
});
47-
if (result) {
48-
resolve({ exists: true, url: url });
49-
return;
5077
}
51-
} catch (e) {
52-
// Continue to next URL
53-
}
78+
});
79+
});
80+
81+
if (postsToProcess.size > 0) {
82+
postsToProcess.forEach(postElement => {
83+
processPost(postElement).catch(e => {
84+
console.error("/mjg/ Emote Replacer: Error during async processPost from observer:", postElement?.id, e);
85+
if (postElement) postElement.setAttribute(PROCESSED_MARKER, 'error');
86+
});
87+
});
5488
}
55-
resolve({ exists: false, url: null });
5689
});
90+
91+
observer.observe(threadElement, { childList: true, subtree: true });
92+
}
93+
94+
// --- Process a single post ---
95+
async function processPost(postElement) {
96+
// Basic checks first
97+
const postId = postElement?.id || 'unknown-element';
98+
if (!postElement || !postElement.matches || !postElement.matches('.post.reply')) return;
99+
100+
// Prevent re-processing or processing posts currently being checked
101+
const currentState = postElement.getAttribute(PROCESSED_MARKER);
102+
// Already processed, has file, no message, emote not found, or currently checking
103+
if (currentState && currentState !== 'limit-not-reached') return;
104+
105+
// Check if it already has a *real* file attachment
106+
if (postElement.querySelector('.file')) { postElement.setAttribute(PROCESSED_MARKER, 'has-file'); return; }
107+
108+
// Check if image limit is reached *now*
109+
const currentImageCount = getImageCount();
110+
if (currentImageCount === -1 || currentImageCount < IMAGE_LIMIT) {
111+
postElement.setAttribute(PROCESSED_MARKER, 'limit-not-reached'); // Mark temporarily
112+
return;
113+
}
114+
// If limit was previously not reached, clear that temporary state
115+
if (currentState === 'limit-not-reached') {
116+
postElement.removeAttribute(PROCESSED_MARKER);
117+
}
118+
119+
const postMessageElement = postElement.querySelector('.postMessage');
120+
if (!postMessageElement) {
121+
postElement.setAttribute(PROCESSED_MARKER, 'no-message');
122+
return;
123+
}
124+
125+
const emoteString = findEmoteInNodes(postMessageElement);
126+
127+
if (emoteString) {
128+
129+
// Mark as checking to prevent concurrent checks from observer
130+
postElement.setAttribute(PROCESSED_MARKER, 'checking');
131+
132+
// Small delay might prevent rare race conditions.
133+
await new Promise(resolve => setTimeout(resolve, 50));
134+
135+
if (postElement.getAttribute(PROCESSED_MARKER) !== 'checking') return; // State changed during await
136+
137+
// Check if the remote image actually exists
138+
const fullImageUrl = await findImageUrl(emoteString);
139+
if (postElement.getAttribute(PROCESSED_MARKER) !== 'checking') return; // State changed during check
140+
141+
if (fullImageUrl) {
142+
addFakeImage(postElement, emoteString, fullImageUrl);
143+
postElement.setAttribute(PROCESSED_MARKER, 'true');
144+
} else {
145+
// Mark as processed but note that the emote was not found
146+
postElement.setAttribute(PROCESSED_MARKER, 'emote-not-found');
147+
}
148+
} else {
149+
// No emote string found in this post, mark it as processed for this state
150+
postElement.setAttribute(PROCESSED_MARKER, 'no-emote-found');
151+
}
152+
}
153+
154+
// --- Get current image count ---
155+
function getImageCount() {
156+
let fileCountElement = document.getElementById('file-count');
157+
if (!fileCountElement) {
158+
const threadStatsDiv = document.querySelector('.thread-stats .ts-images');
159+
if (threadStatsDiv) fileCountElement = threadStatsDiv;
160+
}
161+
if (fileCountElement?.textContent) {
162+
const count = parseInt(fileCountElement.textContent.trim(), 10);
163+
return isNaN(count) ? -1 : count;
164+
}
165+
return -1;
57166
}
58167

59168
// --- Helper: Find Emote String by Traversing Nodes (Recursive) ---
@@ -80,30 +189,35 @@
80189
return null;
81190
}
82191

83-
// --- 1. Check if this is the correct type of thread ---
84-
function isMjgThread() {
85-
const opSubjectElement = document.querySelector('.opContainer .postInfo .subject, .opContainer .postInfoM .subject');
86-
if (!opSubjectElement) return false;
87-
const subjectText = opSubjectElement.textContent.toLowerCase();
88-
return subjectText.includes('mjg') || subjectText.includes('mahjong');
192+
// --- Helper: Find if the emote exists in any of the base folders
193+
async function findImageUrl(emoteString) {
194+
for (const baseURL of EMOTE_BASE_URLS) {
195+
const fullImageUrl = baseURL + encodeURIComponent(emoteString);
196+
if (await checkImageExists(fullImageUrl)) {
197+
return fullImageUrl;
198+
}
199+
}
200+
return null;
89201
}
90202

91-
// --- 2. Get current image count ---
92-
function getImageCount() {
93-
let fileCountElement = document.getElementById('file-count');
94-
if (!fileCountElement) {
95-
const threadStatsDiv = document.querySelector('.thread-stats .ts-images');
96-
if (threadStatsDiv) fileCountElement = threadStatsDiv;
97-
}
98-
if (fileCountElement?.textContent) {
99-
const count = parseInt(fileCountElement.textContent.trim(), 10);
100-
return isNaN(count) ? -1 : count;
101-
}
102-
return -1;
203+
// --- Helper: Check if remote image exists ---
204+
function checkImageExists(url) {
205+
return new Promise(resolve => {
206+
const img = new Image();
207+
img.onload = () => resolve(true); // Image loaded successfully
208+
img.onerror = (err) => resolve(false); // Image failed to load (404, CORS block, invalid etc.)
209+
img.onabort = () => resolve(false); // Handle aborts as well
210+
try {
211+
img.src = url;
212+
} catch (e) {
213+
console.error("/mjg/ Emote Replacer: Error synchronously thrown while setting src for " + url, e);
214+
resolve(false);
215+
}
216+
});
103217
}
104218

105-
// --- 3. Create and inject fake image HTML ---
106-
function addFakeImage(postElement, emoteString, resolvedUrl) {
219+
// --- Create and inject fake image HTML ---
220+
function addFakeImage(postElement, emoteString, fullImageUrl) {
107221
const postId = postElement.id ? postElement.id.substring(1) : null;
108222
if (!postId) {
109223
console.warn("/mjg/ Emote Replacer: Could not get post ID for", postElement);
@@ -112,7 +226,6 @@
112226
// Double check file doesn't exist (in case of race condition)
113227
if (postElement.querySelector('.file')) return;
114228

115-
const fullImageUrl = resolvedUrl;
116229
const uniqueFileId = `f${postId}-emote`;
117230
const uniqueFileTextId = `fT${postId}-emote`;
118231

@@ -169,126 +282,6 @@
169282
} else {
170283
postElement.appendChild(fileDiv);
171284
}
172-
// console.log(`/mjg/ Emote Replacer: Added fake image for ${emoteString} to post ${postId}`);
173-
}
174-
175-
176-
// --- 4. Process a single post ---
177-
async function processPost(postElement) {
178-
// Basic checks first
179-
const postId = postElement?.id || 'unknown-element';
180-
if (!postElement || !postElement.matches || !postElement.matches('.post.reply')) return;
181-
182-
// Prevent re-processing or processing posts currently being checked
183-
const currentState = postElement.getAttribute(PROCESSED_MARKER);
184-
// Already processed, has file, no message, emote not found, or currently checking
185-
if (currentState && currentState !== 'limit-not-reached') return;
186-
187-
// Check if it already has a *real* file attachment
188-
if (postElement.querySelector('.file')) { postElement.setAttribute(PROCESSED_MARKER, 'has-file'); return; }
189-
190-
// Check if image limit is reached *now*
191-
const currentImageCount = getImageCount();
192-
if (currentImageCount === -1 || currentImageCount < IMAGE_LIMIT) {
193-
postElement.setAttribute(PROCESSED_MARKER, 'limit-not-reached'); // Mark temporarily
194-
return;
195-
}
196-
// If limit was previously not reached, clear that temporary state
197-
if (currentState === 'limit-not-reached') {
198-
postElement.removeAttribute(PROCESSED_MARKER);
199-
}
200-
201-
const postMessageElement = postElement.querySelector('.postMessage');
202-
if (!postMessageElement) {
203-
postElement.setAttribute(PROCESSED_MARKER, 'no-message');
204-
return;
205-
}
206-
207-
const emoteString = findEmoteInNodes(postMessageElement);
208-
209-
if (emoteString) {
210-
// Generate all candidate URLs for this emote
211-
const candidateUrls = EMOTE_BASE_URLS.map(base => base + encodeURIComponent(emoteString));
212-
213-
// Mark as checking to prevent concurrent checks from observer
214-
postElement.setAttribute(PROCESSED_MARKER, 'checking');
215-
216-
// Small delay might prevent rare race conditions.
217-
await new Promise(resolve => setTimeout(resolve, 50));
218-
219-
if (postElement.getAttribute(PROCESSED_MARKER) !== 'checking') return; // State changed during await
220-
221-
// Check if the remote image actually exists
222-
const imageExists = await checkImageExists(candidateUrls);
223-
if (postElement.getAttribute(PROCESSED_MARKER) !== 'checking') return; // State changed during check
224-
if (imageExists.exists) {
225-
addFakeImage(postElement, emoteString, imageExists.url);
226-
postElement.setAttribute(PROCESSED_MARKER, 'true');
227-
} else {
228-
// Mark as processed but note that the emote was not found
229-
postElement.setAttribute(PROCESSED_MARKER, 'emote-not-found');
230-
}
231-
} else {
232-
// No emote string found in this post, mark it as processed for this state
233-
postElement.setAttribute(PROCESSED_MARKER, 'no-emote-found');
234-
}
235-
}
236-
237-
// --- 5. Initial Scan ---
238-
function initialScan() {
239-
const posts = document.querySelectorAll('.postContainer.replyContainer .post.reply');
240-
posts.forEach(post => {
241-
processPost(post).catch(e => {
242-
console.error("/mjg/ Emote Replacer: Error during async processPost in initial scan:", post?.id, e);
243-
// Mark post to avoid retrying on error.
244-
if (post) post.setAttribute(PROCESSED_MARKER, 'error');
245-
});
246-
});
247-
}
248-
249-
// --- 6. Observe for new posts ---
250-
function observeNewPosts() {
251-
const threadElement = document.querySelector('.thread');
252-
if (!threadElement) {
253-
console.error('/mjg/ Emote Replacer: Could not find thread element to observe.');
254-
return;
255-
}
256-
const observer = new MutationObserver(mutations => {
257-
let postsToProcess = new Set();
258-
mutations.forEach(mutation => {
259-
mutation.addedNodes.forEach(node => {
260-
if (node.nodeType === Node.ELEMENT_NODE) {
261-
if (node.matches('.postContainer.replyContainer')) {
262-
const postElement = node.querySelector('.post.reply');
263-
if (postElement) postsToProcess.add(postElement);
264-
} else {
265-
node.querySelectorAll('.postContainer.replyContainer .post.reply').forEach(postElement => {
266-
postsToProcess.add(postElement);
267-
});
268-
}
269-
}
270-
});
271-
});
272-
273-
if (postsToProcess.size > 0) {
274-
postsToProcess.forEach(postElement => {
275-
processPost(postElement).catch(e => {
276-
console.error("/mjg/ Emote Replacer: Error during async processPost from observer:", postElement?.id, e);
277-
if (postElement) postElement.setAttribute(PROCESSED_MARKER, 'error');
278-
});
279-
});
280-
}
281-
});
282-
283-
observer.observe(threadElement, { childList: true, subtree: true });
284-
}
285-
286-
// --- Main Execution ---
287-
if (isMjgThread()) {
288-
requestAnimationFrame(() => {
289-
initialScan();
290-
observeNewPosts();
291-
});
292285
}
293286

294287
})();

0 commit comments

Comments
 (0)