FriendsKit

friends.nico の独自機能を再現するユーザスクリプト

当前为 2019-03-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            FriendsKit
// @namespace       https://github.com/yuzulabo
// @version         1.0.1
// @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
// ==/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(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];
                }

                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'), `<img src="${image}" class="emojione"/>`);
                });
            }

            F.regExps.forEach(regexp => {
                html.innerHTML = html.innerHTML.replace(regexp.regexp, `<span style='color: orange'>${regexp.word}</span>`);
            });
            html.innerHTML = html.innerHTML.replace(shorten_regexp, `<a href="http://nico.ms/$1$2" target="_blank" rel=”nofollow”>$1$2</a>`);

            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.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;
}

.status .fa-star {
width: 40px;
height: 40px;
font-size: 2em;
}
`;
    }

    GM_addStyle(css);

    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();
    };
};