Annict 記録をFediverseへ投稿するやつ

記録をFediverse(Misskey, Mastodon)へ投稿

  1. // ==UserScript==
  2. // @name Annict 記録をFediverseへ投稿するやつ
  3. // @namespace https://midra.me
  4. // @version 1.1.1
  5. // @description 記録をFediverse(Misskey, Mastodon)へ投稿
  6. // @author Midra
  7. // @license MIT
  8. // @match https://annict.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=annict.com
  10. // @run-at document-end
  11. // @noframes
  12. // @grant unsafeWindow
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_xmlhttpRequest
  17. // @connect annict.com
  18. // @require https://gf.qytechs.cn/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=156587
  19. // ==/UserScript==
  20.  
  21. (() => {
  22. 'use strict'
  23.  
  24. //----------------------------------------
  25. // 設定初期化
  26. //----------------------------------------
  27. const configInitData = {
  28. postTo: {
  29. label: '投稿先',
  30. type: 'select',
  31. default: 'misskey',
  32. options: {
  33. all: 'すべて',
  34. misskey: 'Misskey',
  35. mastodon: 'Mastodon',
  36. },
  37. },
  38. annictUserName: {
  39. label: 'Annict ユーザ名',
  40. type: 'text',
  41. default: '',
  42. },
  43. annictToken: {
  44. label: 'Annict アクセストークン',
  45. type: 'text',
  46. default: '',
  47. },
  48. misskeyInstance: {
  49. label: 'Misskey インスタンス (httpsは省略)',
  50. type: 'text',
  51. default: 'misskey.io',
  52. },
  53. misskeyVisibility: {
  54. label: 'Misskey 公開範囲',
  55. type: 'select',
  56. default: 'public',
  57. options: {
  58. public: 'パブリック',
  59. home: 'ホーム',
  60. followers: 'フォロワー',
  61. },
  62. },
  63. misskeyToken: {
  64. label: 'Misskey アクセストークン',
  65. type: 'text',
  66. default: '',
  67. },
  68. mastodonInstance: {
  69. label: 'Mastodon インスタンス (httpsは省略)',
  70. type: 'text',
  71. default: 'mstdn.jp',
  72. },
  73. mastodonVisibility: {
  74. label: 'Mastodon 公開範囲',
  75. type: 'select',
  76. default: 'public',
  77. options: {
  78. public: '公開',
  79. unlisted: '未収載',
  80. private: 'フォロワー限定',
  81. },
  82. },
  83. mastodonToken: {
  84. label: 'Mastodon アクセストークン',
  85. type: 'text',
  86. default: '',
  87. },
  88. }
  89. GM_config.init('Annict 記録をFediverseへ投稿するやつ 設定', configInitData)
  90.  
  91. GM_config.onload = () => {
  92. setTimeout(() => {
  93. alert('設定を反映させるにはページを再読み込みしてください。')
  94. }, 200)
  95. }
  96.  
  97. GM_registerMenuCommand('設定', GM_config.open)
  98.  
  99. // 設定取得
  100. const config = {}
  101. Object.keys(configInitData).forEach(v => { config[v] = GM_config.get(v) })
  102.  
  103. const getWork = async (workId) => {
  104. try {
  105. const res = await fetch(`https://api.annict.com/v1/works?${new URLSearchParams({
  106. filter_ids: workId,
  107. fields: 'title',
  108. access_token: config['annictToken'],
  109. })}`)
  110. const json = await res.json()
  111. return json['works'][0]
  112. } catch (e) {
  113. console.error(e)
  114. }
  115. }
  116.  
  117. const getEpisode = async (episodeId) => {
  118. try {
  119. const res = await fetch(`https://api.annict.com/v1/episodes?${new URLSearchParams({
  120. filter_ids: episodeId,
  121. fields: 'number_text,work.title,work.twitter_hashtag',
  122. access_token: config['annictToken'],
  123. })}`)
  124. const json = await res.json()
  125. return json['episodes'][0]
  126. } catch (e) {
  127. console.error(e)
  128. }
  129. }
  130.  
  131. const postToFediverse = async (text) => {
  132. if (typeof text === 'string' && text !== '') {
  133. try {
  134. // Misskeyへ投稿
  135. if (
  136. (
  137. config['postTo'] === 'all' ||
  138. config['postTo'] === 'misskey'
  139. ) &&
  140. config['misskeyInstance'] &&
  141. config['misskeyToken']
  142. ) {
  143. await fetch(`https://${config['misskeyInstance']}/api/notes/create`, {
  144. method: 'POST',
  145. headers: {
  146. 'Content-Type': 'application/json',
  147. },
  148. body: JSON.stringify({
  149. i: config['misskeyToken'],
  150. text: text,
  151. visibility: config['misskeyVisibility'],
  152. }),
  153. })
  154. }
  155. // Mastodonへ投稿
  156. if (
  157. (
  158. config['postTo'] === 'all' ||
  159. config['postTo'] === 'mastodon'
  160. ) &&
  161. config['mastodonInstance'] &&
  162. config['mastodonToken']
  163. ) {
  164. await fetch(`https://${config['mastodonInstance']}/api/v1/statuses`, {
  165. method: 'POST',
  166. headers: {
  167. 'Authorization': `Bearer ${config['mastodonToken']}`,
  168. 'Content-Type': 'application/json',
  169. },
  170. body: JSON.stringify({
  171. status: text,
  172. visibility: config['mastodonVisibility'],
  173. }),
  174. })
  175. }
  176. } catch (e) {
  177. console.error(e)
  178. }
  179. }
  180. }
  181.  
  182. unsafeWindow.fetch = new Proxy(unsafeWindow.fetch, {
  183. apply: async function(target, thisArg, argumentsList) {
  184. const promise = Reflect.apply(target, thisArg, argumentsList)
  185.  
  186. if (
  187. argumentsList[0].startsWith('/api/internal/works/') &&
  188. argumentsList[0].endsWith('/status_select')
  189. ) {
  190. let postText
  191.  
  192. /** @type {Response} */
  193. const response = await promise
  194. const body = JSON.parse(argumentsList[1]?.body || '{}')
  195.  
  196. if (response.ok) {
  197. const status = {
  198. 'no_status': ['未選択'],
  199. 'plan_to_watch': ['見たい', 'wanna_watch'],
  200. 'watching': ['見てる', 'watching'],
  201. 'completed': ['見た', 'watched'],
  202. 'on_hold': ['一時中断', 'on_hold'],
  203. 'dropped': ['視聴中止', 'stop_watching'],
  204. }[body['status_kind']]
  205. const workId = argumentsList[0].split('/')[4]
  206. if (status[1] && workId) {
  207. const work = await getWork(workId)
  208. const title = work['title']
  209. if (title) {
  210. postText = `アニメ「${title}」の視聴ステータスを「${status[0]}」にしました https://annict.com/@${config['annictUserName']}/${status[1]}`
  211. }
  212. }
  213. }
  214.  
  215. await postToFediverse(postText)
  216.  
  217. return response
  218. }
  219.  
  220. return promise
  221. }
  222. })
  223.  
  224. unsafeWindow.XMLHttpRequest.prototype.send = new Proxy(unsafeWindow.XMLHttpRequest.prototype.send, {
  225. apply: async function(target, thisArg, argumentsList) {
  226. Reflect.apply(target, thisArg, argumentsList)
  227.  
  228. /** @type {XMLHttpRequest} */
  229. const req = thisArg
  230.  
  231. req.addEventListener('load', async () => {
  232. try {
  233. if ([200, 201].includes(req.status)) {
  234. let postText
  235.  
  236. const response = JSON.parse(req.response || '{}')
  237. const body = JSON.parse(argumentsList[0] || '{}')
  238.  
  239. if (req.responseURL.endsWith('/api/internal/episode_records')) {
  240. const record_id = response['record_id']
  241. const episode_id = body['episode_id']
  242. if (record_id && episode_id) {
  243. const episode = await getEpisode(episode_id)
  244. const number_text = episode['number_text']
  245. const title = episode['work']['title']
  246. const twitter_hashtag = episode['work']['twitter_hashtag']
  247. if (number_text && title) {
  248. postText = `${title} ${number_text} を見ました https://annict.com/@${config['annictUserName']}/records/${record_id} ${twitter_hashtag ? `#${twitter_hashtag}` : ''}`
  249. }
  250. }
  251. }
  252.  
  253. await postToFediverse(postText)
  254. }
  255. } catch (e) {
  256. console.error(e)
  257. }
  258. })
  259. }
  260. })
  261. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址