Misskey Remote Follow Helper

Remote follow the user of the current page on your Misskey instance via a Tampermonkey menu item.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Misskey Remote Follow Helper
// @namespace    https://controlnet.space/
// @version      0.1
// @description  Remote follow the user of the current page on your Misskey instance via a Tampermonkey menu item.
// @author       ControlNet
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      *
// @license      agpl-3.0
// ==/UserScript==

(function () {
    'use strict';

    /******************************************************************
     * CONFIG STORAGE (user input via menu)
     ******************************************************************/
    const KEY_INSTANCE = 'mrh_instance_host';
    const KEY_TOKEN    = 'mrh_api_token';

    function getInstance() {
        return GM_getValue(KEY_INSTANCE, '');
    }

    function getToken() {
        return GM_getValue(KEY_TOKEN, '');
    }

    function setInstance() {
        const current = getInstance() || '';
        const input = prompt(
            'Enter your Misskey instance URL (required, e.g. https://misskey.io):',
            current
        );
        if (input !== null) {
            const trimmed = input.trim().replace(/\/$/, ''); // remove trailing slash
            if (trimmed) {
                GM_setValue(KEY_INSTANCE, trimmed);
                alert('[Misskey Remote Follow]\nInstance saved:\n' + trimmed);
            } else {
                alert('[Misskey Remote Follow]\nInstance not changed (empty).');
            }
        }
    }

    function setToken() {
        const current = getToken() || '';
        const input = prompt(
            'Enter your Misskey API token (required; stored locally in Tampermonkey):',
            current
        );
        if (input !== null) {
            const trimmed = input.trim();
            if (trimmed) {
                GM_setValue(KEY_TOKEN, trimmed);
                alert('[Misskey Remote Follow]\nAPI token saved.');
            } else {
                alert('[Misskey Remote Follow]\nToken not changed (empty).');
            }
        }
    }

    function ensureConfig() {
        const inst = (getInstance() || '').trim();
        const tok  = (getToken() || '').trim();

        if (!inst && !tok) {
            throw new Error(
                '[Misskey Remote Follow] Misskey instance and API token are not set.\n' +
                'Use Tampermonkey menu:\n' +
                '  - "Set Misskey instance"\n' +
                '  - "Set Misskey API token"'
            );
        }
        if (!inst) {
            throw new Error(
                '[Misskey Remote Follow] Misskey instance is not set.\n' +
                'Use Tampermonkey menu: "Set Misskey instance".'
            );
        }
        if (!tok) {
            throw new Error(
                '[Misskey Remote Follow] Misskey API token is not set.\n' +
                'Use Tampermonkey menu: "Set Misskey API token".'
            );
        }
        return true;
    }

    /******************************************************************
     * UTIL: Misskey API wrapper
     ******************************************************************/
    function misskeyApi(path, payload) {
        const HOME_INSTANCE = getInstance().trim();
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: HOME_INSTANCE.replace(/\/$/, '') + path,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify(payload),
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        try {
                            const json = JSON.parse(res.responseText);
                            resolve(json);
                        } catch (e) {
                            reject(new Error('Failed to parse JSON: ' + e.message));
                        }
                    } else {
                        reject(new Error('HTTP ' + res.status + ' ' + res.responseText));
                    }
                },
                onerror: (err) => {
                    reject(new Error('Network error: ' + JSON.stringify(err)));
                }
            });
        });
    }

    /******************************************************************
     * UTIL: Extract remote user from URL
     *
     * Format 1: https://${remote_host}/@${remote_username}
     *   - Example: https://remote.host/@user
     *   => host = remote.host (from URL), username = user
     *
     * Format 2: https://xxx.yyy/@${remote_username}@${remote_host}
     *   - Example: https://mastodon.example/@[email protected]
     *   => username = alice, host = social.controlnet.space
     ******************************************************************/
    function extractFromUrl(urlString) {
        let url;
        try {
            url = new URL(urlString);
        } catch (e) {
            return null;
        }

        const path = url.pathname;

        // Match /@something
        const m = path.match(/^\/@([^\/]+)$/);
        if (!m) {
            // OPTIONAL: /users/username support (delete if unwanted)
            const userMatch = path.match(/^\/users\/([^\/]+)$/);
            if (userMatch) {
                return {
                    host: url.hostname,
                    username: userMatch[1],
                    source: 'url-users'
                };
            }
            return null;
        }

        const handle = m[1]; // "user" OR "user@host"

        // If no inner '@', it's format 1: https://${remote_host}/@${remote_username}
        if (!handle.includes('@')) {
            return {
                host: url.hostname,
                username: handle,
                source: 'url-format1'
            };
        }

        // Format 2: strictly username first, host second
        // handle = "${remote_username}@${remote_host}"
        const parts = handle.split('@').filter(Boolean);
        if (parts.length < 2) {
            return null;
        }

        const username = parts[0];
        const host = parts.slice(1).join('@'); // mostly parts[1], but join for safety

        return {
            host,
            username,
            source: 'url-format2'
        };
    }

    function getRemoteUserInfo() {
        return extractFromUrl(location.href);
    }

    /******************************************************************
     * MAIN: remote follow
     ******************************************************************/
    async function remoteFollowCurrentUser() {
        try {
            ensureConfig();
        } catch (err) {
            alert(err.message);
            console.error(err);
            return;
        }

        const MISSKEY_TOKEN = getToken().trim();
        const info = getRemoteUserInfo();

        if (!info) {
            const errMsg =
                '[Misskey Remote Follow]\n' +
                'Could not detect a user from this page.\n\n' +
                'Supported URL formats:\n' +
                '  1) https://${remote_host}/@${remote_username}\n' +
                '  2) https://xxx.yyy/@${remote_username}@${remote_host}\n';
            alert(errMsg);
            console.error(new Error(errMsg));
            return;
        }

        const remoteHandle = `${info.username}@${info.host}`;
        console.log('[Misskey Remote Follow] Parsed remote user:', info);

        try {
            // 1) Resolve / ensure user exists on your instance
            const user = await misskeyApi('/api/users/show', {
                i: MISSKEY_TOKEN,
                username: info.username,
                host: info.host
            });

            if (!user || !user.id) {
                throw new Error('users/show did not return a user id');
            }

            // 2) Send follow
            const followRes = await misskeyApi('/api/following/create', {
                i: MISSKEY_TOKEN,
                userId: user.id
            });

            console.log('[Misskey Remote Follow] follow result:', followRes);
            alert(
                `[Misskey Remote Follow]\n` +
                `Sent follow request to ${remoteHandle}.`
            );
        } catch (err) {
            console.error('[Misskey Remote Follow] error:', err);
            alert(
                '[Misskey Remote Follow]\nFailed to follow ' +
                remoteHandle + ':\n' + err.message
            );
        }
    }

    /******************************************************************
     * Tampermonkey menu
     ******************************************************************/
    GM_registerMenuCommand('Remote follow on Misskey', remoteFollowCurrentUser);
    GM_registerMenuCommand('Set Misskey instance', setInstance);
    GM_registerMenuCommand('Set Misskey API token', setToken);
})();