// ==UserScript== // @name FriendsKit // @namespace https://github.com/yuzulabo // @version 1.0.4 // @description friends.nico の独自機能を再現するユーザスクリプト // @author nzws // @match https://knzk.me/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect * // @require https://unpkg.com/blob-util/dist/blob-util.min.js // @downloadURL none // ==/UserScript== const s = localStorage.friendskit; const F = { conf: s ? JSON.parse(s) : { keyword: [] }, imgcache: {}, iconcache: {}, regExps: [], }; const api = F.conf.api_server ? F.conf.api_server : 'https://friendskit.nzws.me/api/'; const user_emoji_regexp = new RegExp(':(_|@)([A-Za-z0-9_@.]+):', 'gm'); const shorten_regexp = new RegExp('(sm|nm|im|sg|mg|bk|lv|co|ch|ar|ap|jk|nw|l\/|dic\/|user\/|mylist\/)([0-9]+)', 'gm'); F.conf.keyword.forEach(word => { F.regExps.push({ word: word, regexp: new RegExp(word, 'gim') }); }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach(node => runner(node)); }); }); function watcher() { const p = location.pathname; if (F.path !== p) { runner(document.querySelector('.column:last-child')); } F.path = p; } function runner(node) { if (!node.tagName) return; const statusAll = node.querySelectorAll('.status__content'); if (!statusAll[0]) return; for (let status of statusAll) { const ue_found = status.innerHTML.match(user_emoji_regexp); if (ue_found) { const display_name_account = status.parentNode.querySelector('.display-name__account'); const status_display_name = status.parentNode.querySelector('.status__display-name'); const origin_acct = display_name_account ? display_name_account.textContent.slice(1) : status_display_name.title; const domain = '@' + (origin_acct.indexOf('@') !== -1 ? origin_acct.split('@')[1] : location.hostname); replaceTool(status, ue_found, domain); } else { replaceTool(status); } } } function replaceTool(status, ue_found, domain) { if (status.hasChildNodes()) { for (let node of status.childNodes) { replaceTool(node, ue_found, domain); } } else { if (status.nodeName === '#text') { const html = document.createElement('span'); html.innerHTML = status.data; if (ue_found) { ue_found.forEach(async data => { let acct = data.slice(2).slice(0, -1); if (acct.indexOf('@') === -1) acct += domain; const image = await getIconUrl(acct); html.innerHTML = html.innerHTML.replace(new RegExp(data, 'gm'), ``); }); } F.regExps.forEach(regexp => { html.innerHTML = html.innerHTML.replace(regexp.regexp, `${regexp.word}`); }); html.innerHTML = html.innerHTML.replace(shorten_regexp, `$1$2`); if (html.innerHTML !== status.data) status.parentNode.replaceChild(html, status); } } } const friendskit = { keyword: { add: (word) => { const key = F.conf.keyword.indexOf(word); if (key !== -1) { console.warn('[FriendsKit]', 'このワードは追加済みです'); return; } F.conf.keyword.push(word); save(); console.log('[FriendsKit]', 'Done✨'); }, remove: (word) => { const key = F.conf.keyword.indexOf(word); if (key === -1) { console.warn('[FriendsKit]', 'このワードは存在しません'); return; } delete F.conf.keyword[key]; save(); console.log('[FriendsKit]', 'Done✨'); }, list: () => { console.log('[FriendsKit]', F.conf.keyword); }, reset: () => { F.conf.keyword = []; save(); console.log('[FriendsKit]', 'Done✨'); } }, changeSettings: (name, value) => { F.conf[name] = value ? value : null; save(); console.log('[FriendsKit]', 'Done✨'); }, exportSettings: () => { GM_setClipboard('friendskit.importSettings(`' + localStorage.friendskit + '`)'); console.log('[FriendsKit]', 'Done✨\nクリップボードにコピーしたコードをインポートしたいページの Console にそのまま打ち込んでください。'); }, resetSettings: () => { delete localStorage.friendskit; return !localStorage.friendskit; }, importSettings: (data) => { try { JSON.parse(data); } catch(e) { console.warn('[FriendsKit]', 'このデータは壊れています', e); return; } localStorage.friendskit = data; console.log('[FriendsKit]', 'Done✨'); } }; exportFunction(friendskit, unsafeWindow, {defineAs: 'friendskit' }); async function getImage(url) { return new Promise(resolve => { if (F.imgcache[url]) { resolve(F.imgcache[url]); return; } blobUtil.imgSrcToDataURL(url, 'image/png', 'Anonymous').then(function (dataurl) { F.imgcache[url] = dataurl; resolve(F.imgcache[url]); }).catch(function (err) { console.warn('[FriendsKit]', '画像取得に失敗', url); }); }); } async function getIconUrl(acct) { return new Promise(resolve => { if (F.iconcache[acct]) { resolve(F.iconcache[acct]); return; } GM_xmlhttpRequest({ method: 'POST', responseType: 'json', url: api + 'get_icon.php?acct=' + acct, onerror: () => { console.warn('[FriendsKit]', 'json取得に失敗', acct); return; }, onload: (response) => { if (response.status !== 200) { console.warn('[FriendsKit]', `json取得に失敗 ${response.status}`, acct); return; } if (response.response.error) { console.warn('[FriendsKit]', response.response.error, acct); return; } F.iconcache[acct] = response.response.url; resolve(F.iconcache[acct]); } }); }); } function save() { const data = JSON.stringify(F.conf); localStorage.friendskit = data; } function at_pizza() { const textarea = document.querySelector('.autosuggest-textarea__textarea'); if (!textarea) return; if (textarea.value.match(/[@|@]ピザ/)) { window.open('https://www.google.com/search?q=近くのピザ屋さん'); } if (textarea.value.match(/[@|@]ハローワーク/)) { window.open('https://www.hellowork.go.jp/'); } } const mainElem = document.getElementById('mastodon'); if (!mainElem) return; observer.observe(mainElem, { childList: true, subtree: true }); let css = ``; window.onload = async () => { if (!F.conf.fav_icon_default_force) { const i = F.conf.fav_icon ? await getImage(F.conf.fav_icon) : 'https://media.knzk.me/media_attachments/files/004/510/885/original/caf4ce0b3e7bd6d3.png'; const ig = F.conf.fav_icon_gray ? await getImage(F.conf.fav_icon_gray) : 'https://media.knzk.me/media_attachments/files/004/510/887/original/bfb5e4222a3423fd.png'; const char = F.conf.fav_icon_char ? F.conf.fav_icon_char : null; css += ` .fa-star { background-image: ${char || !ig ? `none` : `url('${ig}')`}; width: 16px; height: 16px; background-size: cover; background-repeat: no-repeat; background-position: center center; } .fa-star:before { content: '${char ? char : (ig ? '' : '\\f005')}'; } .active .fa-star, .notification__message .fa-star { background-image: ${char || !i ? `none` : `url('${i}')`}; } .active .fa-star:before, .notification__message .fa-star:before { content: '${char ? char : (i ? '' : '\\f005')}'; } `; if (i && !char) { css += ` .active .fa-star, .notification__message .fa-star { background-image: url('${i}'); } `; } } if (!F.conf.no_fav_icon_big) { css += ` .status__info, .status__content { margin-right: 40px; } .status button.star-icon { position: absolute; top: 20px; right: 10px; z-index: 999999; } .status .fa-star { width: 40px; height: 40px; font-size: 2em; } `; } GM_addStyle(css); setInterval(watcher, 1000); document.querySelector('.compose-form__publish-button-wrapper button').addEventListener('click', at_pizza, false); document.querySelector('.autosuggest-textarea__textarea').onkeydown = (e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) at_pizza(); }; };