🏠 Home 

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

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

// ==UserScript==
// @name         Annict 記録をFediverseへ投稿するやつ
// @namespace    https://midra.me
// @version      1.1.1
// @description  記録をFediverse(Misskey, Mastodon)へ投稿
// @author       Midra
// @license      MIT
// @match        https://annict.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=annict.com
// @run-at       document-end
// @noframes
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      annict.com
// @require      https://greasyfork.org/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=156587
// ==/UserScript==
(() => {
'use strict'
//----------------------------------------
// 設定初期化
//----------------------------------------
const configInitData = {
postTo: {
label: '投稿先',
type: 'select',
default: 'misskey',
options: {
all: 'すべて',
misskey: 'Misskey',
mastodon: 'Mastodon',
},
},
annictUserName: {
label: 'Annict ユーザ名',
type: 'text',
default: '',
},
annictToken: {
label: 'Annict アクセストークン',
type: 'text',
default: '',
},
misskeyInstance: {
label: 'Misskey インスタンス (httpsは省略)',
type: 'text',
default: 'misskey.io',
},
misskeyVisibility: {
label: 'Misskey 公開範囲',
type: 'select',
default: 'public',
options: {
public: 'パブリック',
home: 'ホーム',
followers: 'フォロワー',
},
},
misskeyToken: {
label: 'Misskey アクセストークン',
type: 'text',
default: '',
},
mastodonInstance: {
label: 'Mastodon インスタンス (httpsは省略)',
type: 'text',
default: 'mstdn.jp',
},
mastodonVisibility: {
label: 'Mastodon 公開範囲',
type: 'select',
default: 'public',
options: {
public: '公開',
unlisted: '未収載',
private: 'フォロワー限定',
},
},
mastodonToken: {
label: 'Mastodon アクセストークン',
type: 'text',
default: '',
},
}
GM_config.init('Annict 記録をFediverseへ投稿するやつ 設定', configInitData)
GM_config.onload = () => {
setTimeout(() => {
alert('設定を反映させるにはページを再読み込みしてください。')
}, 200)
}
GM_registerMenuCommand('設定', GM_config.open)
// 設定取得
const config = {}
Object.keys(configInitData).forEach(v => { config[v] = GM_config.get(v) })
const getWork = async (workId) => {
try {
const res = await fetch(`https://api.annict.com/v1/works?${new URLSearchParams({
filter_ids: workId,
fields: 'title',
access_token: config['annictToken'],
})}`)
const json = await res.json()
return json['works'][0]
} catch (e) {
console.error(e)
}
}
const getEpisode = async (episodeId) => {
try {
const res = await fetch(`https://api.annict.com/v1/episodes?${new URLSearchParams({
filter_ids: episodeId,
fields: 'number_text,work.title,work.twitter_hashtag',
access_token: config['annictToken'],
})}`)
const json = await res.json()
return json['episodes'][0]
} catch (e) {
console.error(e)
}
}
const postToFediverse = async (text) => {
if (typeof text === 'string' && text !== '') {
try {
// Misskeyへ投稿
if (
(
config['postTo'] === 'all' ||
config['postTo'] === 'misskey'
) &&
config['misskeyInstance'] &&
config['misskeyToken']
) {
await fetch(`https://${config['misskeyInstance']}/api/notes/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
i: config['misskeyToken'],
text: text,
visibility: config['misskeyVisibility'],
}),
})
}
// Mastodonへ投稿
if (
(
config['postTo'] === 'all' ||
config['postTo'] === 'mastodon'
) &&
config['mastodonInstance'] &&
config['mastodonToken']
) {
await fetch(`https://${config['mastodonInstance']}/api/v1/statuses`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config['mastodonToken']}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: text,
visibility: config['mastodonVisibility'],
}),
})
}
} catch (e) {
console.error(e)
}
}
}
unsafeWindow.fetch = new Proxy(unsafeWindow.fetch, {
apply: async function(target, thisArg, argumentsList) {
const promise = Reflect.apply(target, thisArg, argumentsList)
if (
argumentsList[0].startsWith('/api/internal/works/') &&
argumentsList[0].endsWith('/status_select')
) {
let postText
/** @type {Response} */
const response = await promise
const body = JSON.parse(argumentsList[1]?.body || '{}')
if (response.ok) {
const status = {
'no_status': ['未選択'],
'plan_to_watch': ['見たい', 'wanna_watch'],
'watching': ['見てる', 'watching'],
'completed': ['見た', 'watched'],
'on_hold': ['一時中断', 'on_hold'],
'dropped': ['視聴中止', 'stop_watching'],
}[body['status_kind']]
const workId = argumentsList[0].split('/')[4]
if (status[1] && workId) {
const work = await getWork(workId)
const title = work['title']
if (title) {
postText = `アニメ「${title}」の視聴ステータスを「${status[0]}」にしました https://annict.com/@${config['annictUserName']}/${status[1]}`
}
}
}
await postToFediverse(postText)
return response
}
return promise
}
})
unsafeWindow.XMLHttpRequest.prototype.send = new Proxy(unsafeWindow.XMLHttpRequest.prototype.send, {
apply: async function(target, thisArg, argumentsList) {
Reflect.apply(target, thisArg, argumentsList)
/** @type {XMLHttpRequest} */
const req = thisArg
req.addEventListener('load', async () => {
try {
if ([200, 201].includes(req.status)) {
let postText
const response = JSON.parse(req.response || '{}')
const body = JSON.parse(argumentsList[0] || '{}')
if (req.responseURL.endsWith('/api/internal/episode_records')) {
const record_id = response['record_id']
const episode_id = body['episode_id']
if (record_id && episode_id) {
const episode = await getEpisode(episode_id)
const number_text = episode['number_text']
const title = episode['work']['title']
const twitter_hashtag = episode['work']['twitter_hashtag']
if (number_text && title) {
postText = `${title} ${number_text} を見ました https://annict.com/@${config['annictUserName']}/records/${record_id} ${twitter_hashtag ? `#${twitter_hashtag}` : ''}`
}
}
}
await postToFediverse(postText)
}
} catch (e) {
console.error(e)
}
})
}
})
})()