Skip to content

Instantly share code, notes, and snippets.

@unarist
Last active November 1, 2018 13:50
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save unarist/91da256246fb438811f2e1a05785abad to your computer and use it in GitHub Desktop.
Save unarist/91da256246fb438811f2e1a05785abad to your computer and use it in GitHub Desktop.
Mastodon - 期限付きミュート
// ==UserScript==
// @name Mastodon - 期限付きミュート
// @namespace https://github.com/unarist/
// @version 0.11
// @author unarist
// @match https://*/web/*
// @grant none
// @downloadURL https://gist.github.com/unarist/91da256246fb438811f2e1a05785abad/raw/mastodon-timed-mute.user.js
// ==/UserScript==
/*
標準のミュート機能で期限を指定できるようにするUserScriptです。
具体的には「いつ誰を解除するか」をlocalStorageに記憶しておき、
その時間を過ぎた適当なタイミングにミュート解除を行います。
ミュート期限はミュートしたブラウザに記憶されるので、
・期限を過ぎていてもそのブラウザでWebUIを開かなければ解除されません
・「閲覧履歴の削除」「プライベートモード」などでは期限情報が消え、自動解除が行われない場合があります
自動解除された後はリロードしなくても当該トゥートが表示されるようになりますが、
既に読み込まれていたTLの分はそのままです。(つまり新しく読み込んだ分だけ)
ページ表示時にミュート解除された場合も、初回読み込み分では非表示のままです。
v0.11: "無期限"が即時解除になっていたバグを修正 (thanks: @mitarashi_dango)
v0.10: consoleに処理結果を流すように、エラーメッセージに(空の)statusTextではなくstatusを使うように
v0.9: Mastodon1.5対応、モバイルレイアウトで崩れないように「期限:」のラベルを一旦削除
*/
(function() {
'use strict';
const log = s => console.log('mastodon-timed-mute.user.js: ' + s);
const options = [
/* [label, minutes] */
['5分', 5],
['30分', 30],
['1時間', 60],
['2時間', 120],
['6時間', 360],
['12時間', 720],
['無期限', 0]
].map(([l, m]) => [l, m * 60 * 1000]);
if (!document.querySelector('.app-holder[data-react-class="Mastodon"], #mastodon')) return;
const token = JSON.parse(document.getElementById('initial-state').textContent).meta.access_token;
const root = document.querySelector('.modal-root'),
tag = (e,p) => Object.assign(document.createElement(e), p || {}),
load = () => JSON.parse(localStorage.getItem('timed-mute')) || {},
save = obj => localStorage.setItem('timed-mute', JSON.stringify(obj));
function respAsJson(resp) {
if (!resp.ok) throw new Error(new URL(resp.url).pathname + ' ' + resp.status);
return resp.json();
}
/* core */
let timerId = null;
function migrate() {
let state = load();
const v = state.v;
if(v === undefined) {
state = {v:2, default: 0, items: state };
}
save(state);
}
function reload(timeout = false) {
if (timeout && timerId !== null) clearTimeout(timerId);
const t = Date.now();
const state = load();
const items = state.items;
for (const uid in items) {
if(items[uid].expires < t) {
unmute(items[uid].acct, uid);
delete items[uid];
}
}
save(state);
timerId = setTimeout(reload, 5 * 60, true);
return state;
}
function unmute(acct, uid) {
fetch(location.origin + '/api/v1/accounts/' + uid + '/unmute', {
method: 'post',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(respAsJson)
.then(() => log(`unmuted ${uid} (${acct})`))
.catch(msg => {
log(`failed to unmute - acct: ${acct}, uid: ${uid}, msg: ${msg}`);
alert(acct + 'の自動ミュート解除に失敗しました。\n' + msg);
});
}
function addEntry(acct, expires) {
fetch(location.origin + '/api/v1/accounts/search?q=' + acct, {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(respAsJson)
.then(json => {
if (json.length === 0) throw new Error('対象のアカウントが見つかりませんでした');
const uid = json[0].id;
const state = reload();
state.items[uid] = {acct, expires};
save(state);
log(`registered ${uid} (${acct}) to unmute at ${expires}`);
})
.catch(msg => {
log(`failed to register - acct: ${acct}, expires: ${expires}, msg: ${msg}`);
alert('期限付きミュートの設定に失敗しました。\n' + msg);
});
}
/* ミュート設定 */
new MutationObserver(() => {
const button = [...root.querySelectorAll('.button')].find(e => e.textContent.match(/ミュート|Mute/));
if (!button || button.classList.contains('---timed-mute')) return;
button.classList.add('---timed-mute');
const acct = root.querySelector('strong').textContent.slice(1);
const container = button.parentElement.insertBefore(tag('div', {/*textContent: '期限: '*/}), button);
const select = container.appendChild(tag('select'));
const defaultDuration = load().default;
for (const [label, duration] of options)
select.appendChild(tag('option', {value: duration, textContent: label, selected: duration === defaultDuration}));
button.addEventListener('click', () => {
const selectedValue = Number(select.options[select.selectedIndex].value);
save(Object.assign(load(), { default: selectedValue }));
if (selectedValue > 0) {
const expires = Date.now() + selectedValue;
addEntry(acct, expires);
}
});
}).observe(root, { childList: true, subtree: true });
migrate();
reload();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment