// ==UserScript== // @name FriendsKit // @namespace https://github.com/yuzulabo // @version 1.3.2 // @description friends.nico の独自機能を再現するユーザスクリプト // @author nzws // @match https://knzk.me/* // @match https://best-friends.chat/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_notification // @grant unsafeWindow // @connect friendskit.nzws.me // @connect media.knzk.me // @connect media.best-friends.chat // @require https://unpkg.com/blob-util/dist/blob-util.min.js // @downloadURL https://update.greasyfork.cloud/scripts/381132/FriendsKit.user.js // @updateURL https://update.greasyfork.cloud/scripts/381132/FriendsKit.meta.js // ==/UserScript== const version = '1.3.2'; const s = localStorage.friendskit; const F = { conf: s ? JSON.parse(s) : { keyword: [] }, imgcache: {}, iconcache: {} }; 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 nico_ms_shorten_regexp = new RegExp('(sm|nm|im|sg|mg|bk|lv|co|ch|ar|ap|jk|nw|so|l\/|dic\/|user\/|mylist\/)([0-9]+)', 'gmi'); const nico_ms_watch_regexp = new RegExp('(watch\/)([0-9]+)', 'gmi'); const keyword_escaped = []; F.conf.keyword.forEach(value => { ['\\', '^', '$', '*', '+', '?', '.', '(', ')', '|', '{', '}', '[', ']'].forEach(meta => { value = value.replace(meta, '\\' + meta); }); keyword_escaped.push(value); }); const keyword_regexp = new RegExp(`(${keyword_escaped.join('|')})`, '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')); if (p === '/web/getting-started' && !document.querySelector('.friendskit-cp-btn')) { const settingLi = document.createElement('li'); settingLi.innerHTML = ` · FriendsKit CP (v${version})`; document.querySelector('.getting-started__footer ul').appendChild(settingLi); settingLi.addEventListener('click', openCP); } } 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 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 origin_domain = '@' + (origin_acct.indexOf('@') !== -1 ? origin_acct.split('@')[1] : location.hostname); replaceTool(status, origin_domain); } } function replaceTool(status, domain) { if (status.hasChildNodes()) { for (let node of status.childNodes) { replaceTool(node, domain); } } else { if (status.nodeName !== '#text') return; let is_replaced = false; const html = document.createElement('span'); html.innerHTML = status.data; if (F.conf.keyword.length > 0 && F.conf.keyword[0]) { html.innerHTML = html.innerHTML.replace(keyword_regexp, `$1`); } if (!findParentByTagName(status, 'A')) { html.innerHTML = html.innerHTML.replace(nico_ms_shorten_regexp, `$1$2`) .replace(nico_ms_watch_regexp, `$1$2`); } if (html.innerHTML !== status.data) { status.parentNode.replaceChild(html, status); is_replaced = true; } const ue_found = html.innerHTML.match(user_emoji_regexp); 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'), `:_${acct}:`); }); if (!is_replaced) { status.parentNode.replaceChild(html, status); } } } } function findParentByTagName(element, tagName, max = 3) { if (max < 1 || element.tagName === tagName) { return element.tagName === tagName; } else if (element.parentNode) { return findParentByTagName(element.parentNode, tagName, max - 1); } else { return false; } } function openCP() { const div = document.createElement('div'); div.className = 'friendskit-cp'; div.innerHTML = `
FriendsKit CP
キーワードハイライト設定
カンマ(,)区切りで指定
お気に入りアイコン設定

* アイコン設定で複数の設定がされている時、この設定で一番上の物が優先されます。

文字にする (1~2文字程度):
画像にする (画像URLを指定):
お気に入りアイコン拡大
細かなやつ
FriendsKit v${version}

リリースノート(更新履歴) を見る

GitHub: yuzulabo/FriendsKit
Greasy Fork: 381132-friendskit

`; document.body.appendChild(div); document.querySelector('.app-holder').classList.add('friendskit-disable'); const btns = document.querySelectorAll(".fcp-clickable"); for (let btn of btns) { btn.addEventListener('click', (e) => CPOpr(e)); } } function CPOpr(e) { const mode = e.target.dataset.fcp; if (!mode) return; if (mode === 'save') { const keyword = document.getElementById('friendskit-keyword').value; const keywords = keyword ? Array.from(new Set(keyword.split(','))) : []; const newConf = { keyword: keywords, fav_icon_default_force: document.getElementById('fav_icon_default_force').checked, fav_icon_char: document.getElementById('fav_icon_char').value, fav_icon: document.getElementById('fav_icon').value, fav_icon_gray: document.getElementById('fav_icon_gray').value, no_fav_icon_big: document.getElementById('no_fav_icon_big').checked, }; Object.assign(F.conf, newConf); save(); alert('保存しました。再読み込みします...'); location.reload(); } else if (mode === 'close') { if (!confirm('変更は破棄されますがよろしいですか?')) return; const element = document.querySelector('.friendskit-cp'); element.parentNode.removeChild(element); document.querySelector('.app-holder').classList.remove('friendskit-disable'); } else if (mode === 'import-settings') { const code = prompt('エクスポート時に出力したコードを入力してください:'); if (!code) return; if (friendskit.importSettings(code)) { alert('インポートしました。再読み込みします...'); location.reload(); } else { alert('このコードは壊れています。インポートできません。'); } } else if (mode === 'export-settings') { friendskit.exportSettings('CP'); alert('設定のエクスポートをクリップボードにコピーしました。'); } else if (mode === 'reset-settings') { if (!confirm('設定をリセットします!よろしいですか?')) return; if (!confirm('まじで?')) return; if (!confirm('まじか...')) return; if (friendskit.resetSettings()) { location.reload(); } } } 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: (type) => { if (type === 'CP') { GM_setClipboard(localStorage.friendskit); } else { 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 false; } localStorage.friendskit = data; console.log('[FriendsKit]', 'Done✨'); return true; } }; 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 + '&domain=' + location.hostname, 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 = ` .friendskit-cp { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #e6ebf0; color: #000; padding: 20px; border-radius: 5px; min-width: 55%; max-width: 95%; min-height: 50%; max-height: 65%; overflow-y: scroll; } .friendskit-disable { filter: blur(4px); pointer-events: none; } .friendskit-cp textarea, .friendskit-cp .input-text, .friendskit-cp label { display: block; width: 100%; margin: 10px 0; } .fcp-clickable { cursor: pointer; } .h1 { font-size: 2.4rem; } .h2 { font-size: 1.7rem; } .h3 { font-size: 1.3rem; } .h1, .h2, .h3 { margin: 1rem 0; padding-bottom: 0.3rem; font-weight: 500; line-height: 1.2; border-bottom: 1px solid #c0cdd9; } .close { float: right; color: #000; text-shadow: 0 1px 0 #fff; opacity: .5; } .button.danger { background: #df405a; } `; window.onload = async () => { 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(); }; if (!F.conf.fav_icon_default_force) { const i = await getImage(F.conf.fav_icon ? F.conf.fav_icon : 'https://i.imgur.com/iZQJgSW.png'); const ig = await getImage(F.conf.fav_icon_gray ? F.conf.fav_icon_gray : 'https://i.imgur.com/ninYNIi.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); console.log(`%c..: FriendsKit v${version} :..`, ' background: black;font-size: large;color: orange'); if (localStorage.friendskit_version !== version) { GM_notification({ title: `FriendsKit v${version}にアップデートしました。`, text: `クリックしてリリースノート(更新履歴)を表示`, highlight: true, onclick: () => { window.open('https://github.com/yuzulabo/FriendsKit/releases'); } }); } localStorage.friendskit_version = version; };