Greasy Fork is available in English.

Fediverse Open on Main Server

Open Users or Notes on services that supports ActivityPub on your main Misskey server. Open the home page of this script and execute the user script command to set the main server.

נכון ליום 06-09-2023. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name Fediverse Open on Main Server
  3. // @name:ja Fediverse メインサーバーで開く
  4. // @description Open Users or Notes on services that supports ActivityPub on your main Misskey server. Open the home page of this script and execute the user script command to set the main server.
  5. // @description:ja ActivityPubに対応しているサービスのUser、またはNoteを、メインで利用しているMisskeyサーバーで開きます。このスクリプトのホームページを開いて、ユーザースクリプトコマンドを実行して、メインサーバーを指定してください。
  6. // @namespace https://greasyfork.org/users/137
  7. // @version 1.0.0
  8. // @match https://greasyfork.org/*/scripts/474630-*
  9. // @match https://mastodon.social/*
  10. // @match https://pawoo.net/*
  11. // @match https://mstdn.jp/*
  12. // @match https://misskey.io/*
  13. // @match https://mastodon.cloud/*
  14. // @match https://fedibird.com/*
  15. // @match https://nijimiss.moe/*
  16. // @match https://buicha.social/*
  17. // @match https://misskey.niri.la/*
  18. // @match https://vcasskey.net/*
  19. // @require https://greasyfork.org/scripts/19616/code/utilities.js?version=895049
  20. // @license MPL-2.0
  21. // @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
  22. // @compatible Edge
  23. // @compatible Firefox Firefoxを推奨 / Firefox is recommended
  24. // @compatible Opera
  25. // @compatible Chrome
  26. // @grant GM.registerMenuCommand
  27. // @grant GM.setValue
  28. // @grant GM.getValue
  29. // @grant GM.deleteValue
  30. // @grant GM_xmlhttpRequest
  31. // @run-at document-start
  32. // @noframes
  33. // @icon https://codeberg.org/fediverse/distributopia/raw/branch/main/all-logos-in-one-basket/public/basket/Fediverse_logo_proposal-1-min.svg
  34. // @author 100の人
  35. // @homepageURL https://greasyfork.org/scripts/474630
  36. // ==/UserScript==
  37.  
  38. /*global Gettext, _, h */
  39.  
  40. 'use strict';
  41.  
  42. // L10N
  43. Gettext.setLocalizedTexts({
  44. /*eslint-disable quote-props, max-len */
  45. 'ja': {
  46. 'Fediverse Open on Main Server': 'Fediverse メインサーバーで開く',
  47. 'Fediverse Set your main server': 'Fediverse メインサーバーの設定',
  48. 'Main server URL': 'メインサーバーのURL',
  49. 'Add the URLs of the main server and the server to which you want to add the user script command to the “User @match” in the user script settings in the format like “https://example.com/*”.':
  50. 'メインサーバー、およびユーザースクリプトコマンドを追加したいサーバーのURLを、「https://example.com/*」のような形式で、ユーザースクリプト設定の「ユーザー @match」へ追加してください。',
  51. 'Cancel': 'キャンセル',
  52. 'OK': 'OK',
  53. 'Failed to look up.': '照会に失敗しました。',
  54. 'Open the home page of this script and configure from the user script command.':
  55. '当スクリプトのホームページを開いて、ユーザースクリプトコマンドから設定を行ってください。',
  56. },
  57. /*eslint-enable quote-props, max-len */
  58. });
  59. Gettext.originalLocale = 'en';
  60. Gettext.setLocale(navigator.language);
  61.  
  62. class HttpError extends Error
  63. {
  64. name = 'HttpError';
  65. response;
  66. /**
  67. * @param {Response} response
  68. */
  69. constructor(response)
  70. {
  71. super(response.status + ' ' + response.statusText + '\n\n');
  72. this.response = response;
  73. response.text().then(body => {
  74. this.message += '\n\n' + body;
  75. });
  76. }
  77. }
  78.  
  79. /**
  80. * @param {string} serverURL
  81. * @returns {Promise.<string>}
  82. */
  83. async function miAuth(serverURL)
  84. {
  85. const sessionId = crypto.randomUUID();
  86. await Promise.all([ GM.setValue('miAuthSessionId', sessionId), GM.setValue('urlWaitingMiAuth', location.href) ]);
  87. location.assign(`${serverURL}/miauth/${sessionId}?${new URLSearchParams({
  88. name: _('Fediverse Open on Main Server'),
  89. callback: serverURL,
  90. })}`);
  91. }
  92.  
  93. /**
  94. * @param {string} accessToken
  95. * @param {string} url
  96. * @returns {Promise.<string>}
  97. */
  98. async function lookUpOnMisskey(serverURL, accessToken, url)
  99. {
  100. let json;
  101. const apiURL = `${serverURL}/api/ap/show`;
  102. const requestInit = {
  103. method: 'POST',
  104. headers: { 'content-type': 'application/json' },
  105. body: JSON.stringify({ i: accessToken, uri: url }),
  106. };
  107. if (typeof GM_xmlhttpRequest !== 'undefined') { //eslint-disable-line camelcase
  108. // 通信がContent Security PolicyによってブロックされるViolemntmonkeyの不具合を回避
  109. const response = await new Promise(function (resolve, reject) {
  110. GM_xmlhttpRequest(Object.assign({ //eslint-disable-line new-cap
  111. url: apiURL,
  112. data: requestInit.body,
  113. onload: resolve,
  114. onerror: reject,
  115. ontimeout: reject,
  116. }, requestInit));
  117. });
  118.  
  119. if (response.status !== 200) {
  120. return Promise.reject(new HttpError(new Response(response.responseText, response)));
  121. }
  122.  
  123. json = JSON.parse(response.responseText);
  124. } else {
  125. const response = await fetch(apiURL, requestInit);
  126.  
  127. if (!response.ok) {
  128. return Promise.reject(new HttpError(response));
  129. }
  130.  
  131. json = await response.json();
  132. }
  133. const { type, object: { username, host, id } } = json;
  134. switch (type) {
  135. case 'User':
  136. return serverURL + '/@' + username + (host ? '@' + host : '');
  137. case 'Note':
  138. return serverURL + '/notes/' + id;
  139. }
  140. }
  141.  
  142. switch (location.host) {
  143. case 'greasyfork.org': {
  144. /** @type {HTMLDialogElement} */
  145. let dialog, form;
  146. GM.registerMenuCommand(_('Fediverse Set your main server'), async function () {
  147. const [ url ] = await Promise.all([ 'url' ].map(name => GM.getValue(name, '')));
  148. if (!dialog) {
  149. document.body.insertAdjacentHTML('beforeend', h`<dialog>
  150. <form method="dialog">
  151. <input type="hidden" name="application" value="Misskey" />
  152. <p><label>
  153. ${_('Main server URL')}
  154. <input type="url" name="url" placeholder="https://example.com" pattern="https?://[^\\/]+" />
  155. </label></p>
  156. <p>${_('Add the URLs of the main server and the server to which you want to add the user script command to the “User @match” in the user script settings in the format like “https://example.com/*”.' /* eslint-disable-line max-len */)}</p>
  157. <button name="cancel">${_('Cancel')}</button> <button>${_('OK')}</button>
  158. </form>
  159. </dialog>`);
  160.  
  161. dialog = document.body.lastElementChild;
  162. form = dialog.getElementsByTagName('form')[0];
  163. form.url.addEventListener('change', function (event) {
  164. let url;
  165. try {
  166. url = new URL(event.target.value);
  167. } catch (exception) {
  168. if (exception.name !== 'TypeError') {
  169. throw exception;
  170. }
  171. }
  172. if (!url) {
  173. return;
  174. }
  175. event.target.value = url.origin;
  176. });
  177. let chromium = false;
  178. form.addEventListener('submit', function (event) {
  179. if (event.submitter?.name === 'cancel') {
  180. event.preventDefault();
  181. dialog.close();
  182. }
  183. chromium = true;
  184. });
  185. form.addEventListener('formdata', function (event) {
  186. chromium = false;
  187. if (event.formData.get('url') !== url) {
  188. GM.deleteValue('accessToken');
  189. }
  190.  
  191. for (const [ name, value ] of event.formData) {
  192. GM.setValue(name, value);
  193. }
  194. });
  195. // Chromiumでformdataイベントが発生しない不具合の回避
  196. dialog.addEventListener('close', function () {
  197. if (!chromium) {
  198. return;
  199. }
  200. form.dispatchEvent(new FormDataEvent('formdata', { formData: new FormData(form) }));
  201. });
  202. }
  203. form.url.value = url;
  204.  
  205. dialog.showModal();
  206. });
  207. break;
  208. }
  209. default:
  210. if (location.search.startsWith('?session=')) {
  211. // MiAuthで認可が終わった後のリダイレクトの可能性があれば
  212. Promise.all([ 'application', 'url', 'miAuthSessionId', 'urlWaitingMiAuth' ].map(name => GM.getValue(name)))
  213. .then(async function ([ application, serverURL, miAuthSessionId, urlWaitingMiAuth ]) {
  214. if (application !== 'Misskey' || location.origin !== serverURL) {
  215. return;
  216. }
  217.  
  218. const session = new URLSearchParams(location.search).get('session');
  219. if (session !== miAuthSessionId) {
  220. return;
  221. }
  222.  
  223. await Promise.all([ 'miAuthSessionId', 'urlWaitingMiAuth' ].map(name => GM.deleteValue(name)));
  224.  
  225. // アクセストークンを取得
  226. const response
  227. = await fetch(`${serverURL}/api/miauth/${miAuthSessionId}/check`, { method: 'POST' });
  228. if (!response.ok) {
  229. console.error(response);
  230. return;
  231. }
  232. const { ok, token } = await response.json();
  233. if (!ok) {
  234. console.error(response);
  235. return;
  236. }
  237.  
  238. await GM.setValue('accessToken', token);
  239.  
  240. // 照会
  241. let url;
  242. try {
  243. url = await lookUpOnMisskey(serverURL, token, urlWaitingMiAuth);
  244. } catch (exception) {
  245. if (exception.name !== 'HttpError') {
  246. throw exception;
  247. }
  248. switch (exception.response.status) {
  249. case 500:
  250. alert(_('Failed to look up.')); //eslint-disable-line no-alert
  251. return;
  252. default:
  253. throw exception;
  254. }
  255. }
  256. location.replace(url);
  257. });
  258. } else {
  259. GM.registerMenuCommand(_('Fediverse Open on Main Server'), async function () {
  260. const [ application, serverURL, accessToken ]
  261. = await Promise.all([ 'application', 'url', 'accessToken' ].map(name => GM.getValue(name)));
  262. if (!application || !serverURL) {
  263. //eslint-disable-next-line no-alert
  264. alert(_('Open the home page of this script and configure from the user script command.'));
  265. return;
  266. }
  267.  
  268. let url;
  269. switch (application) {
  270. case 'Misskey': {
  271. if (!accessToken) {
  272. await miAuth(serverURL);
  273. return;
  274. }
  275.  
  276. try {
  277. url = await lookUpOnMisskey(serverURL, accessToken, location.href);
  278. } catch (exception) {
  279. if (exception.name !== 'HttpError') {
  280. throw exception;
  281. }
  282. switch (exception.response.status) {
  283. case 401:
  284. await miAuth(serverURL);
  285. return;
  286. case 500:
  287. break;
  288. default:
  289. throw exception;
  290. }
  291. }
  292. }
  293. }
  294.  
  295. if (!url) {
  296. alert(_('Failed to look up.')); //eslint-disable-line no-alert
  297. return;
  298. }
  299. location.assign(url);
  300. });
  301. }
  302. }