2023-01-18 13:45:14 +00:00
|
|
|
/*
|
|
|
|
GoToSocial
|
2023-03-12 17:49:06 +00:00
|
|
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
2023-01-18 13:45:14 +00:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const Promise = require("bluebird");
|
|
|
|
const fileDownload = require("js-file-download");
|
2023-02-03 11:07:40 +00:00
|
|
|
const csv = require("papaparse");
|
|
|
|
const { nanoid } = require("nanoid");
|
|
|
|
|
|
|
|
const { isValidDomainBlock, hasBetterScope } = require("../../domain-block");
|
2023-01-18 13:45:14 +00:00
|
|
|
|
|
|
|
const {
|
|
|
|
replaceCacheOnMutation,
|
|
|
|
domainListToObject,
|
|
|
|
unwrapRes
|
|
|
|
} = require("../lib");
|
|
|
|
|
|
|
|
function parseDomainList(list) {
|
|
|
|
if (list[0] == "[") {
|
|
|
|
return JSON.parse(list);
|
2023-02-03 11:07:40 +00:00
|
|
|
} else if (list.startsWith("#domain")) { // Mastodon CSV
|
|
|
|
const { data, errors } = csv.parse(list, {
|
|
|
|
header: true,
|
|
|
|
transformHeader: (header) => header.slice(1), // removes starting '#'
|
|
|
|
skipEmptyLines: true,
|
|
|
|
dynamicTyping: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
let error = "";
|
|
|
|
errors.forEach((err) => {
|
|
|
|
error += `${err.message} (line ${err.row})`;
|
|
|
|
});
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
2023-01-18 13:45:14 +00:00
|
|
|
} else {
|
|
|
|
return list.split("\n").map((line) => {
|
|
|
|
let domain = line.trim();
|
|
|
|
let valid = true;
|
|
|
|
if (domain.startsWith("http")) {
|
|
|
|
try {
|
|
|
|
domain = new URL(domain).hostname;
|
|
|
|
} catch (e) {
|
|
|
|
valid = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return domain.length > 0
|
|
|
|
? { domain, valid }
|
|
|
|
: null;
|
|
|
|
}).filter((a) => a); // not `null`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function validateDomainList(list) {
|
|
|
|
list.forEach((entry) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
if (entry.domain.startsWith("*.")) {
|
|
|
|
// domain block always includes all subdomains, wildcard is meaningless here
|
|
|
|
entry.domain = entry.domain.slice(2);
|
|
|
|
}
|
|
|
|
|
|
|
|
entry.valid = (entry.valid !== false) && isValidDomainBlock(entry.domain);
|
|
|
|
if (entry.valid) {
|
|
|
|
entry.suggest = hasBetterScope(entry.domain);
|
|
|
|
}
|
2023-01-18 13:45:14 +00:00
|
|
|
entry.checked = entry.valid;
|
|
|
|
});
|
|
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
function deduplicateDomainList(list) {
|
|
|
|
let domains = new Set();
|
|
|
|
return list.filter((entry) => {
|
|
|
|
if (domains.has(entry.domain)) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
domains.add(entry.domain);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = (build) => ({
|
|
|
|
processDomainList: build.mutation({
|
|
|
|
queryFn: (formData) => {
|
|
|
|
return Promise.try(() => {
|
|
|
|
if (formData.domains == undefined || formData.domains.length == 0) {
|
|
|
|
throw "No domains entered";
|
|
|
|
}
|
|
|
|
return parseDomainList(formData.domains);
|
|
|
|
}).then((parsed) => {
|
|
|
|
return deduplicateDomainList(parsed);
|
|
|
|
}).then((deduped) => {
|
|
|
|
return validateDomainList(deduped);
|
|
|
|
}).then((data) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
data.forEach((entry) => {
|
|
|
|
entry.key = nanoid(); // unique id that stays stable even if domain gets modified by user
|
|
|
|
});
|
2023-01-18 13:45:14 +00:00
|
|
|
return { data };
|
|
|
|
}).catch((e) => {
|
|
|
|
return { error: e.toString() };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
exportDomainList: build.mutation({
|
|
|
|
queryFn: (formData, api, _extraOpts, baseQuery) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
let process;
|
|
|
|
|
|
|
|
if (formData.exportType == "json") {
|
|
|
|
process = {
|
|
|
|
transformEntry: (entry) => ({
|
|
|
|
domain: entry.domain,
|
|
|
|
public_comment: entry.public_comment,
|
|
|
|
obfuscate: entry.obfuscate
|
|
|
|
}),
|
|
|
|
stringify: (list) => JSON.stringify(list),
|
|
|
|
extension: ".json",
|
|
|
|
mime: "application/json"
|
|
|
|
};
|
|
|
|
} else if (formData.exportType == "csv") {
|
|
|
|
process = {
|
|
|
|
transformEntry: (entry) => [
|
|
|
|
entry.domain,
|
|
|
|
"suspend", // severity
|
|
|
|
false, // reject_media
|
|
|
|
false, // reject_reports
|
|
|
|
entry.public_comment,
|
|
|
|
entry.obfuscate ?? false
|
|
|
|
],
|
|
|
|
stringify: (list) => csv.unparse({
|
|
|
|
fields: "#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate".split(","),
|
|
|
|
data: list
|
|
|
|
}),
|
|
|
|
extension: ".csv",
|
|
|
|
mime: "text/csv"
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
process = {
|
|
|
|
transformEntry: (entry) => entry.domain,
|
|
|
|
stringify: (list) => list.join("\n"),
|
|
|
|
extension: ".txt",
|
|
|
|
mime: "text/plain"
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-18 13:45:14 +00:00
|
|
|
return Promise.try(() => {
|
|
|
|
return baseQuery({
|
|
|
|
url: `/api/v1/admin/domain_blocks`
|
|
|
|
});
|
|
|
|
}).then(unwrapRes).then((blockedInstances) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
return blockedInstances.map(process.transformEntry);
|
2023-01-18 13:45:14 +00:00
|
|
|
}).then((exportList) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
return process.stringify(exportList);
|
2023-01-18 13:45:14 +00:00
|
|
|
}).then((exportAsString) => {
|
|
|
|
if (formData.action == "export") {
|
|
|
|
return {
|
|
|
|
data: exportAsString
|
|
|
|
};
|
|
|
|
} else if (formData.action == "export-file") {
|
|
|
|
let domain = new URL(api.getState().oauth.instance).host;
|
|
|
|
let date = new Date();
|
|
|
|
|
|
|
|
let filename = [
|
|
|
|
domain,
|
|
|
|
"blocklist",
|
|
|
|
date.getFullYear(),
|
|
|
|
(date.getMonth() + 1).toString().padStart(2, "0"),
|
|
|
|
date.getDate().toString().padStart(2, "0"),
|
|
|
|
].join("-");
|
|
|
|
|
2023-02-03 11:07:40 +00:00
|
|
|
fileDownload(
|
|
|
|
exportAsString,
|
|
|
|
filename + process.extension,
|
|
|
|
process.mime
|
|
|
|
);
|
2023-01-18 13:45:14 +00:00
|
|
|
}
|
|
|
|
return { data: null };
|
|
|
|
}).catch((e) => {
|
|
|
|
return { error: e };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
importDomainList: build.mutation({
|
|
|
|
query: (formData) => {
|
|
|
|
const { domains } = formData;
|
|
|
|
|
|
|
|
// add/replace comments, obfuscation data
|
|
|
|
let process = entryProcessor(formData);
|
|
|
|
domains.forEach((entry) => {
|
|
|
|
process(entry);
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
method: "POST",
|
|
|
|
url: `/api/v1/admin/domain_blocks?import=true`,
|
|
|
|
asForm: true,
|
|
|
|
discardEmpty: true,
|
|
|
|
body: {
|
|
|
|
domains: new Blob([JSON.stringify(domains)], { type: "application/json" })
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
transformResponse: domainListToObject,
|
|
|
|
...replaceCacheOnMutation("instanceBlocks")
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
2023-02-03 11:07:40 +00:00
|
|
|
const internalKeys = new Set("key,suggest,valid,checked".split(","));
|
2023-01-18 13:45:14 +00:00
|
|
|
function entryProcessor(formData) {
|
|
|
|
let funcs = [];
|
|
|
|
|
|
|
|
["private_comment", "public_comment"].forEach((type) => {
|
|
|
|
let text = formData[type].trim();
|
|
|
|
|
|
|
|
if (text.length > 0) {
|
|
|
|
let behavior = formData[`${type}_behavior`];
|
|
|
|
|
|
|
|
if (behavior == "append") {
|
|
|
|
funcs.push(function appendComment(entry) {
|
|
|
|
if (entry[type] == undefined) {
|
|
|
|
entry[type] = text;
|
|
|
|
} else {
|
|
|
|
entry[type] = [entry[type], text].join("\n");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (behavior == "replace") {
|
|
|
|
funcs.push(function replaceComment(entry) {
|
|
|
|
entry[type] = text;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return function process(entry) {
|
|
|
|
funcs.forEach((func) => {
|
|
|
|
func(entry);
|
|
|
|
});
|
|
|
|
|
|
|
|
entry.obfuscate = formData.obfuscate;
|
|
|
|
|
|
|
|
Object.entries(entry).forEach(([key, val]) => {
|
2023-02-03 11:07:40 +00:00
|
|
|
if (internalKeys.has(key) || val == undefined) {
|
2023-01-18 13:45:14 +00:00
|
|
|
delete entry[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|