Skip to content

Instantly share code, notes, and snippets.

@unarist
Last active March 12, 2023 05:23
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save unarist/3134f59953569a4a8ea692185b94eaeb to your computer and use it in GitHub Desktop.
Save unarist/3134f59953569a4a8ea692185b94eaeb to your computer and use it in GitHub Desktop.
Mastodon - SSTP over HTTP で喋らせるボタンを生やすやつ
// ==UserScript==
// @name Mastodon - SSTP over HTTP で喋らせるボタンを生やすやつ
// @namespace https://github.com/unarist/
// @version 0.1
// @author unarist
// @match https://mstdn.maud.io/*
// @match https://ukadon.shillest.net/*
// @grant GM.xmlHttpRequest
// @downloadURL https://gist.github.com/unarist/3134f59953569a4a8ea692185b94eaeb/raw/mastodon-sstp-over-http.user.js
// @run-at document-idle
// @noframes
// ==/UserScript==
/*
使い方
* SSP 2.6.33 以上を起動しておく
* このUserScriptを入れると返信ボタンとかに並んでうかどんアイコンが出てるので、それを押す
* 「127.0.0.1に接続していいか?本当か?」みたいに言われたら、このドメインを許可とかする(SSPへの接続用)
* その投稿の内容を SSTP over HTTP で送信して、ゴーストが喋る
よくある質問
* SSPはCORS全許可でしょ?なんでGM_XHRがいるの? → Mastodon側が持ってるCSPでブロックされるので…
* うかどん以外の一部鯖で使ったら127.0.0.1以外にアクセス許可を求められるのはなんで? → 画像を別オリジンに置いてる鯖はそっちも許可がいるので…
* @connect 127.0.0.1 書いとけばよくない? → それだけ書くと鯖ごとの別オリジンが聞かれもしないし、 @connect * は心理的に微妙だったので…
*/
(function() {
'use strict';
const tag = (name, props = {}, children = []) => {
const e = Object.assign(document.createElement(name), props);
if (typeof props.style === "object") Object.assign(e.style, props.style);
(children.forEach ? children : [children]).forEach(c => e.appendChild(c));
return e;
};
const fetchBlob = url => new Promise((res,rej) => GM.xmlHttpRequest({
method:'GET',
url,
responseType:"blob",
onload:res,
onerror:rej,
})).then(x=>x.response);
const postSSTP = body => new Promise((res,rej) => GM.xmlHttpRequest({
method:'POST',
url:'http://127.0.0.1:9801/api/sstp/v1',
headers: {"Content-Type": "text/plain"},
data: body,
responseType:"text",
onload:res,
onerror:rej,
})).then(x=>x.response)
const getDataURL = async (url, maxw, maxh) => {
const img = await createImageBitmap(await fetchBlob(url));
const scale = Math.min(1, maxw / img.width, maxh / img.height);
const canvas = document.createElement('canvas');
canvas.width = img.width * scale;
canvas.height = img.height * scale;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL();
}
const onClick = async e => {
const root = e.target.closest('.status__wrapper');
const avatar_url = root.querySelector('.account__avatar img').src;
const display_name = root.querySelector('.display-name__html').textContent.trim();
const acct = root.querySelector('.display-name__account').textContent.trim();
const body = root.ariaLabel.replace(new RegExp(`^[^,]+, (.*), [^,]+, ${acct.slice(1)}(, [^,]+)?$`, "s"), "$1").trim().replace(/\n/g, "\\n");
const media_urls = [...root.querySelectorAll('.media-gallery__item-thumbnail img')].map(x => x.src);
let script = "";
script += `\\![set,balloonwait,0]\\0\\_b["${await getDataURL(avatar_url, 32, 32)}",inline,--option=use_self_alpha]\\_l[@4,]${display_name}\\n\\_l[@36,]${acct}\\_l[0,36]`;
script += `\\![set,balloonwait]${body}`;
if (media_urls) {
script += "\\n" + (await Promise.all(media_urls.map(async url => `\\_b["${await getDataURL(url, 240, 80)}",inline,--option=use_self_alpha]`))).join(" ");
}
script += "\\e";
postSSTP(`NOTIFY SSTP/1.1
Sender: ぶらうざのゆーざーすくりぷと
Script: ${script}
Charset: UTF-8`).then(console.debug);
};
new MutationObserver(() => {
for (const el of document.querySelectorAll('.status__action-bar:not(.__ukabutton)')) {
el.classList.add('__ukabutton');
el.append(tag('button', {
className: 'icon-button',
style: "width: 18px; height: 18px; background: no-repeat center/18px url(https://ukadon.shillest.net/favicon.ico); opacity: 0.5;",
onclick: onClick
}));
}
}).observe(document.body, {childList: 1, subtree:1});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment