Forked from unarist/mastodon-sstp-over-http.user.js
Last active
June 8, 2024 11:54
-
-
Save nikolat/c945aa7dce03dd86a43c947a32fb83b9 to your computer and use it in GitHub Desktop.
SSTP over HTTP で喋らせるボタンを生やすやつ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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}); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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}); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 div > div > 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