NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name SPWorlds - Upvotes and Downvotes viewer
// @namespace https://github.com/SuhEugene/SPWorlds-UpAndDown
// @version 2024-08-28
// @description Отображает апвоуты и даунвоуты поста, вместо их разницы.
// @author SuhEugene, DearFox
// @match https://spworlds.ru/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=spworlds.ru
// @grant none
// @license MIT
// @updateURL https://github.com/SuhEugene/SPWorlds-UpAndDown/blob/master/spworlds-up-n-down.user.js?raw=true
// @homepageURL https://github.com/SuhEugene/SPWorlds-UpAndDown
// @contributionURL https://github.com/SuhEugene/SPWorlds-UpAndDown
// ==/UserScript==
(function () {
'use strict';
// Сохраняем оригинальный для вызова в будущем
const originalFetch = fetch;
const DIVIDER =
'<p style="background:#3d3d4b; width:2px; display:inline-block; height: 20px; border-radius:2px; margin: 0 12px;"></p>';
// Функция для обработки данных поста
function processPostData(serverId, data) {
if (!data) return console.warn('Переданный ответ пуст');
if (!data.id) return console.warn('Переданный ответ не содержит идентификатор поста');
if (data.upvotes === undefined || data.downvotes === undefined)
return console.warn('Ответ не содержит полей голосов за/против поста');
const id = data.id;
const upvotes = data.upvotes;
const downvotes = data.downvotes;
const updateAnchor = anchor => {
console.warn('Заменяем ссылку...');
const originalText = anchor.innerText.split(' : + ')[0];
anchor.innerText = `${originalText} : + ${upvotes} | - ${downvotes}`;
};
// Функция для обновления элемента
const updatePost = () => {
const anchor = document.querySelector(`a[href="/${serverId}/feed/${id}"]`);
if (!anchor) return false;
const postEl = anchor.closest('.relative.space-y-4');
if (!postEl) {
console.warn('Пост не найден');
updateAnchor(anchor);
return true;
}
const counterEl = postEl.querySelector('button[title=Апвоут] + p');
if (!counterEl) {
console.warn('Счётчик не найден');
updateAnchor(anchor);
return true;
}
const upvoteEl = postEl.querySelector('button[title=Апвоут]');
const downvoteEl = postEl.querySelector('button[title=Даунвоут]');
if (!upvoteEl || !downvoteEl) {
console.warn('Одна из кнопок не найдена');
updateAnchor(anchor);
return true;
}
const counterParent = upvoteEl.parentElement;
if (!counterParent) {
console.warn('Не найден родитель кнопок');
updateAnchor(anchor);
return true;
}
const upvoteSVGEl = upvoteEl.querySelector('svg');
const downvoteSVGEl = downvoteEl.querySelector('svg');
if (!upvoteSVGEl || !downvoteSVGEl) {
console.warn('SVG одной из кнопок не найден');
updateAnchor(anchor);
return true;
}
upvoteEl.classList.add('flex');
upvoteEl.classList.add('font-medium');
downvoteEl.classList.add('flex');
downvoteEl.classList.add('font-medium');
upvoteEl.innerHTML = `${upvoteSVGEl.outerHTML}<span style="color: white;">${upvotes}</span>`;
downvoteEl.innerHTML = `<span style="color: white;">${downvotes}</span>${downvoteSVGEl.outerHTML}`;
counterEl.outerHTML = DIVIDER;
return true;
};
// Создание MutationObserver для отслеживания изменений в DOM
const observer = new MutationObserver(() => updatePost() && observer.disconnect());
// Начало наблюдения за изменениями в DOM
observer.observe(document.body, { childList: true, subtree: true });
// Пытаемся обновить элемент сразу, если он уже существует
updatePost();
}
const GET_METHOD_REGEXES = [
// Конкретный пост
/^https:\/\/spworlds\.ru\/api\/(?<serverId>[a-z0-9_]+)\/posts\/[0-9a-fA-F-]+$/,
// Посты аккаунта
/^https:\/\/spworlds\.ru\/api\/(?<serverId>[a-z0-9_]+)\/posts\/from\/account\/[0-9a-fA-F-]+(\?.*)?$/,
// Посты группы
/^https:\/\/spworlds\.ru\/api\/(?<serverId>[a-z0-9_]+)\/posts\/from\/group\/[0-9a-fA-F-]+(\?.*)?$/,
// Посты страницы новостей
/^https:\/\/spworlds\.ru\/api\/(?<serverId>[a-z0-9_]+)\/posts\?.*$/
];
const checkGetRequest = async (url, options, response) => {
let serverId = null;
for (const regex of GET_METHOD_REGEXES) {
const match = url.match(regex);
if (!match) continue;
serverId = match.groups.serverId;
break;
}
if (!serverId) return;
try {
const data = await response.json();
if (Array.isArray(data)) data.forEach(post => processPostData(serverId, post));
else processPostData(serverId, data);
} catch (error) {
console.error('Ошибка при разборе ответа на получение поста(ов):', error);
}
};
const UPDATE_POST_REGEX = /^https:\/\/spworlds\.ru\/api\/(?<serverId>[a-z0-9_]+)\/posts\/[0-9a-fA-F-]+$/;
const checkPostRequest = async (url, options, response) => {
const match = url.match(UPDATE_POST_REGEX);
if (!match) return;
const [serverUrl] = match;
const serverId = match.groups.serverId;
if (!serverId) return;
const body = options?.body;
if (!body) return;
let jsonBody = null;
try {
jsonBody = JSON.parse(body);
} catch (error) {
console.error('Не удалось пропарсить тело POST запроса к API sp worlds', error);
}
if (!jsonBody) return;
if (jsonBody.isUpvote === undefined || jsonBody.vote === undefined) return;
try {
const json = await originalFetch(serverUrl, {
credentials: 'include'
}).then(r => r.json());
if (!json) return;
processPostData(serverId, json);
} catch (error) {
console.error('Ошибка кастомного получения поста:', error);
}
};
// Перехват fetch
window.fetch = async function (url, options = {}) {
const method = (options.method || 'GET').toUpperCase();
if (method === 'GET') {
const response = await originalFetch.apply(this, arguments);
checkGetRequest(url, options, response.clone());
return response;
} else if (method === 'POST') {
const response = await originalFetch.apply(this, arguments);
checkPostRequest(url, options, response.clone());
return response;
}
// Если запрос не соответствует условиям, выполняем его без изменений
return originalFetch.apply(this, arguments);
};
})();