// ==UserScript==
// @name FriendsKit
// @namespace https://github.com/yuzulabo
// @version 0.1.2
// @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');
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(async node => {
if (!node.tagName) return;
const status = node.querySelector('.status__content');
if (!status) return;
const ue_found = status.innerHTML.match(user_emoji_regexp);
if (ue_found) {
let domain = '';
const status_display_name = node.querySelector('.status__display-name');
if (status_display_name) {
const origin_acct = status_display_name.title;
if (origin_acct.indexOf('@') !== -1) {
domain = '@' + origin_acct.split('@')[1];
} else {
domain = '@' + location.hostname;
}
} else {
const origin_acct = node.querySelector('.display-name__account').textContent.slice(1);
domain = '@' + origin_acct.split('@')[1];
}
ue_found.forEach(async data => {
let acct = data.slice(2).slice(0, -1);
if (acct.indexOf('@') === -1) acct += domain;
const image = await getIconUrl(acct);
const regExp = new RegExp(data, 'gm');
status.innerHTML = status.innerHTML.replace(regExp, `
`);
});
}
F.regExps.forEach(regexp => replaceHighlight(regexp, status));
});
});
});
function replaceHighlight(regexp, status) {
if (status.hasChildNodes()) {
for (let node of status.childNodes) {
replaceHighlight(regexp, node);
}
} else {
if (status.nodeName === '#text') {
const html = document.createElement('span');
html.innerHTML = status.data.replace(regexp.regexp, `${regexp.word}`);
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);
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.indexOf('@ピザ') !== -1) {
window.open('https://www.google.com/search?q=近くのピザ屋さん');
}
}
let css = ``;
window.onload = async () => {
if (F.conf.fav_icon && F.conf.fav_icon_gray) {
const i = await getImage(F.conf.fav_icon);
const ig = await getImage(F.conf.fav_icon_gray);
css += `
.fa-star {
background-image: url('${ig}');
width: 16px;
height: 16px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
}
.active .fa-star, .notification__message .fa-star {
background-image: url('${i}');
}
.fa-star:before {
content: '';
}
`;
}
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;
}
.status .fa-star {
width: 40px;
height: 40px;
font-size: 2em;
}
`;
}
GM_addStyle(css);
const mainElem = document.getElementById('mastodon');
if (!mainElem) return;
observer.observe(mainElem, { childList: true, subtree: true });
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();
};
};