├── .gitignore ├── counter ├── dtf_counter.meta.js └── dtf_counter.user.js ├── dislikes ├── dislikes_count.meta.js └── dislikes_count.user.js ├── favourites ├── favourites_count.meta.js └── favourites_count.user.js ├── reply-with-quote ├── reply-with-quote.meta.js └── reply-with-quote.user.js ├── plus-popup-remover ├── plus_popup_remover.meta.js └── plus_popup_remover.user.js ├── profile-counters ├── profile-counters.meta.js └── profile-counters.user.js ├── black-list ├── black-list.meta.js └── black-list.user.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | public/vehicle_photos 4 | .DS_Store 5 | coverage 6 | .nyc_output 7 | newrelic_agent\.log 8 | report*.json 9 | dist 10 | .env 11 | upload 12 | -------------------------------------------------------------------------------- /counter/dtf_counter.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf counter 3 | // @version 3 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Добавляет счётчики на страницу редактирования статьи 6 | // @author Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/counter/dtf_counter.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/counter/dtf_counter.user.js 9 | // @include *://dtf.ru/* 10 | // @grant none 11 | // ==/UserScript== -------------------------------------------------------------------------------- /dislikes/dislikes_count.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj dislikes counter 3 | // @version 5 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Добавляет счётчики дизлайков в посты 6 | // @author Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/dislikes/dislikes_count.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/dislikes/dislikes_count.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== -------------------------------------------------------------------------------- /favourites/favourites_count.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj favourites counter 3 | // @version 4 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Добавляет счётчики добавления в избранное 6 | // @author Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/favourites/favourites_count.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/favourites/favourites_count.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== -------------------------------------------------------------------------------- /reply-with-quote/reply-with-quote.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj favourites counter 3 | // @version 4 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Добавляет кнопку для ответа с цитатой 6 | // @author Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/reply-with-quote/reply-with-quote.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/reply-with-quote/reply-with-quote.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== -------------------------------------------------------------------------------- /plus-popup-remover/plus_popup_remover.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj plus popup remover 3 | // @version 2 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Вырезает надоедливое окно о покупке плюса 6 | // @author Apanasik Andrei 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/plus-popup-remover/plus_popup_remover.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/plus-popup-remover/plus_popup_remover.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== -------------------------------------------------------------------------------- /profile-counters/profile-counters.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Отображение счётчиков количества статей и комментариев в профилях участников 3 | // @version 3 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Возвращаем количество постов и комментариев в профиле 6 | // @author Apanasik aka Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/profile-counters/profile-counters.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/profile-counters/profile-counters.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @include *://*.vc.ru* 14 | // @include *://vc.ru/* 15 | // ==/UserScript== -------------------------------------------------------------------------------- /black-list/black-list.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Separate blacklist 3 | // @version 1 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Раздельный ЧС для постов, репостов и комментариев 6 | // @author Apanasik aka Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/black-list/black-list.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/black-list/black-list.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @include *://*.vc.ru* 14 | // @include *://vc.ru/* 15 | // @grant GM_addStyle 16 | // @grant GM.setValue 17 | // @grant GM.getValue 18 | // @grant unsafeWindow 19 | // ==/UserScript== -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Скрипты 2 | Различные скрипты для сайтов Комитета. 3 | 4 | ## Счётчики количества статей и комментариев в профилях участников 5 | ![Пример счётчиков на DTF](https://user-images.githubusercontent.com/1946939/145716783-a5a41122-ca11-4dd0-98ac-c8469ca501f2.png "Пример счётчиков") 6 | 7 | 8 | ### Известные проблемы 9 | 1. Не работает для профилей, которые скрыла администрация или сам участник в настройках. 10 | 2. Не учитываются посты и комменты в закрытых подсайтах. 11 | 3. Отображается не сразу, т. к. нужно сделать дополнительный запрос к Очобе. 12 | 4. После переходов между страницами может перестать работать. 13 | 14 | ## Счётчик количества слов и символов в статье 15 | 16 | ![Пример счётчика](https://github.com/Suvitruf/dtf-scripts/assets/1946939/f0fc1ba7-77c2-4ad9-9155-f13c6f844f53) 17 | 18 | Открываем [этот скрипт](https://github.com/Suvitruf/dtf-scripts/raw/master/counter/dtf_counter.user.js) и откроется окно установки. 19 | 20 | # Установка 21 | 1. Устанавливаем Tampermonkey. 22 | - Chrome (Vivaldi, Яндекс Браузер, Chromium, etc.) — https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo 23 | - Firefox — https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/ 24 | - Opera — https://addons.opera.com/en/extensions/details/tampermonkey-beta/ 25 | - Safari — с сайта разработчика расширения или из Mac Store 26 | 2. Открываем файл нужного скрипта, например, для отображения счётчиков постов и комментов: https://github.com/Suvitruf/dtf-scripts/raw/master/profile-counters/profile-counters.user.js. 27 | Устанавливаем скрипт (жмём на кнопку Install / Установить). -------------------------------------------------------------------------------- /plus-popup-remover/plus_popup_remover.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj plus popup remover 3 | // @version 2 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Вырезает надоедливое окно о покупке плюса 6 | // @author Apanasik Andrei 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/plus-popup-remover/plus_popup_remover.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/plus-popup-remover/plus_popup_remover.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== 15 | 16 | (function () { 17 | subOnChanges(); 18 | })(); 19 | 20 | function getPopup() { 21 | let plusPopup = document.querySelector('.plus-sheet'); 22 | if (plusPopup) 23 | return plusPopup; 24 | 25 | plusPopup = document.querySelector('.plus-sheet__gradient'); 26 | 27 | return plusPopup; 28 | } 29 | 30 | function subOnChanges() { 31 | // корневой контейнер 32 | const container = document.querySelector('.app--content-entry'); 33 | 34 | const observer = new MutationObserver(() => { 35 | let plusPopup = getPopup(); 36 | 37 | // если на странице есть попап 38 | if (plusPopup) { 39 | // возвращаем основной скрол на странице 40 | container.style = ''; 41 | 42 | // вырезаем попап 43 | container.removeChild(plusPopup.parentNode.parentNode.parentNode); 44 | } 45 | }); 46 | const config = { 47 | characterData: false, 48 | attributes: true, 49 | childList: true, 50 | subtree: false, 51 | }; 52 | 53 | // подписываемся на его изменения 54 | observer.observe(container, config); 55 | } -------------------------------------------------------------------------------- /dislikes/dislikes_count.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj dislikes counter 3 | // @version 5 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/dislikes/dislikes_count.meta.js 6 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/dislikes/dislikes_count.user.js 7 | // @description Добавляет счётчики дизлайков в посты 8 | // @author Suvitruf 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== 15 | const postIdRx = /\/([0-9]*)/; 16 | 17 | let lastLocation; 18 | 19 | (function () { 20 | startChecker() 21 | .catch(); 22 | })(); 23 | 24 | async function startChecker() { 25 | setInterval(async function () { 26 | const itemsBlocks = document.getElementsByClassName('l-hidden entry_data'); 27 | if (!itemsBlocks || !itemsBlocks.length) 28 | return; 29 | 30 | const dataBlock = itemsBlocks[0]; 31 | const info = dataBlock.attributes.getNamedItem("data-article-info"); 32 | const location = document.location; 33 | 34 | if(!info || (lastLocation === location.href)) 35 | return; 36 | 37 | const value = JSON.parse(info.value); 38 | const likes = value.likes; 39 | 40 | const lastSlash = location.href.lastIndexOf('/'); 41 | const id = postIdRx.exec(location.href.substring(lastSlash)); 42 | const post = await fetch(`https://api.dtf.ru/v1.9/entry/${id[1]}`); 43 | if (post.status !== 200) 44 | return; 45 | 46 | lastLocation = location.href; 47 | 48 | const data = await post.json(); 49 | const likesSum = data.result.likes.summ; 50 | setCounters(likes, likesSum, location); 51 | 52 | }, 2000); 53 | } 54 | 55 | function setCounters(likes, likesSum, location){ 56 | const itemsBlocks = document.getElementsByClassName('content-footer__item'); 57 | if (!itemsBlocks || !itemsBlocks.length) 58 | return; 59 | 60 | let likesSpan; 61 | for (const item of itemsBlocks) { 62 | const likesBlock = item.getElementsByClassName('like-button__count'); 63 | if (likesBlock && likesBlock.length) { 64 | likesSpan = likesBlock[0]; 65 | break; 66 | } 67 | } 68 | 69 | if (!likesSpan) 70 | return; 71 | 72 | likesSpan.innerHTML = likes + ' (' + likesSum + '/' + Math.abs(likes - likesSum) + ')'; 73 | } -------------------------------------------------------------------------------- /favourites/favourites_count.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj favourites counter 3 | // @version 4 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/favourites/favourites_count.meta.js 6 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/favourites/favourites_count.user.js 7 | // @description Добавляет счётчики добавления в избранное 8 | // @author Suvitruf 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== 15 | (function () { 16 | startChecker(); 17 | })(); 18 | 19 | let lastBlock; 20 | 21 | function startChecker() { 22 | setInterval(function () { 23 | const itemsBlocks = document.getElementsByClassName('content-footer__item'); 24 | if (!itemsBlocks || !itemsBlocks.length) 25 | return; 26 | 27 | let favBlock; 28 | for (const item of itemsBlocks) { 29 | const favMarkers = item.getElementsByClassName('favorite_marker__action'); 30 | if (favMarkers && favMarkers.length) { 31 | favBlock = favMarkers[0]; 32 | break; 33 | } 34 | } 35 | 36 | if (!favBlock) 37 | return; 38 | 39 | const dataBlocks = document.getElementsByClassName('l-hidden entry_data'); 40 | if (!dataBlocks || !dataBlocks.length) 41 | return; 42 | 43 | const dataBlock = dataBlocks[0]; 44 | 45 | if (dataBlock !== null && (lastBlock === null || lastBlock !== favBlock)) { 46 | lastBlock = favBlock; 47 | getCount(dataBlock, favBlock); 48 | } 49 | }, 3000); 50 | } 51 | 52 | function getCount(dataBlock, favBlock) { 53 | const count = JSON.parse(dataBlock.dataset.articleInfo).favorites; 54 | 55 | let counter = document.getElementById('favorite_suvitruf_counter'); 56 | if (!counter) { 57 | counter = document.createElement('div'); 58 | counter.id = 'favorite_suvitruf_counter'; 59 | counter.innerText = count; 60 | counter.classList.add("comments_counter__count__value"); 61 | counter.classList.add("comments_counter"); 62 | favBlock.parentNode.onclick = () => { 63 | let intCount = parseInt(counter.innerText); 64 | counter.innerText = counter.parentNode.classList.contains('favorite_marker--active') ? intCount + 1 : intCount - 1; 65 | } 66 | favBlock.parentNode.appendChild(counter); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /reply-with-quote/reply-with-quote.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf/tj reply with quote 3 | // @version 4 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/reply-with-quote/reply-with-quote.meta.js 6 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/reply-with-quote/reply-with-quote.user.js 7 | // @description Добавляет кнопку для ответа с цитатой 8 | // @author Suvitruf 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @grant none 14 | // ==/UserScript== 15 | 16 | function getNestedElementsByClassName(className, node = document) { 17 | let nodeArray = []; 18 | if (node.classList && node.classList.contains(className)) { 19 | nodeArray.push(node); 20 | } 21 | if (node.children) { 22 | for (let i = 0; i < node.children.length; i++) { 23 | nodeArray = nodeArray.concat(getNestedElementsByClassName(className, node.children[i])); 24 | } 25 | } 26 | return nodeArray; 27 | } 28 | 29 | function formatText(text) { 30 | if (!text) 31 | return ''; 32 | return '> ' + text.replaceAll('\n', '\n> '); 33 | } 34 | 35 | function addReplyButtons() { 36 | const replyButtons = document.querySelectorAll('div[class*="comments__item__reply"]:not(.already_added_reply_quote)'); // check by class if it has already been added 37 | 38 | if (!replyButtons || !replyButtons.length) 39 | return; 40 | 41 | const newButtons = []; 42 | for (const button of replyButtons) { 43 | button.classList.add('already_added_reply_quote'); // add the class to prevent recursion buttons 44 | const copyButton = button.cloneNode(true); 45 | copyButton.style.cssText = '-moz-user-select: -moz-none; -khtml-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none;'; // add styles to prevent the selection from unselect but it will be still unselect 46 | const children = copyButton.childNodes; 47 | for (const child in children) { 48 | if (!children.hasOwnProperty(child)) 49 | continue; 50 | 51 | const item = children[child]; 52 | if (item.nodeName.toLowerCase() !== 'span') 53 | continue; 54 | item.innerHTML = 'Цитата'; 55 | } 56 | 57 | newButtons.push({ 58 | button: copyButton, 59 | originalButton: button 60 | }); 61 | } 62 | 63 | for (const button of newButtons) { 64 | const parent = button.originalButton.parentNode; 65 | button.originalButton.after(button.button); 66 | 67 | const contentNodes = parent.getElementsByClassName('comments__item__text'); 68 | if (!contentNodes || !contentNodes.length) 69 | continue; 70 | 71 | const rootNode = parent.parentNode.parentNode; 72 | 73 | button.button.addEventListener('click', function () { 74 | const text = window.getSelection().toString() || contentNodes[0].innerText; // if we have selected text - get it otherwise get all the text of comment 75 | window.getSelection().removeAllRanges(); // just remove selected text after 76 | 77 | // при клике на "ответить" создаётся инпут 78 | // мне лень копаться в инвентах и что-то там переопределять, поэтому просто ждём какое-то время 79 | // через которое это поле явно уже будет создано 80 | setTimeout(function () { 81 | const contentInputs = getNestedElementsByClassName('content_editable', rootNode); 82 | if (!contentInputs || !contentInputs.length) 83 | return; 84 | 85 | const placeholders = getNestedElementsByClassName('thesis__placeholder', rootNode); 86 | if (placeholders && placeholders.length) { 87 | placeholders[0].parentNode.removeChild(placeholders[0]); 88 | } 89 | 90 | contentInputs[0].innerText = formatText(text); 91 | }, 30); 92 | }); 93 | } 94 | } 95 | 96 | addReplyButtons(); 97 | 98 | // Welp, don't override AJAX request cuz it's shit idea. Just set listener :) 99 | addEventListener('DOMContentLoaded', function () { 100 | addReplyButtons(); 101 | }); 102 | 103 | addEventListener('DOMNodeInserted', function () { 104 | addReplyButtons(); 105 | }); 106 | -------------------------------------------------------------------------------- /profile-counters/profile-counters.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Отображение счётчиков количества статей и комментариев в профилях участников 3 | // @version 3 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Возвращаем количество постов и комментариев в профиле 6 | // @author Apanasik aka Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/profile-counters/profile-counters.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/profile-counters/profile-counters.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @include *://*.vc.ru* 14 | // @include *://vc.ru/* 15 | // ==/UserScript== 16 | 17 | const profileRx = /\/u\/([0-9]*)-([A-Za-z]|[0-9]|-)*(\/entries|\/comments|\/favourites|\/votes|\/drafts|\/updates|\/donates|\/details|)+/; 18 | const userIdRx = /\/u\/([0-9]*)/; 19 | 20 | let dataSet = false; 21 | 22 | function setCounter(urlPart, label, value) { 23 | const tabBlock = document.querySelector(`a[href*="${urlPart}"][class*="v-tab"]`); 24 | if (!tabBlock) 25 | return; 26 | 27 | tabBlock.firstChild.innerHTML = `${label}${value}`; 28 | } 29 | 30 | async function loadProfileViaAPI(id, host) { 31 | // я не смог вытащить этих данных из текущей страницы 32 | // поэтому пока единственным решением вижу её скачать вручную и спарсить 33 | const profile = await fetch(`https://api.${host}/v1.9/user/${id}`); 34 | if (profile.status !== 200) 35 | return; 36 | 37 | const rawData = await profile.json(); 38 | 39 | // может быть 403 ошибка, если, к примеру, администрация скрыла профиль 40 | // профиль того же Олегоси не получить через api 41 | if (rawData.error) { 42 | console.error('cant load profile info: ', rawData.message); 43 | 44 | return; 45 | } 46 | 47 | const json = rawData.result; 48 | 49 | if (!json || !json.counters) 50 | return; 51 | 52 | const postsCount = json.counters.entries; 53 | const commentsCount = json.counters.comments; 54 | 55 | setCounter('entries', 'Статьи', postsCount); 56 | setCounter('comments', 'Комментарии', commentsCount); 57 | 58 | dataSet = true; 59 | } 60 | 61 | 62 | async function loadProfileAsIs(id, host) { 63 | // я не смог вытащить этих данных из текущей страницы 64 | // поэтому пока единственным решением вижу её скачать вручную и спарсить 65 | const profile = await fetch(`https://${host}/u/${id}`); 66 | if (profile.status !== 200) 67 | return; 68 | 69 | const rawData = await profile.text(); 70 | 71 | const data = rawData.match(//gms); 72 | const raw = data[0]; 73 | let jsonStr = raw.substr(raw.indexOf('{')); 74 | jsonStr = jsonStr.substr(0, jsonStr.lastIndexOf('}') + 1); 75 | const json = JSON.parse(jsonStr.replace(/"/g, '"')); 76 | 77 | if (!json.header || !json.header.tabs || !json.header.tabs.length) 78 | return; 79 | 80 | for (const tab of json.header.tabs) { 81 | if (tab.label === `Черновики`) 82 | continue; 83 | 84 | const tabBlock = document.querySelector(`a[href*="${tab.url}"][class*="v-tab"]`); 85 | if (!tabBlock) 86 | continue; 87 | 88 | tabBlock.firstChild.innerHTML = `${tab.label}${tab.counter}`; 89 | } 90 | } 91 | 92 | async function loadProfile(id, host) { 93 | console.log('loadProfile', id); 94 | try { 95 | await loadProfileViaAPI(id, host); 96 | } catch (e) { 97 | console.error(e); 98 | await loadProfileAsIs(id, host); 99 | } 100 | 101 | dataSet = true; 102 | } 103 | 104 | function clear() { 105 | dataSet = false; 106 | } 107 | 108 | async function checkIfProfile() { 109 | // получаем текущий адрес и проверяем на то, что это профиль 110 | const location = document.location; 111 | const href = location.href; 112 | 113 | // не какая-либо страница юзера 114 | const uIndex = href.indexOf('/u/'); 115 | // явно не профиль 116 | if (uIndex === -1) { 117 | clear(); 118 | return; 119 | } 120 | 121 | const lastSlash = href.lastIndexOf('/'); 122 | // какая-то юзерская страница, но не корневая 123 | if (lastSlash !== uIndex + 2) { 124 | const rx = profileRx.exec(href); 125 | 126 | // скорей всего конкретный пост 127 | if (!(rx && rx.length && rx.length >= 4 && rx[3])) { 128 | clear(); 129 | return; 130 | } 131 | } 132 | 133 | // сюда попали, если это корневая страница профиля или одна из дополнительных (комменты, закладки и т. п). 134 | 135 | // проверяем, если уже устанавливали счётчики, чтоб лишний раз не нагружать страницу 136 | if (dataSet) 137 | return; 138 | 139 | // вытаскиваем id юзера 140 | const id = userIdRx.exec(href); 141 | 142 | await loadProfile(id[1], location.host); 143 | } 144 | 145 | async function checkProfile() { 146 | checkIfProfile(); 147 | // хрен знает, как сделать, чтоб проверялка работала только при открытии профиля 148 | setInterval(() => { 149 | new Promise(checkIfProfile) 150 | .catch(er => { 151 | console.error(er); 152 | }); 153 | }, 3000); 154 | } 155 | 156 | checkProfile() 157 | .catch(er => { 158 | console.error(er); 159 | }); -------------------------------------------------------------------------------- /counter/dtf_counter.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name dtf counter 3 | // @version 3 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Добавляет счётчики на страницу редактирования статьи 6 | // @author Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/counter/dtf_counter.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/counter/dtf_counter.user.js 9 | // @include *://dtf.ru/* 10 | // @grant none 11 | // ==/UserScript== 12 | const URL_REGEX = new RegExp('dtf.ru/u/(.*)writing='); 13 | 14 | let oldHref = document.location.href; 15 | let updater = null; 16 | 17 | function checkElem() { 18 | waitForElm('.editor-cp-tabs').then((elem) => { 19 | addBlock(elem); 20 | startCounter(); 21 | }); 22 | } 23 | 24 | (function () { 25 | checkElem(); 26 | })(); 27 | 28 | 29 | window.onload = function () { 30 | const bodyList = document.querySelector('body') 31 | 32 | const observer = new MutationObserver(function (mutations) { 33 | if (oldHref !== document.location.href) { 34 | oldHref = document.location.href; 35 | if (URL_REGEX.test(oldHref)) 36 | checkElem(); 37 | else { 38 | if (updater) 39 | clearInterval(updater); 40 | } 41 | } 42 | }); 43 | 44 | const config = { 45 | childList: true, 46 | subtree: true 47 | }; 48 | 49 | observer.observe(bodyList, config); 50 | }; 51 | 52 | /*** 53 | * Ожидание появления элемента по selector'у 54 | * https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists 55 | */ 56 | 57 | function waitForElm(selector) { 58 | return new Promise(resolve => { 59 | if (document.querySelector(selector)) { 60 | return resolve(document.querySelector(selector)); 61 | } 62 | 63 | const observer = new MutationObserver(mutations => { 64 | if (document.querySelector(selector)) { 65 | resolve(document.querySelector(selector)); 66 | observer.disconnect(); 67 | } 68 | }); 69 | 70 | observer.observe(document.body, { 71 | childList: true, 72 | subtree: true 73 | }); 74 | }); 75 | } 76 | 77 | /*** 78 | * Стартуем таймер, который раз в секунду будет пересчитывать. 79 | */ 80 | 81 | function startCounter() { 82 | updater = setInterval(function () { 83 | const block = document.getElementsByClassName('ce-block__content'); 84 | const paragraphs = document.getElementsByClassName('ce-paragraph'); 85 | 86 | if (block && block.length) { 87 | let words = 0; 88 | let letters = 0; 89 | 90 | for (let element of block) { 91 | const txt = element.textContent || element.innerText || ''; 92 | const counters = calc(txt); 93 | 94 | words += counters.words; 95 | letters += counters.letters; 96 | } 97 | 98 | const wordsCounter = document.getElementById('words_counter'); 99 | wordsCounter.innerHTML = 'Слов: ' + words; 100 | 101 | const lettersCounter = document.getElementById('letters_counter'); 102 | lettersCounter.innerHTML = 'Букв: ' + letters; 103 | 104 | const paragraphsCounter = document.getElementById('paragraphs_counter'); 105 | paragraphsCounter.innerHTML = 'Параграфов: ' + paragraphs.length; 106 | } 107 | }, 1000); 108 | } 109 | 110 | function addBlock(parent) { 111 | const curCounter = document.getElementById('counters_block'); 112 | if (curCounter) 113 | return; 114 | 115 | const countersContent = document.createElement('div'); 116 | countersContent.className = 'editor-cp-tab__content'; 117 | countersContent.id = 'counters_block' 118 | 119 | const counterMenu = document.createElement('div'); 120 | ['words_counter', 'letters_counter', 'paragraphs_counter'].forEach((id) => { 121 | const pElem = document.createElement('p'); 122 | pElem.id = id; 123 | counterMenu.appendChild(pElem); 124 | }); 125 | countersContent.appendChild(counterMenu); 126 | 127 | const countersDiv = document.createElement('div'); 128 | countersDiv.className = 'editor-cp-tab'; 129 | countersDiv.id = 'counter_div' 130 | 131 | countersDiv.addEventListener('click', () => { 132 | const counterBlock = document.getElementById('counter_div'); 133 | counterBlock.className = counterBlock.className === 'editor-cp-tab' 134 | ? 'editor-cp-tab editor-cp-tab--active' 135 | : 'editor-cp-tab'; 136 | }); 137 | 138 | const countersLabel = document.createElement('div'); 139 | countersLabel.className = 'editor-cp-tab__label'; 140 | 141 | const label = document.createElement('span'); 142 | label.innerText = 'Статистика'; 143 | countersLabel.appendChild(label); 144 | 145 | countersDiv.appendChild(countersLabel); 146 | countersDiv.appendChild(countersContent); 147 | 148 | parent.insertAdjacentElement('beforeend', countersDiv); 149 | } 150 | 151 | 152 | function calc(txt) { 153 | const words = countWords(txt); 154 | const letters = countLetters(txt); 155 | 156 | return { 157 | words, 158 | letters 159 | }; 160 | } 161 | 162 | function countWords(s) { 163 | // https://stackoverflow.com/questions/18679576/counting-words-in-string 164 | s = s.replace(/(^\s*)|(\s*$)/gi, '');//exclude start and end white-space 165 | s = s.replace(/[ ]{2,}/gi, ' ');//2 or more space to 1 166 | s = s.replace(/\n /, '\n'); // exclude newline with a start spacing 167 | return s.split(' ').filter(function (str) { 168 | return str != ''; 169 | }).length; 170 | } 171 | 172 | function countLetters(s) { 173 | // учитываем русские и английские символы 174 | return s.replace(/[^a-zA-Zа-яА-Я]/g, '').length; 175 | } -------------------------------------------------------------------------------- /black-list/black-list.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Separate blacklist 3 | // @version 1 4 | // @namespace https://github.com/Suvitruf/dtf-scripts 5 | // @description Раздельный ЧС для постов, репостов и комментариев 6 | // @author Apanasik aka Suvitruf 7 | // @updateURL https://github.com/Suvitruf/dtf-scripts/raw/master/black-list/black-list.meta.js 8 | // @downloadURL https://github.com/Suvitruf/dtf-scripts/raw/master/black-list/black-list.user.js 9 | // @include *://*.dtf.ru* 10 | // @include *://dtf.ru/* 11 | // @include *://*.tjournal.ru* 12 | // @include *://tjournal.ru/* 13 | // @include *://*.vc.ru* 14 | // @include *://vc.ru/* 15 | // @grant GM_addStyle 16 | // @grant GM.setValue 17 | // @grant GM.getValue 18 | // @grant unsafeWindow 19 | // ==/UserScript== 20 | 21 | GM_addStyle(` 22 | #black_list_icon_block { 23 | cursor: pointer; 24 | } 25 | 26 | .black_list_users { 27 | --image-size: 36px; 28 | --image-radius: 6px; 29 | --grid-gap: 16px; 30 | height: 420px; 31 | overflow: auto; 32 | } 33 | 34 | .black-list-item { 35 | display: -ms-flexbox; 36 | display: flex; 37 | -ms-flex-align: center; 38 | align-items: center; 39 | min-width: 0; 40 | position: relative; 41 | padding: 12px 0; 42 | } 43 | .black-list-item__main { 44 | -ms-flex: 1; 45 | flex: 1; 46 | display: -ms-flexbox; 47 | display: flex; 48 | -ms-flex-align: center; 49 | align-items: center; 50 | min-width: 0; 51 | } 52 | .black-list-item__image:not(:last-child) { 53 | margin-right: 12px; 54 | } 55 | 56 | .black-list-item__image { 57 | -ms-flex-negative: 0; 58 | flex-shrink: 0; 59 | width: var(--image-size); 60 | height: var(--image-size); 61 | background-color: #dedede; 62 | border-radius: var(--image-radius); 63 | -webkit-box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1); 64 | box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1); 65 | background-position: 50% 50%; 66 | background-repeat: no-repeat; 67 | background-size: cover; 68 | } 69 | 70 | .black-list-item__label-container { 71 | -ms-flex: 1; 72 | flex: 1; 73 | white-space: nowrap; 74 | overflow: hidden; 75 | text-overflow: ellipsis; 76 | } 77 | 78 | .black-list-item__label { 79 | font-size: 18px; 80 | line-height: 1.45em; 81 | font-weight: 500; 82 | } 83 | 84 | .black-list-item-checkbox { 85 | width: 20px; 86 | height: 20px; 87 | } 88 | 89 | .black-list-item-checkboxes, .black-list-item-checkboxes-header { 90 | width: 120px; 91 | display: flex; 92 | } 93 | 94 | .black-list-item-del { 95 | width: 30px; 96 | } 97 | 98 | .black-list-item-checkbox-header-item { 99 | width: 30px; 100 | height: 20px; 101 | padding: 5px; 102 | display: block; 103 | } 104 | .black-list-item-checkboxes-header { 105 | margin-right: 30px; 106 | } 107 | 108 | .black-list-item-checkboxes-and-del { 109 | display: flex; 110 | } 111 | 112 | .comment-black-listed { 113 | color: #595959; 114 | } 115 | `); 116 | 117 | const userRx = /\/u\/([0-9]*)/; 118 | 119 | function getApiUserUrl(id) { 120 | return `https://api.${document.location.host}/v1.9/user/${id}`; 121 | } 122 | 123 | function getDefaultUserState() { 124 | return { 125 | p: 1, 126 | r: 1, 127 | c: 1 128 | }; 129 | } 130 | 131 | 132 | async function getUserInfo(id) { 133 | try { 134 | const user = await fetch(getApiUserUrl(id)); 135 | 136 | return user.json(); 137 | } catch (e) { 138 | console.error('cant get user', e); 139 | 140 | return null; 141 | } 142 | } 143 | 144 | async function getBlackList() { 145 | return GM.getValue('dtfBlackListLivesMatter', {blackList: []}); 146 | } 147 | 148 | function setBlackList(list) { 149 | GM.setValue('dtfBlackListLivesMatter', list); 150 | } 151 | 152 | function checkPostBlockToHide(block, list, type) { 153 | const href = block.getAttribute('href'); 154 | 155 | const rx = userRx.exec(href); 156 | 157 | if (!rx || !rx.length) 158 | return; 159 | 160 | const blockedUser = list.blackList.find(u => u.id == rx[1]); 161 | if (!blockedUser || blockedUser.state[type] === 0) 162 | return; 163 | 164 | if (block.parentNode && block.parentNode.parentNode && block.parentNode.parentNode.parentNode && block.parentNode.parentNode.parentNode.parentNode) { 165 | block.parentNode.parentNode.parentNode.parentNode.style.display = 'none'; 166 | } 167 | } 168 | 169 | async function checkArticles(list) { 170 | const contentBlocks = document.querySelectorAll('a[class*="content-header-author"][class*="content-header__item"]'); 171 | for (const block of contentBlocks) { 172 | checkPostBlockToHide(block, list, 'p'); 173 | } 174 | } 175 | 176 | async function checkReposts(list) { 177 | const contentBlocks = document.querySelectorAll('a[class*="content-header-repost__name"]'); 178 | for (const block of contentBlocks) { 179 | checkPostBlockToHide(block, list, 'r'); 180 | } 181 | } 182 | 183 | async function checkComments(list) { 184 | const contentBlocks = document.querySelectorAll('a[class*="comment__avatar"]'); 185 | for (const block of contentBlocks) { 186 | const href = block.getAttribute('href'); 187 | 188 | const rx = userRx.exec(href); 189 | if (!rx || !rx.length) 190 | continue; 191 | 192 | const blockedUser = list.blackList.find(u => u.id == rx[1]); 193 | if (!blockedUser || blockedUser.state.c === 0) 194 | continue; 195 | 196 | if (block.parentNode && block.parentNode.parentNode) { 197 | block.parentNode.parentNode.innerHTML = '

