diff --git a/peertube-plugin-auto-block-videos/README.md b/peertube-plugin-auto-block-videos/README.md new file mode 100644 index 0000000..5cc69ff --- /dev/null +++ b/peertube-plugin-auto-block-videos/README.md @@ -0,0 +1,60 @@ +# Auto block videos plugin for PeerTube + +Auto block videos based on public blocklists. + +## Block lists + +**Add your public list here** + +## Blocklist URL format + +This plugin expects the following JSON format from public blocklists: + +``` +{ + data: { + value: string + action?: 'add' | 'remove' // Default is 'add' + updatedAt?: string // ISO 8601 + }[] +} +``` + +For example: + +``` +{ + data: [ + { + value: 'https://framatube.org/videos/watch/37938234-ddf2-46d7-8967-8ac84820d5cd' + }, + { + value: 'https://peertube.cpy.re/videos/watch/f78a97f8-a142-4ce1-a5bd-154bf9386504', + updatedAt: '2020-05-07T14:42:48.954Z' + } + ] +} +``` + +This plugin does not apply a diff, so if you want to remove an entity from the blocklist, add `action: 'remove'` to the object. + +For example, to revert `https://peertube.cpy.re/videos/watch/f78a97f8-a142-4ce1-a5bd-154bf9386504` from the blocklist, update the JSON: + +``` +{ + data: [ + { + value: 'https://peertube.cpy.re/videos/watch/f78a97f8-a142-4ce1-a5bd-154bf9386504', + action: 'remove' + }, + { + value: 'https://framatube.org/videos/watch/37938234-ddf2-46d7-8967-8ac84820d5cd' + } + ] +} +``` + +The purpose of the `updatedAt` field is to not override admin blocks/unblocks: + * Plugin auto block video A with an `updatedAt: '2020-05-07T14:42:48.954Z'` + * Admin thinks this video is fine so it unblocks video A + * On another check, the plugin won't re-block the account A because the `updatedAt` is before the last check diff --git a/peertube-plugin-auto-block-videos/main.js b/peertube-plugin-auto-block-videos/main.js new file mode 100644 index 0000000..024428e --- /dev/null +++ b/peertube-plugin-auto-block-videos/main.js @@ -0,0 +1,181 @@ +const simpleGet = require('simple-get') + +const store = { + urls: [], + checkIntervalSeconds: null, + alreadyAdded: new Set(), + alreadyRemoved: new Set(), + timeout: null +} + +async function register ({ + settingsManager, + storageManager, + peertubeHelpers, + registerSetting +}) { + const { logger } = peertubeHelpers + + registerSetting({ + name: 'blocklist-urls', + label: 'Blocklist URLs (one per line)', + type: 'input-textarea', + private: true + }) + + registerSetting({ + name: 'check-seconds-interval', + label: 'Blocklist check frequency (seconds)', + type: 'input', + private: true, + default: 3600 // 1 Hour + }) + + const settings = await settingsManager.getSettings([ 'check-seconds-interval', 'blocklist-urls' ]) + + await load(peertubeHelpers, storageManager, settings['blocklist-urls'], settings['check-seconds-interval']) + + settingsManager.onSettingsChange(settings => { + load(peertubeHelpers, storageManager, settings['blocklist-urls'], settings['check-seconds-interval']) + .catch(err => logger.error('Cannot load auto block videos plugin.', { err })) + }) +} + +async function unregister () { + return +} + +module.exports = { + register, + unregister +} + +// ############################################################################ + +async function load (peertubeHelpers, storageManager, blocklistUrls, checkIntervalSeconds) { + const { logger } = peertubeHelpers + + if (store.timeout) clearTimeout(store.timeout) + + store.checkIntervalSeconds = checkIntervalSeconds + + store.urls = (blocklistUrls || '').split('\n') + .filter(url => !!url) + + if (store.urls.length === 0) { + logger.info('Do not load auto block videos plugin because of empty blocklist URLs.') + return + } + + logger.info('Loaded %d blocklist URLs for auto block videos plugin.', store.urls.length, { urls: store.urls }) + + runLater(peertubeHelpers, storageManager) +} + +async function runCheck (peertubeHelpers, storageManager) { + const { logger } = peertubeHelpers + + if (store.urls.length === 0) return runLater(peertubeHelpers, storageManager) + + let lastChecks = await storageManager.getData('last-checks') + if (!lastChecks) lastChecks = {} + + const newLastCheck = {} + + for (const url of store.urls) { + try { + const { data } = await get(url) + newLastCheck[url] = new Date().toISOString() + + const lastCheckTime = lastChecks[url] + ? new Date(lastChecks[url]).getTime() + : 0 + + if (Array.isArray(data.data) === false) { + logger.error('JSON response is not valid from %s.', { data }) + continue + } + + for (const entity of data.data) { + if (!entity.value) { + logger.error('JSON entity is not valid.', { entity }) + continue + } + + // We already checked this entity? + if (entity.updatedAt) { + const updatedAtTime = new Date(entity.updatedAt).getTime() + + if (updatedAtTime < lastCheckTime) continue + } + + if (entity.action === 'remove') await removeEntity(peertubeHelpers, entity.value) + else await addEntity(peertubeHelpers, entity.value) + } + } catch (err) { + logger.warn('Cannot auto block videos from %s.', url, { err }) + } + } + + await storageManager.storeData('last-checks', newLastCheck) + + runLater(peertubeHelpers, storageManager) +} + +function runLater (peertubeHelpers, storageManager) { + const { logger } = peertubeHelpers + + logger.debug('Will run auto videos block check in %d seconds.', store.checkIntervalSeconds) + + store.timeout = setTimeout(() => { + runCheck(peertubeHelpers, storageManager) + }, store.checkIntervalSeconds * 1000) +} + +function get (url) { + return new Promise((resolve, reject) => { + simpleGet.concat({ url, method: 'GET', json: true }, function (err, res, data) { + if (err) return reject(err) + + return resolve({ res, data }) + }) + }) +} + +async function addEntity (peertubeHelpers, value) { + const { moderation, videos, logger } = peertubeHelpers + + if (store.alreadyAdded.has(value)) return + + store.alreadyRemoved.delete(value) + store.alreadyAdded.add(value) + + const video = await videos.loadByUrl(value) + if (!video) return + + if (video.remote !== true) { + logger.info('Do not auto block our own video %s.', value) + return + } + + logger.info('Auto block video %s from blocklist.', value) + + const reason = 'Automatically blocked from auto block plugin.' + return moderation.blacklistVideo({ videoIdOrUUID: video.id, createOptions: { reason } }) +} + +async function removeEntity (peertubeHelpers, value) { + const { moderation, logger, videos } = peertubeHelpers + + if (store.alreadyRemoved.has(value)) return + + store.alreadyAdded.delete(value) + store.alreadyRemoved.add(value) + + const video = await videos.loadByUrl(value) + if (!video) return + + logger.info('Auto removing video %s from blocklist.', value) + + return moderation.unblacklistVideo({ videoIdOrUUID: video.id }) +} diff --git a/peertube-plugin-auto-block-videos/package-lock.json b/peertube-plugin-auto-block-videos/package-lock.json new file mode 100644 index 0000000..17bd225 --- /dev/null +++ b/peertube-plugin-auto-block-videos/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "peertube-plugin-auto-block-videos", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/peertube-plugin-auto-block-videos/package.json b/peertube-plugin-auto-block-videos/package.json new file mode 100644 index 0000000..c39e228 --- /dev/null +++ b/peertube-plugin-auto-block-videos/package.json @@ -0,0 +1,23 @@ +{ + "name": "peertube-plugin-auto-block-videos", + "version": "0.0.1", + "description": "Auto block videos plugin for PeerTube", + "engine": { + "peertube": ">=2.2.0" + }, + "keywords": [ + "peertube", + "plugin" + ], + "homepage": "https://framagit.org/framasoft/peertube/official-plugins/tree/master/peertube-plugin-auto-block-videos", + "author": "Chocobozzz", + "bugs": "https://framagit.org/framasoft/peertube/official-plugins/issues", + "library": "./main.js", + "staticDirs": {}, + "css": [], + "clientScripts": [], + "translations": {}, + "dependencies": { + "simple-get": "^3.1.0" + } +} diff --git a/peertube-plugin-auto-block-videos/tests/blocklist-sample.json b/peertube-plugin-auto-block-videos/tests/blocklist-sample.json new file mode 100644 index 0000000..6c48881 --- /dev/null +++ b/peertube-plugin-auto-block-videos/tests/blocklist-sample.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "value": "peertube.cpy.re" + }, + { + "value": "peertube3.cpy.re" + } + ] +}