Skip to content

Instantly share code, notes, and snippets.

@nikolat
Forked from unarist/mastodon-sstp-over-http.user.js
Last active February 25, 2024 06:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nikolat/c945aa7dce03dd86a43c947a32fb83b9 to your computer and use it in GitHub Desktop.
Save nikolat/c945aa7dce03dd86a43c947a32fb83b9 to your computer and use it in GitHub Desktop.
SSTP over HTTP で喋らせるボタンを生やすやつ
// ==UserScript==
// @name Bluesky - SSTP over HTTP で喋らせるボタンを生やすやつ
// @namespace https://github.com/nikolat/
// @version 0.1
// @contributor unarist https://gist.github.com/unarist/3134f59953569a4a8ea692185b94eaeb
// @author nikolat https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9
// @match https://tokimeki.blue/*
// @grant GM.xmlHttpRequest
// @homepage https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9
// @downloadURL https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9/raw/4d488784ae5c9a38ab4957a21239b7668da46ed5/bluesky-sstp-over-http.user.js
// @run-at document-idle
// @noframes
// @license Unknown (as-is)
// ==/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) => {
if (!url) {
return "";
}
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 => {
let root;
let avatar_url;
let display_name;
let acct;
let body;
let media_urls;
switch (document.domain) {
//TOKIMEKI Bluesky
case 'tokimeki.blue':
root = e.target.closest('.timeline__item');
avatar_url = root.querySelector('.timeline__image img') ? root.querySelector('.timeline__image img').src : '';
display_name = root.querySelector('.timeline__user').textContent.trim();
acct = '';
body = root.querySelector('.timeline__text').textContent.trim().replace(/\n/g, "\\n");
media_urls = [...root.querySelectorAll('.timeline-images-wrap img')].map(x => x.src);
break;
default:
break;
}
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(() => {
let q;
switch (document.domain) {
//TOKIMEKI Bluesky
case 'tokimeki.blue':
q = '.timeline-reaction:not(.__ukabutton)';
break;
default:
break;
}
for (const el of document.querySelectorAll(q)) {
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});
})();
// ==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.getAttribute('aria-label').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});
})();
// ==UserScript==
// @name Nostr - SSTP over HTTP で喋らせるボタンを生やすやつ
// @namespace https://github.com/nikolat/
// @version 0.1
// @contributor unarist https://gist.github.com/unarist/3134f59953569a4a8ea692185b94eaeb
// @author nikolat https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9
// @match https://nostter.app/*
// @match https://rabbit.syusui.net/*
// @grant GM.xmlHttpRequest
// @homepage https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9
// @downloadURL https://gist.github.com/nikolat/c945aa7dce03dd86a43c947a32fb83b9/raw/4d488784ae5c9a38ab4957a21239b7668da46ed5/nostr-sstp-over-http.user.js
// @run-at document-idle
// @noframes
// @license Unknown (as-is)
// ==/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) => {
if (!url) {
return "";
}
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 => {
let root;
let avatar_url;
let display_name;
let acct;
let body;
let media_urls;
switch (document.domain) {
//nostter
case 'nostter.app':
root = e.target.closest('main > div.timeline > ul.timeline > li > article');
avatar_url = root.querySelector('div > a > img.picture').src;
display_name = root.querySelector('.display_name').textContent.trim();
acct = root.querySelector('.name').textContent.trim();
body = root.querySelector('.content').textContent.trim().replace(/\n/g, "\\n");
media_urls = [...root.querySelectorAll('.media img')].map(x => x.src);
break;
//Rabbit
case 'rabbit.syusui.net':
root = e.target.closest('.shrink-0');
avatar_url = root.querySelector('.author-icon img').src;
display_name = root.querySelector('.author-name').textContent.trim();
acct = root.querySelector('.author-username').textContent.trim();
body = root.querySelector('.content span').textContent.trim().replace(/\n/g, "\\n");
media_urls = [...root.querySelectorAll('.content img')].map(x => x.src);
break;
default:
break;
}
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(() => {
let q;
switch (document.domain) {
//nostter
case 'nostter.app':
q = '.action-menu:not(.__ukabutton)';
break;
//Rabbit
case 'rabbit.syusui.net':
q = '.actions:not(.__ukabutton)';
break;
default:
break;
}
for (const el of document.querySelectorAll(q)) {
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