Комментарий скрыт

'; 198 | } 199 | } 200 | } 201 | 202 | async function checkPosts() { 203 | const list = await getBlackList(); 204 | 205 | await checkArticles(list); 206 | await checkComments(list); 207 | await checkReposts(list); 208 | } 209 | 210 | function createBlackListedUsersHeader() { 211 | const row = document.createElement('div'); 212 | row.innerHTML = `
213 |
214 |
215 |
216 |
217 | П 218 | Р 219 | К 220 |
221 |
222 |
223 |
224 |
`; 225 | 226 | return row; 227 | } 228 | 229 | function onUserStateChanged(type, checked, userId) { 230 | getBlackList() 231 | .then(list => { 232 | const users = list.blackList.filter(u => u.id === userId); 233 | if (users.length) { 234 | users[0].state[type] = checked ? 1 : 0; 235 | } 236 | setBlackList(list); 237 | }); 238 | } 239 | 240 | function onUserDeleted(userId) { 241 | getBlackList() 242 | .then(list => { 243 | list.blackList = list.blackList.filter(u => u.id !== userId); 244 | setBlackList(list); 245 | 246 | const userBlock = document.getElementById(`black_list_item_${userId}`); 247 | if (userBlock) 248 | userBlock.remove(); 249 | }); 250 | } 251 | 252 | function createBlackListedUserRow(user) { 253 | const row = document.createElement('div'); 254 | row.innerHTML = `
255 | 256 |
257 |
${user.name} 258 |
259 |
260 |
261 |
262 | 263 | 264 | 265 |
266 |
267 | X 268 |
269 |
270 |
`; 271 | 272 | return row; 273 | } 274 | 275 | function createBlackListedUsersBlock() { 276 | const root = document.createElement('div'); 277 | root.innerHTML = ` 278 |
279 |
280 |
281 | 282 |
283 |
284 |
285 | `; 286 | 287 | return root; 288 | } 289 | 290 | async function addToBlackList(profile) { 291 | const list = await getBlackList(); 292 | const user = list.blackList.find(u => u.id === profile.id); 293 | if (user) { 294 | user.ava = profile.avatar_url; 295 | user.name = profile.name; 296 | 297 | } else { 298 | const u = { 299 | id: profile.id, 300 | ava: profile.avatar_url, 301 | name: profile.name, 302 | state: getDefaultUserState() 303 | }; 304 | list.blackList.push(u); 305 | 306 | const blackListContainer = document.getElementById('black_list_users'); 307 | blackListContainer.appendChild(createBlackListedUserRow(u, list)); 308 | } 309 | 310 | const inp = document.getElementById('search-user-input'); 311 | if (inp) 312 | inp.value = ''; 313 | 314 | setBlackList(list); 315 | } 316 | 317 | function tryToAddUserToBlockList(url) { 318 | const rx = userRx.exec(url); 319 | 320 | if (!rx || !rx.length) 321 | return; 322 | 323 | getUserInfo(rx[1]) 324 | .then(user => { 325 | if (user && user.result) { 326 | return addToBlackList(user.result); 327 | } 328 | }) 329 | .then(); 330 | } 331 | 332 | function createSearchUserBox() { 333 | const root = document.createElement('div'); 334 | root.innerHTML = ` 335 |
336 |
337 | 338 |
339 |
`; 340 | 341 | window.addEventListener('keydown', (event) => { 342 | const inp = document.getElementById('search-user-input'); 343 | 344 | if (event.code === 'Enter') { 345 | const text = inp.value; 346 | if (!text) 347 | return; 348 | 349 | tryToAddUserToBlockList(text); 350 | } 351 | }, true); 352 | 353 | return root; 354 | } 355 | 356 | function initBlackListPanel() { 357 | const root = document.createElement('div'); 358 | root.style.width = '100%'; 359 | root.style.height = 'calc(100vh)'; 360 | root.style.position = 'fixed'; 361 | root.style['z-index'] = '9999'; 362 | root.style.background = '#00000088'; 363 | root.style.display = 'none'; 364 | root.style.top = '0'; 365 | root.style.left = '0'; 366 | 367 | const panel = document.createElement('div'); 368 | panel.style.width = '500px'; 369 | panel.style.height = '500px'; 370 | panel.style['margin'] = '0 auto'; 371 | panel.style.background = '#fff'; 372 | panel.style.padding = '20px'; 373 | panel.style.position = 'fixed'; 374 | panel.style.top = '0'; 375 | panel.style.left = '0'; 376 | panel.style.right = '0'; 377 | panel.style.bottom = '0'; 378 | panel.addEventListener('click', function (e) { 379 | e.stopPropagation(); 380 | }); 381 | 382 | panel.appendChild(createSearchUserBox()); 383 | panel.appendChild(createBlackListedUsersBlock()); 384 | 385 | const bg = document.createElement('div'); 386 | bg.style.width = '100%'; 387 | bg.style.height = '100%'; 388 | bg.style['margin'] = '0 auto'; 389 | 390 | bg.addEventListener('click', function () { 391 | console.log('wtf'); 392 | root.style.display = 'none'; 393 | }); 394 | root.appendChild(bg); 395 | root.appendChild(panel); 396 | 397 | return root; 398 | } 399 | 400 | function createBlackListPanelAndIcon() { 401 | const panel = initBlackListPanel(); 402 | 403 | const root = document.createElement('div'); 404 | root.id = 'black_list_icon_block'; 405 | root.style.display = 'flex'; 406 | root.style['align-items'] = 'center'; 407 | 408 | const text = document.createElement('p'); 409 | text.id = 'black_list_text'; 410 | text.innerHTML = 'BL'; 411 | 412 | text.addEventListener('click', function () { 413 | panel.style.display = '';//panel.style.display ? '' : 'none'; 414 | }); 415 | 416 | root.appendChild(panel); 417 | root.appendChild(text); 418 | 419 | return root; 420 | } 421 | 422 | async function fillBlockedList() { 423 | const list = await getBlackList(); 424 | const blackList = list.blackList; 425 | 426 | const blackListContainer = document.getElementById('black_list_users'); 427 | 428 | blackListContainer.appendChild(createBlackListedUsersHeader()); 429 | for (const user of blackList) { 430 | blackListContainer.appendChild(createBlackListedUserRow(user, blackList)); 431 | } 432 | } 433 | 434 | async function addSettingsButton() { 435 | let settingsButton = document.querySelectorAll('div[class*="site-header-messenger"]'); 436 | while (!settingsButton || !settingsButton.length) { 437 | await new Promise(resolve => { 438 | setTimeout(resolve, 200); 439 | }); 440 | settingsButton = document.querySelectorAll('div[class*="site-header-messenger"]'); 441 | } 442 | 443 | const chatIcon = settingsButton[0]; 444 | const headerBLock = chatIcon.parentNode; 445 | const blackListIcon = createBlackListPanelAndIcon(); 446 | fillBlockedList(); 447 | 448 | headerBLock.insertBefore(blackListIcon, chatIcon); 449 | } 450 | 451 | async function checkBlackList() { 452 | checkPosts(); 453 | setInterval(() => { 454 | new Promise(checkPosts) 455 | .catch(er => { 456 | console.error(er); 457 | }); 458 | }, 5000); 459 | } 460 | 461 | addSettingsButton() 462 | .catch(er => { 463 | console.error(er); 464 | }); 465 | 466 | checkBlackList() 467 | .catch(er => { 468 | console.error(er); 469 | }); 470 | 471 | if (!unsafeWindow.onUserDeleted) { 472 | unsafeWindow.onUserDeleted = onUserDeleted; 473 | } 474 | 475 | if (!unsafeWindow.onUserStateChanged) { 476 | unsafeWindow.onUserStateChanged = onUserStateChanged; 477 | } 478 | 479 | // слишком часто прокает эта херота 480 | // в идеале нужно, конечно, setInterval выпилить, а это допилить и фильтры добавить 481 | // а то пока что оно вешает браузер, т. к. много раз DOM перебирает 482 | // addEventListener('DOMContentLoaded', function () { 483 | // console.log('DOMContentLoaded'); 484 | // checkPosts(); 485 | // }); 486 | // 487 | // addEventListener('DOMNodeInserted', function () { 488 | // console.log('DOMNodeInserted'); 489 | // checkPosts(); 490 | // }); 491 | --------------------------------------------------------------------------------