Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as jsrsasign from "jsrsasign";
|
||||
import config from "../../config/index.js";
|
||||
const ECC_PRELUDE = Buffer.from([
|
||||
0x04
|
||||
]);
|
||||
const NULL_BYTE = Buffer.from([
|
||||
0
|
||||
]);
|
||||
const PEM_PRELUDE = Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex");
|
||||
// Android Safetynet attestations are signed with this cert:
|
||||
const GSR2 = `-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||
-----END CERTIFICATE-----\n`;
|
||||
function base64URLDecode(source) {
|
||||
return Buffer.from(source.replace(/\-/g, "+").replace(/_/g, "/"), "base64");
|
||||
}
|
||||
function getCertSubject(certificate) {
|
||||
const subjectCert = new jsrsasign.X509();
|
||||
subjectCert.readCertPEM(certificate);
|
||||
const subjectString = subjectCert.getSubjectString();
|
||||
const subjectFields = subjectString.slice(1).split("/");
|
||||
const fields = {};
|
||||
for (const field of subjectFields){
|
||||
const eqIndex = field.indexOf("=");
|
||||
fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
function verifyCertificateChain(certificates) {
|
||||
let valid = true;
|
||||
for(let i = 0; i < certificates.length; i++){
|
||||
const Cert = certificates[i];
|
||||
const certificate = new jsrsasign.X509();
|
||||
certificate.readCertPEM(Cert);
|
||||
const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
|
||||
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex, 0, [
|
||||
0
|
||||
]);
|
||||
const algorithm = certificate.getSignatureAlgorithmField();
|
||||
const signatureHex = certificate.getSignatureValueHex();
|
||||
// Verify against CA
|
||||
const Signature = new jsrsasign.KJUR.crypto.Signature({
|
||||
alg: algorithm
|
||||
});
|
||||
Signature.init(CACert);
|
||||
Signature.updateHex(certStruct);
|
||||
valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
function PEMString(pemBuffer, type = "CERTIFICATE") {
|
||||
if (pemBuffer.length === 65 && pemBuffer[0] === 0x04) {
|
||||
pemBuffer = Buffer.concat([
|
||||
PEM_PRELUDE,
|
||||
pemBuffer
|
||||
], 91);
|
||||
type = "PUBLIC KEY";
|
||||
}
|
||||
const cert = pemBuffer.toString("base64");
|
||||
const keyParts = [];
|
||||
const max = Math.ceil(cert.length / 64);
|
||||
let start = 0;
|
||||
for(let i = 0; i < max; i++){
|
||||
keyParts.push(cert.substring(start, start + 64));
|
||||
start += 64;
|
||||
}
|
||||
return `-----BEGIN ${type}-----\n${keyParts.join("\n")}\n-----END ${type}-----\n`;
|
||||
}
|
||||
export function hash(data) {
|
||||
return crypto.createHash("sha256").update(data).digest();
|
||||
}
|
||||
export function verifyLogin({ publicKey, authenticatorData, clientDataJSON, clientData, signature, challenge }) {
|
||||
if (clientData.type !== "webauthn.get") {
|
||||
throw new Error("type is not webauthn.get");
|
||||
}
|
||||
if (hash(clientData.challenge).toString("hex") !== challenge) {
|
||||
throw new Error("challenge mismatch");
|
||||
}
|
||||
if (clientData.origin !== `${config.scheme}://${config.host}`) {
|
||||
throw new Error("origin mismatch");
|
||||
}
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
hash(clientDataJSON)
|
||||
], 32 + authenticatorData.length);
|
||||
return crypto.createVerify("SHA256").update(verificationData).verify(PEMString(publicKey), signature);
|
||||
}
|
||||
export const procedures = {
|
||||
none: {
|
||||
verify ({ publicKey }) {
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyU2F = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
publicKey: publicKeyU2F,
|
||||
valid: true
|
||||
};
|
||||
}
|
||||
},
|
||||
"android-key": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
if (attStmt.alg !== -7) {
|
||||
throw new Error("alg mismatch");
|
||||
}
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
const attCert = attStmt.x5c[0];
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
if (!attCert.equals(publicKeyData)) {
|
||||
throw new Error("public key mismatch");
|
||||
}
|
||||
const isValid = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
||||
return {
|
||||
valid: isValid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
// what a stupid attestation
|
||||
"android-safetynet": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const verificationData = hash(Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]));
|
||||
const jwsParts = attStmt.response.toString("utf-8").split(".");
|
||||
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString("utf-8"));
|
||||
const response = JSON.parse(base64URLDecode(jwsParts[1]).toString("utf-8"));
|
||||
const signature = jwsParts[2];
|
||||
if (!verificationData.equals(Buffer.from(response.nonce, "base64"))) {
|
||||
throw new Error("invalid nonce");
|
||||
}
|
||||
const certificateChain = header.x5c.map((key)=>PEMString(key)).concat([
|
||||
GSR2
|
||||
]);
|
||||
if (getCertSubject(certificateChain[0]).CN !== "attest.android.com") {
|
||||
throw new Error("invalid common name");
|
||||
}
|
||||
if (!verifyCertificateChain(certificateChain)) {
|
||||
throw new Error("Invalid certificate chain!");
|
||||
}
|
||||
const signatureBase = Buffer.from(`${jwsParts[0]}.${jwsParts[1]}`, "utf-8");
|
||||
const valid = crypto.createVerify("sha256").update(signatureBase).verify(certificateChain[0], base64URLDecode(signature));
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
valid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
packed: {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
if (attStmt.x5c) {
|
||||
const attCert = attStmt.x5c[0];
|
||||
const validSignature = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyData = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
} else if (attStmt.ecdaaKeyId) {
|
||||
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
|
||||
throw new Error("ECDAA-Verify is not supported");
|
||||
} else {
|
||||
if (attStmt.alg !== -7) throw new Error("alg mismatch");
|
||||
throw new Error("self attestation is not supported");
|
||||
}
|
||||
}
|
||||
},
|
||||
"fido-u2f": {
|
||||
verify ({ attStmt, authenticatorData, clientDataHash, publicKey, rpIdHash, credentialId }) {
|
||||
const x5c = attStmt.x5c;
|
||||
if (x5c.length !== 1) {
|
||||
throw new Error("x5c length does not match expectation");
|
||||
}
|
||||
const attCert = x5c[0];
|
||||
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
||||
const negTwo = publicKey.get(-2);
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error("invalid or no -2 key given");
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error("invalid or no -3 key given");
|
||||
}
|
||||
const publicKeyU2F = Buffer.concat([
|
||||
ECC_PRELUDE,
|
||||
negTwo,
|
||||
negThree
|
||||
], 1 + 32 + 32);
|
||||
const verificationData = Buffer.concat([
|
||||
NULL_BYTE,
|
||||
rpIdHash,
|
||||
clientDataHash,
|
||||
credentialId,
|
||||
publicKeyU2F
|
||||
]);
|
||||
const validSignature = crypto.createVerify("SHA256").update(verificationData).verify(PEMString(attCert), attStmt.sig);
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyU2F
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { UserIps } from "../../models/index.js";
|
||||
import { fetchMeta } from "../../misc/fetch-meta.js";
|
||||
import authenticate, { AuthenticationError } from "./authenticate.js";
|
||||
import call from "./call.js";
|
||||
import { ApiError } from "./error.js";
|
||||
const userIpHistories = new Map();
|
||||
setInterval(()=>{
|
||||
userIpHistories.clear();
|
||||
}, 1000 * 60 * 60);
|
||||
export default ((endpoint, ctx)=>new Promise((res)=>{
|
||||
const body = ctx.is("multipart/form-data") ? ctx.request.body : ctx.method === "GET" ? ctx.query : ctx.request.body;
|
||||
const reply = (x, y)=>{
|
||||
if (x == null) {
|
||||
ctx.status = 204;
|
||||
} else if (typeof x === "number" && y) {
|
||||
ctx.status = x;
|
||||
ctx.body = {
|
||||
error: {
|
||||
message: y.message,
|
||||
code: y.code,
|
||||
id: y.id,
|
||||
kind: y.kind,
|
||||
...y.info ? {
|
||||
info: y.info
|
||||
} : {}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||
ctx.body = typeof x === "string" ? JSON.stringify(x) : x;
|
||||
}
|
||||
res();
|
||||
};
|
||||
// Authentication
|
||||
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
||||
authenticate(ctx.headers.authorization, ctx.method === "GET" ? null : body["i"]).then(([user, app])=>{
|
||||
// API invoking
|
||||
call(endpoint.name, user, app, body, ctx).then((res)=>{
|
||||
if (ctx.method === "GET" && endpoint.meta.cacheSec && !body["i"] && !user) {
|
||||
ctx.set("Cache-Control", `public, max-age=${endpoint.meta.cacheSec}`);
|
||||
}
|
||||
reply(res);
|
||||
}).catch((e)=>{
|
||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === "client" ? 400 : 500, e);
|
||||
});
|
||||
// Log IP
|
||||
if (user) {
|
||||
fetchMeta().then((meta)=>{
|
||||
if (!meta.enableIpLogging) return;
|
||||
const ip = ctx.ip;
|
||||
const ips = userIpHistories.get(user.id);
|
||||
if (ips == null || !ips.has(ip)) {
|
||||
if (ips == null) {
|
||||
userIpHistories.set(user.id, new Set([
|
||||
ip
|
||||
]));
|
||||
} else {
|
||||
ips.add(ip);
|
||||
}
|
||||
try {
|
||||
UserIps.createQueryBuilder().insert().values({
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ip
|
||||
}).orIgnore(true).execute();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((e)=>{
|
||||
if (e instanceof AuthenticationError) {
|
||||
ctx.response.status = 403;
|
||||
ctx.response.set("WWW-Authenticate", "Bearer");
|
||||
ctx.response.body = {
|
||||
message: `Authentication failed: ${e.message}`,
|
||||
code: "AUTHENTICATION_FAILED",
|
||||
id: "b0a7f5f8-dc2f-4171-b91f-de88ad238e14",
|
||||
kind: "client"
|
||||
};
|
||||
res();
|
||||
} else {
|
||||
reply(500, new ApiError());
|
||||
}
|
||||
});
|
||||
}));
|
||||
@@ -0,0 +1,87 @@
|
||||
import isNativeToken from "./common/is-native-token.js";
|
||||
import { Users, AccessTokens, Apps } from "../../models/index.js";
|
||||
import { Cache } from "../../misc/cache.js";
|
||||
import { localUserByIdCache, localUserByNativeTokenCache } from "../../services/user-cache.js";
|
||||
const appCache = new Cache("app", 60 * 30);
|
||||
export class AuthenticationError extends Error {
|
||||
constructor(message){
|
||||
super(message);
|
||||
this.name = "AuthenticationError";
|
||||
}
|
||||
}
|
||||
export default (async (authorization, bodyToken, bypassUserCache = false)=>{
|
||||
let token = null;
|
||||
// check if there is an authorization header set
|
||||
if (authorization != null) {
|
||||
if (bodyToken != null) {
|
||||
throw new AuthenticationError("using multiple authorization schemes");
|
||||
}
|
||||
// check if OAuth 2.0 Bearer tokens are being used
|
||||
// Authorization schemes are case insensitive
|
||||
if (authorization.substring(0, 7).toLowerCase() === "bearer ") {
|
||||
token = authorization.substring(7);
|
||||
} else {
|
||||
throw new AuthenticationError("unsupported authentication scheme");
|
||||
}
|
||||
} else if (bodyToken != null) {
|
||||
token = bodyToken;
|
||||
} else {
|
||||
return [
|
||||
null,
|
||||
null
|
||||
];
|
||||
}
|
||||
if (isNativeToken(token)) {
|
||||
const user = bypassUserCache ? await Users.findOneBy({
|
||||
token
|
||||
}) : await localUserByNativeTokenCache.fetch(token, ()=>Users.findOneBy({
|
||||
token: token ?? undefined
|
||||
}), true);
|
||||
if (user == null) {
|
||||
throw new AuthenticationError("unknown token");
|
||||
}
|
||||
return [
|
||||
user,
|
||||
null
|
||||
];
|
||||
} else {
|
||||
const accessToken = await AccessTokens.findOne({
|
||||
where: [
|
||||
{
|
||||
hash: token.toLowerCase()
|
||||
},
|
||||
{
|
||||
token: token
|
||||
}
|
||||
]
|
||||
});
|
||||
if (accessToken == null) {
|
||||
throw new AuthenticationError("unknown token");
|
||||
}
|
||||
AccessTokens.update(accessToken.id, {
|
||||
lastUsedAt: new Date()
|
||||
});
|
||||
const user = bypassUserCache ? await Users.findOneBy({
|
||||
id: accessToken.userId
|
||||
}) : await localUserByIdCache.fetch(accessToken.userId, ()=>Users.findOneBy({
|
||||
id: accessToken.userId
|
||||
}), true);
|
||||
if (accessToken.appId) {
|
||||
const app = await appCache.fetch(accessToken.appId, ()=>Apps.findOneByOrFail({
|
||||
id: accessToken.appId
|
||||
}), true);
|
||||
return [
|
||||
user,
|
||||
{
|
||||
id: accessToken.id,
|
||||
permission: app.permission
|
||||
}
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
user,
|
||||
accessToken
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { performance } from "perf_hooks";
|
||||
import { getIpHash } from "../../misc/get-ip-hash.js";
|
||||
import { limiter } from "./limiter.js";
|
||||
import endpoints from "./endpoints.js";
|
||||
import compatibility from "./compatibility.js";
|
||||
import { ApiError } from "./error.js";
|
||||
import { apiLogger } from "./logger.js";
|
||||
import { fetchMeta } from "../../misc/fetch-meta.js";
|
||||
const accessDenied = {
|
||||
message: "Access denied.",
|
||||
code: "ACCESS_DENIED",
|
||||
id: "56f35758-7dd5-468b-8439-5d6fb8ec9b8e"
|
||||
};
|
||||
export default (async (endpoint, user, token, data, ctx)=>{
|
||||
const isSecure = user != null && token == null;
|
||||
const isModerator = user != null && (user.isModerator || user.isAdmin);
|
||||
const ep = endpoints.find((e)=>e.name === endpoint) || compatibility.find((e)=>e.name === endpoint);
|
||||
if (ep == null) {
|
||||
throw new ApiError({
|
||||
message: "No such endpoint.",
|
||||
code: "NO_SUCH_ENDPOINT",
|
||||
id: "f8080b67-5f9c-4eb7-8c18-7f1eeae8f709",
|
||||
httpStatusCode: 404
|
||||
});
|
||||
}
|
||||
if (ep.meta.secure && !isSecure) {
|
||||
throw new ApiError(accessDenied);
|
||||
}
|
||||
if (ep.meta.limit) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
limitActor = getIpHash(ctx.ip);
|
||||
}
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
if (!limit.key) {
|
||||
limit.key = ep.name;
|
||||
}
|
||||
// Rate limit
|
||||
await limiter(limit, limitActor).catch((e)=>{
|
||||
const remainingTime = e.remainingTime ? `Please try again in ${e.remainingTime}.` : "Please try again later.";
|
||||
throw new ApiError({
|
||||
message: `Rate limit exceeded. ${remainingTime}`,
|
||||
code: "RATE_LIMIT_EXCEEDED",
|
||||
id: "d5826d14-3982-4d2e-8011-b9e9f02499ef",
|
||||
httpStatusCode: 429
|
||||
});
|
||||
});
|
||||
}
|
||||
if (ep.meta.requireCredential && user == null) {
|
||||
throw new ApiError({
|
||||
message: "Credential required.",
|
||||
code: "CREDENTIAL_REQUIRED",
|
||||
id: "1384574d-a912-4b81-8601-c7b1c4085df1",
|
||||
httpStatusCode: 401
|
||||
});
|
||||
}
|
||||
if (ep.meta.requireCredential && user.isSuspended) {
|
||||
throw new ApiError({
|
||||
message: "Your account has been suspended.",
|
||||
code: "YOUR_ACCOUNT_SUSPENDED",
|
||||
id: "a8c724b3-6e9c-4b46-b1a8-bc3ed6258370",
|
||||
httpStatusCode: 403
|
||||
});
|
||||
}
|
||||
if (ep.meta.requireAdmin && !user.isAdmin) {
|
||||
throw new ApiError(accessDenied, {
|
||||
reason: "You are not an admin."
|
||||
});
|
||||
}
|
||||
if (ep.meta.requireModerator && !isModerator) {
|
||||
throw new ApiError(accessDenied, {
|
||||
reason: "You are not a moderator."
|
||||
});
|
||||
}
|
||||
if (token && ep.meta.kind && !token.permission.some((p)=>p === ep.meta.kind)) {
|
||||
throw new ApiError({
|
||||
message: "Your app does not have the necessary permissions to use this endpoint.",
|
||||
code: "PERMISSION_DENIED",
|
||||
id: "1370e5b7-d4eb-4566-bb1d-7748ee6a1838"
|
||||
});
|
||||
}
|
||||
// private mode
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode && ep.meta.requireCredentialPrivateMode && user == null) {
|
||||
throw new ApiError({
|
||||
message: "Credential required.",
|
||||
code: "CREDENTIAL_REQUIRED",
|
||||
id: "1384574d-a912-4b81-8601-c7b1c4085df1",
|
||||
httpStatusCode: 401
|
||||
});
|
||||
}
|
||||
// Cast non JSON input
|
||||
if ((ep.meta.requireFile || ctx?.method === "GET") && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)){
|
||||
const param = ep.params.properties[k];
|
||||
if ([
|
||||
"boolean",
|
||||
"number",
|
||||
"integer"
|
||||
].includes(param.type ?? "") && typeof data[k] === "string") {
|
||||
try {
|
||||
data[k] = JSON.parse(data[k]);
|
||||
} catch (e) {
|
||||
throw new ApiError({
|
||||
message: "Invalid param.",
|
||||
code: "INVALID_PARAM",
|
||||
id: "0b5f1631-7c1a-41a6-b399-cce335f34d85"
|
||||
}, {
|
||||
param: k,
|
||||
reason: `cannot cast to ${param.type}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// API invoking
|
||||
const before = performance.now();
|
||||
return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e)=>{
|
||||
if (e instanceof ApiError) {
|
||||
throw e;
|
||||
} else {
|
||||
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, {
|
||||
ep: ep.name,
|
||||
ps: data,
|
||||
e: {
|
||||
message: e.message,
|
||||
code: e.name,
|
||||
stack: e.stack
|
||||
}
|
||||
});
|
||||
throw new ApiError(null, {
|
||||
e: {
|
||||
message: e.message,
|
||||
code: e.name,
|
||||
stack: e.stack
|
||||
}
|
||||
});
|
||||
}
|
||||
}).finally(()=>{
|
||||
const after = performance.now();
|
||||
const time = after - before;
|
||||
if (time > 1000) {
|
||||
apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Blockings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
export function generateBlockedUserQuery(q, me) {
|
||||
const blockingQuery = Blockings.createQueryBuilder("blocking").select("blocking.blockerId").where("blocking.blockeeId = :blockeeId", {
|
||||
blockeeId: me.id
|
||||
}).andWhere("blocking.groupId IS NULL");
|
||||
const groupBlockingQuery = Blockings.createQueryBuilder("blocking").select("blocking.groupId").where("blocking.blockeeId = :groupBlockeeId", {
|
||||
groupBlockeeId: me.id
|
||||
}).andWhere("blocking.groupId IS NOT NULL");
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
// 投稿の引用元の作者にブロックされていない
|
||||
q.andWhere(`note.userId NOT IN (${blockingQuery.getQuery()})`).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.groupId IS NULL").orWhere(`note.groupId NOT IN (${groupBlockingQuery.getQuery()})`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyUserId IS NULL").orWhere(`note.replyUserId NOT IN (${blockingQuery.getQuery()})`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.renoteUserId IS NULL").orWhere(`note.renoteUserId NOT IN (${blockingQuery.getQuery()})`);
|
||||
}));
|
||||
q.setParameters(blockingQuery.getParameters());
|
||||
q.setParameters(groupBlockingQuery.getParameters());
|
||||
}
|
||||
export function generateBlockQueryForUsers(q, me) {
|
||||
const blockingQuery = Blockings.createQueryBuilder("blocking").select("blocking.blockeeId").where("blocking.blockerId = :blockerId", {
|
||||
blockerId: me.id
|
||||
});
|
||||
const blockedQuery = Blockings.createQueryBuilder("blocking").select("blocking.blockerId").where("blocking.blockeeId = :blockeeId", {
|
||||
blockeeId: me.id
|
||||
});
|
||||
q.andWhere(`user.id NOT IN (${blockingQuery.getQuery()})`);
|
||||
q.setParameters(blockingQuery.getParameters());
|
||||
q.andWhere(`user.id NOT IN (${blockedQuery.getQuery()})`);
|
||||
q.setParameters(blockedQuery.getParameters());
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ChannelFollowings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateChannelQuery(q, me) {
|
||||
if (me == null) {
|
||||
q.andWhere("note.channelId IS NULL");
|
||||
} else {
|
||||
q.leftJoinAndSelect("note.channel", "channel");
|
||||
const channelFollowingQuery = ChannelFollowings.createQueryBuilder("channelFollowing").select("channelFollowing.followeeId").where("channelFollowing.followerId = :followerId", {
|
||||
followerId: me.id
|
||||
});
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb// チャンネルのノートではない
|
||||
.where("note.channelId IS NULL")// または自分がフォローしているチャンネルのノート
|
||||
.orWhere(`note.channelId IN (${channelFollowingQuery.getQuery()})`);
|
||||
}));
|
||||
q.setParameters(channelFollowingQuery.getParameters());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function generateExcludeMemorietQuery(query) {
|
||||
query.andWhere(`NOT EXISTS (SELECT 1 FROM "memoriet" "memoriet_exclude" WHERE "memoriet_exclude"."noteId" = note.id)`);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { Followings, Notes } from "../../../models/index.js";
|
||||
import { Cache } from "../../../misc/cache.js";
|
||||
import { apiLogger } from "../logger.js";
|
||||
export const cache = new Cache("homeTlQueryData", 60 * 60 * 24);
|
||||
const cutoff = 250; // 250 posts in the last 7 days, constant determined by comparing benchmarks for cutoff values between 100 and 2500
|
||||
const logger = apiLogger.createSubLogger("heuristics");
|
||||
export async function generateFollowingQuery(q, me) {
|
||||
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :meId");
|
||||
const heuristic = await cache.fetch(me.id, async ()=>{
|
||||
let curr = new Date();
|
||||
let prev = new Date();
|
||||
prev.setDate(prev.getDate() - 7);
|
||||
return Notes.createQueryBuilder('note').where(`note.createdAt > :prev`, {
|
||||
prev
|
||||
}).andWhere(`note.createdAt < :curr`, {
|
||||
curr
|
||||
}).andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
qb.orWhere(`note.userId = :meId`, {
|
||||
meId: me.id
|
||||
});
|
||||
})).getCount().then((res)=>{
|
||||
logger.info(`Calculating heuristics for user ${me.id} took ${new Date().getTime() - curr.getTime()}ms`);
|
||||
return res;
|
||||
});
|
||||
});
|
||||
const shouldUseUnion = heuristic < cutoff;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
if (shouldUseUnion) {
|
||||
qb.where(`note.userId = ANY(array(${followingQuery.getQuery()} UNION ALL VALUES (:meId)))`);
|
||||
} else {
|
||||
qb.where(`note.userId = :meId`);
|
||||
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
}
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { sqlLikeEscape } from "../../../misc/sql-like-escape.js";
|
||||
import { sqlRegexEscape } from "../../../misc/sql-regex-escape.js";
|
||||
import { Followings, NoteFavorites, NoteReactions, Users } from "../../../models/index.js";
|
||||
const filters = {
|
||||
"from": fromFilter,
|
||||
"-from": fromFilterInverse,
|
||||
"mention": mentionFilter,
|
||||
"-mention": mentionFilterInverse,
|
||||
"reply": replyFilter,
|
||||
"-reply": replyFilterInverse,
|
||||
"to": replyFilter,
|
||||
"-to": replyFilterInverse,
|
||||
"before": beforeFilter,
|
||||
"until": beforeFilter,
|
||||
"after": afterFilter,
|
||||
"since": afterFilter,
|
||||
"instance": instanceFilter,
|
||||
"-instance": instanceFilterInverse,
|
||||
"domain": instanceFilter,
|
||||
"-domain": instanceFilterInverse,
|
||||
"host": instanceFilter,
|
||||
"-host": instanceFilterInverse,
|
||||
"filter": miscFilter,
|
||||
"-filter": miscFilterInverse,
|
||||
"in": inFilter,
|
||||
"-in": inFilterInverse,
|
||||
"has": attachmentFilter
|
||||
};
|
||||
export function generateFtsQuery(query, q) {
|
||||
const components = q.trim().split(" ");
|
||||
const terms = [];
|
||||
let finalTerms = [];
|
||||
let counter = 0;
|
||||
let caseSensitive = false;
|
||||
let matchWords = false;
|
||||
for (const component of components){
|
||||
const split = component.split(":");
|
||||
if (split.length > 1 && filters[split[0]] !== undefined) filters[split[0]](query, split.slice(1).join(":"), counter++);
|
||||
else if (split.length > 1 && (split[0] === "search" || split[0] === "match")) matchWords = split[1] === 'word' || split[1] === 'words';
|
||||
else if (split.length > 1 && split[0] === "case") caseSensitive = split[1] === 'sensitive';
|
||||
else terms.push(component);
|
||||
}
|
||||
let idx = 0;
|
||||
let state = 'idle';
|
||||
for(let i = 0; i < terms.length; i++){
|
||||
if (state === 'idle') {
|
||||
if (terms[i].startsWith('"') && terms[i].endsWith('"') || terms[i].startsWith('(') && terms[i].endsWith(')')) {
|
||||
finalTerms.push(trimStartAndEnd(terms[i]));
|
||||
} else if (terms[i].startsWith('"')) {
|
||||
idx = i;
|
||||
state = 'quote';
|
||||
} else if (terms[i].startsWith('(')) {
|
||||
idx = i;
|
||||
state = 'parenthesis';
|
||||
} else {
|
||||
finalTerms.push(terms[i]);
|
||||
}
|
||||
} else if (state === 'quote' && terms[i].endsWith('"')) {
|
||||
finalTerms.push(extractToken(terms, idx, i));
|
||||
state = 'idle';
|
||||
} else if (state === 'parenthesis' && terms[i].endsWith(')')) {
|
||||
query.andWhere(new Brackets((qb)=>{
|
||||
for (const term of extractToken(terms, idx, i).split(' OR ')){
|
||||
const id = counter++;
|
||||
appendSearchQuery(term, 'or', query, qb, id, term.startsWith('-'), matchWords, caseSensitive);
|
||||
}
|
||||
}));
|
||||
state = 'idle';
|
||||
}
|
||||
}
|
||||
if (state != "idle") {
|
||||
finalTerms.push(...extractToken(terms, idx, terms.length - 1, false).substring(1).split(' '));
|
||||
}
|
||||
for (const term of finalTerms){
|
||||
const id = counter++;
|
||||
appendSearchQuery(term, 'and', query, query, id, term.startsWith('-'), matchWords, caseSensitive);
|
||||
}
|
||||
}
|
||||
function fromFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function fromFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function mentionFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true
|
||||
});
|
||||
query.andWhere(`note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[]`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function mentionFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true
|
||||
});
|
||||
query.andWhere(`NOT (note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[])`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function replyFilter(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function replyFilterInverse(query, filter, id) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
function beforeFilter(query, filter) {
|
||||
query.andWhere('note.createdAt < :before', {
|
||||
before: filter
|
||||
});
|
||||
}
|
||||
function afterFilter(query, filter) {
|
||||
query.andWhere('note.createdAt > :after', {
|
||||
after: filter
|
||||
});
|
||||
}
|
||||
function instanceFilter(query, filter, id) {
|
||||
if (filter === 'local') {
|
||||
query.andWhere(`note.userHost IS NULL`);
|
||||
} else {
|
||||
query.andWhere(`note.userHost = :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
function instanceFilterInverse(query, filter, id) {
|
||||
if (filter === 'local') {
|
||||
query.andWhere(`note.userHost IS NOT NULL`);
|
||||
} else {
|
||||
query.andWhere(`note.userHost <> :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
function miscFilter(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'followers') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followerId').where('following.followeeId = :meId');
|
||||
} else if (filter === 'following') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followeeId').where('following.followerId = :meId');
|
||||
} else if (filter === 'replies' || filter === 'reply') {
|
||||
query.andWhere('note.replyId IS NOT NULL');
|
||||
} else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') {
|
||||
query.andWhere('note.renoteId IS NOT NULL');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.userId IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function miscFilterInverse(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'followers') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followerId').where('following.followeeId = :meId');
|
||||
} else if (filter === 'following') {
|
||||
subQuery = Followings.createQueryBuilder('following').select('following.followeeId').where('following.followerId = :meId');
|
||||
} else if (filter === 'replies' || filter === 'reply') {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
} else if (filter === 'boosts' || filter === 'boost' || filter === 'renotes' || filter === 'renote') {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.userId NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function inFilter(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'bookmarks') {
|
||||
subQuery = NoteFavorites.createQueryBuilder('bookmark').select('bookmark.noteId').where('bookmark.userId = :meId');
|
||||
} else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') {
|
||||
subQuery = NoteReactions.createQueryBuilder('react').select('react.noteId').where('react.userId = :meId');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.id IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function inFilterInverse(query, filter) {
|
||||
let subQuery = null;
|
||||
if (filter === 'bookmarks') {
|
||||
subQuery = NoteFavorites.createQueryBuilder('bookmark').select('bookmark.noteId').where('bookmark.userId = :meId');
|
||||
} else if (filter === 'favorites' || filter === 'favourites' || filter === 'reactions' || filter === 'likes') {
|
||||
subQuery = NoteReactions.createQueryBuilder('react').select('react.noteId').where('react.userId = :meId');
|
||||
}
|
||||
if (subQuery !== null) query.andWhere(`note.id NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
function attachmentFilter(query, filter) {
|
||||
switch(filter){
|
||||
case 'image':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%image/%'`);
|
||||
break;
|
||||
case 'video':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%video/%'`);
|
||||
break;
|
||||
case 'audio':
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%audio/%'`);
|
||||
break;
|
||||
case 'file':
|
||||
query.andWhere(`note."attachedFileTypes" <> '{}'`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%image/%')`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%video/%')`);
|
||||
query.andWhere(`NOT (note."attachedFileTypes"::varchar ILIKE '%audio/%')`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
function generateUserSubquery(filter, id) {
|
||||
if (filter.startsWith('@')) filter = filter.substring(1);
|
||||
const split = filter.split('@');
|
||||
const query = Users.createQueryBuilder('user').select('user.id').where(`user.usernameLower = :user_${id}`).andWhere(`user.host ${split[1] !== undefined ? `= :host_${id}` : 'IS NULL'}`);
|
||||
query.setParameter(`user_${id}`, split[0].toLowerCase());
|
||||
if (split[1] !== undefined) query.setParameter(`host_${id}`, split[1].toLowerCase());
|
||||
return query;
|
||||
}
|
||||
function extractToken(array, start, end, trim = true) {
|
||||
const slice = array.slice(start, end + 1).join(" ");
|
||||
return trim ? trimStartAndEnd(slice) : slice;
|
||||
}
|
||||
function trimStartAndEnd(str) {
|
||||
return str.substring(1, str.length - 1);
|
||||
}
|
||||
function appendSearchQuery(term, mode, query, qb, id, negate, matchWords, caseSensitive) {
|
||||
const sql = `note.text ${getSearchMatchOperator(negate, matchWords, caseSensitive)} :q_${id}`;
|
||||
if (mode === 'and') qb.andWhere(sql);
|
||||
else if (mode === 'or') qb.orWhere(sql);
|
||||
query.setParameter(`q_${id}`, escapeSqlSearchParam(term.substring(negate ? 1 : 0), matchWords));
|
||||
}
|
||||
function getSearchMatchOperator(negate, matchWords, caseSensitive) {
|
||||
const negatePrefix = matchWords ? '!' : 'NOT ';
|
||||
return `${negate ? negatePrefix : ''}${matchWords ? caseSensitive ? '~' : '~*' : caseSensitive ? 'LIKE' : 'ILIKE'}`;
|
||||
}
|
||||
function escapeSqlSearchParam(param, matchWords) {
|
||||
return matchWords ? `\\y${sqlRegexEscape(param)}\\y` : `%${sqlLikeEscape(param)}%`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { UserListJoinings, UserLists } from "../../../models/index.js";
|
||||
export function generateListQuery(q, me) {
|
||||
const listQuery = UserLists.createQueryBuilder("list").select("list.id").where("list.hideFromHomeTl = TRUE").andWhere("list.userId = :meId");
|
||||
const memberQuery = UserListJoinings.createQueryBuilder("member").select("member.userId").where(`member.userListId IN (${listQuery.getQuery()})`);
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.userId = :meId`);
|
||||
qb.orWhere(`note.userId NOT IN (${memberQuery.getQuery()})`);
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Brackets } from "typeorm";
|
||||
export function shouldHideEUsersFor(viewer) {
|
||||
return !!viewer && !viewer.isAdmin && !viewer.isModerator && (viewer.minorBadges ?? []).some((badge)=>badge === "K" || badge === "T");
|
||||
}
|
||||
export function generateMinorBadgeUserVisibilityQuery(q, viewer, alias = "user") {
|
||||
if (!shouldHideEUsersFor(viewer)) return;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`${alias}.id = :minorBadgeViewerId`).orWhere(`NOT ('E' = ANY(${alias}."minorBadges"))`);
|
||||
}));
|
||||
q.setParameter("minorBadgeViewerId", viewer.id);
|
||||
}
|
||||
export function generateMinorBadgeNoteVisibilityQuery(q, viewer, noteAlias = "note") {
|
||||
if (!shouldHideEUsersFor(viewer)) return;
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`${noteAlias}."userId" = :minorBadgeViewerId`).orWhere(`${noteAlias}."userId" NOT IN (` + `SELECT "id" FROM "user" WHERE 'E' = ANY("minorBadges")` + `)`);
|
||||
}));
|
||||
q.setParameter("minorBadgeViewerId", viewer.id);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NoteThreadMutings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateMutedNoteThreadQuery(q, me) {
|
||||
const mutedQuery = NoteThreadMutings.createQueryBuilder("threadMuted").select("threadMuted.threadId").where("threadMuted.userId = :userId", {
|
||||
userId: me.id
|
||||
});
|
||||
q.andWhere(`note.id NOT IN (${mutedQuery.getQuery()})`);
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.threadId IS NULL").orWhere(`note.threadId NOT IN (${mutedQuery.getQuery()})`);
|
||||
}));
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { Mutings, UserProfiles } from "../../../models/index.js";
|
||||
export function generateMutedUserQuery(q, me, exclude) {
|
||||
const mutingQuery = Mutings.createQueryBuilder("muting").select("muting.muteeId").where("muting.muterId = :muterId", {
|
||||
muterId: me.id
|
||||
});
|
||||
if (exclude) {
|
||||
mutingQuery.andWhere("muting.muteeId != :excludeId", {
|
||||
excludeId: exclude.id
|
||||
});
|
||||
}
|
||||
const mutingInstanceQuery = UserProfiles.createQueryBuilder("user_profile").select("user_profile.mutedInstances").where("user_profile.userId = :muterId", {
|
||||
muterId: me.id
|
||||
});
|
||||
// 投稿の作者をミュートしていない かつ
|
||||
// 投稿の返信先の作者をミュートしていない かつ
|
||||
// 投稿の引用元の作者をミュートしていない
|
||||
q.andWhere(`note.userId NOT IN (${mutingQuery.getQuery()})`).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyUserId IS NULL").orWhere(`note.replyUserId NOT IN (${mutingQuery.getQuery()})`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.renoteUserId IS NULL").orWhere(`note.renoteUserId NOT IN (${mutingQuery.getQuery()})`);
|
||||
}))// mute instances
|
||||
.andWhere(new Brackets((qb)=>{
|
||||
qb.andWhere("note.userHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.userHost)`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyUserHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.replyUserHost)`);
|
||||
})).andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.renoteUserHost IS NULL").orWhere(`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.renoteUserHost)`);
|
||||
}));
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
q.setParameters(mutingInstanceQuery.getParameters());
|
||||
}
|
||||
export function generateMutedUserQueryForUsers(q, me) {
|
||||
const mutingQuery = Mutings.createQueryBuilder("muting").select("muting.muteeId").where("muting.muterId = :muterId", {
|
||||
muterId: me.id
|
||||
});
|
||||
q.andWhere(`user.id NOT IN (${mutingQuery.getQuery()})`);
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { secureRndstr } from "../../../misc/secure-rndstr.js";
|
||||
export default (()=>secureRndstr(16));
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Brackets } from "typeorm";
|
||||
export function generateRepliesQuery(q, withReplies, me) {
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyId IS NULL") // 返信ではない
|
||||
.orWhere(new Brackets((qb)=>{
|
||||
qb.where(// 返信だけど投稿者自身への返信
|
||||
"note.replyId IS NOT NULL").andWhere("note.replyUserId = note.userId");
|
||||
}));
|
||||
}));
|
||||
} else if (!withReplies) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyId IS NULL") // 返信ではない
|
||||
.orWhere("note.replyUserId = :meId", {
|
||||
meId: me.id
|
||||
}) // 返信だけど自分のノートへの返信
|
||||
.orWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyId IS NOT NULL") // 返信だけど自分の行った返信
|
||||
.andWhere("note.userId = :meId", {
|
||||
meId: me.id
|
||||
});
|
||||
})).orWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyId IS NOT NULL") // 返信だけど投稿者自身への返信
|
||||
.andWhere("note.replyUserId = note.userId");
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Followings } from "../../../models/index.js";
|
||||
import { Brackets } from "typeorm";
|
||||
import { generateMinorBadgeNoteVisibilityQuery } from "./generate-minor-badge-visibility-query.js";
|
||||
export function generateVisibilityQuery(q, me, options) {
|
||||
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
|
||||
if (me == null) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility = 'public'`).orWhere(`note.visibility = 'home'`);
|
||||
})).andWhere('note.localOnly = FALSE');
|
||||
} else {
|
||||
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :meId");
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb// 公開投稿である
|
||||
.where(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility = 'public'`).orWhere(`note.visibility = 'home'`);
|
||||
}))// または 自分自身
|
||||
.orWhere("note.userId = :meId")// または 自分宛て
|
||||
.orWhere(":meId = ANY(note.visibleUserIds)").orWhere(":meId = ANY(note.mentions)").orWhere(new Brackets((qb)=>{
|
||||
qb// または フォロワー宛ての投稿であり、
|
||||
.where(`note.visibility = 'followers'`).andWhere(new Brackets((qb)=>{
|
||||
qb// 自分がフォロワーである
|
||||
.where(`note.userId IN (${followingQuery.getQuery()})`)// または 自分の投稿へのリプライ
|
||||
.orWhere("note.replyUserId = :meId");
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`note.visibility != 'hidden'`).orWhere(`note.userId = :meId`);
|
||||
}));
|
||||
q.setParameters({
|
||||
meId: me.id
|
||||
});
|
||||
}
|
||||
if (!options?.allowAdservice) {
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(`NOT ('adservice' = ANY(note.tags))`);
|
||||
if (me) {
|
||||
qb.orWhere("note.userId = :meId");
|
||||
}
|
||||
}));
|
||||
}
|
||||
generateMinorBadgeNoteVisibilityQuery(q, me);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Brackets } from "typeorm";
|
||||
import { RenoteMutings } from "../../../models/index.js";
|
||||
export function generateMutedUserRenotesQueryForNotes(q, me) {
|
||||
const mutingQuery = RenoteMutings.createQueryBuilder("renote_muting").select("renote_muting.muteeId").where("renote_muting.muterId = :muterId", {
|
||||
muterId: me.id
|
||||
});
|
||||
q.andWhere(new Brackets((qb)=>{
|
||||
qb.where(new Brackets((qb)=>{
|
||||
qb.where("note.renoteId IS NOT NULL");
|
||||
qb.andWhere("note.text IS NULL");
|
||||
qb.andWhere(`note.userId NOT IN (${mutingQuery.getQuery()})`);
|
||||
})).orWhere("note.renoteId IS NULL").orWhere("note.text IS NOT NULL");
|
||||
}));
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UserGroupJoinings, UserGroups } from "../../../models/index.js";
|
||||
export async function getGroupActor(groupId, user) {
|
||||
if (groupId == null) return null;
|
||||
const group = await UserGroups.findOneBy({
|
||||
id: groupId
|
||||
});
|
||||
if (group == null) return null;
|
||||
if (group.userId === user.id) return group;
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
userId: user.id,
|
||||
userGroupId: group.id
|
||||
});
|
||||
return joining == null ? null : group;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IdentifiableError } from "../../../misc/identifiable-error.js";
|
||||
import { Notes, Users } from "../../../models/index.js";
|
||||
import { generateVisibilityQuery } from "./generate-visibility-query.js";
|
||||
/**
|
||||
* Get note for API processing, taking into account visibility.
|
||||
*/ export async function getNote(noteId, me, options) {
|
||||
const query = Notes.createQueryBuilder("note").where("note.id = :id", {
|
||||
id: noteId
|
||||
});
|
||||
generateVisibilityQuery(query, me, options);
|
||||
const note = await query.getOne();
|
||||
if (note == null || me == null && note.localOnly) {
|
||||
throw new IdentifiableError("9725d0ce-ba28-4dde-95a7-2cbb2c15de24", "No such note.");
|
||||
}
|
||||
return note;
|
||||
}
|
||||
/**
|
||||
* Get user for API processing
|
||||
*/ export async function getUser(userId) {
|
||||
const user = await Users.findOneBy({
|
||||
id: userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new IdentifiableError("15348ddd-432d-49c2-8a5a-8069753becff", "No such user.");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
/**
|
||||
* Get remote user for API processing
|
||||
*/ export async function getRemoteUser(userId) {
|
||||
const user = await getUser(userId);
|
||||
if (!Users.isRemoteUser(user)) {
|
||||
throw new Error("user is not a remote user");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
/**
|
||||
* Get local user for API processing
|
||||
*/ export async function getLocalUser(userId) {
|
||||
const user = await getUser(userId);
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new Error("user is not a local user");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import rndstr from "rndstr";
|
||||
import { Notes, UserProfiles, NoteReactions } from "../../../models/index.js";
|
||||
import { generateMutedUserQuery } from "./generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "./generate-block-query.js";
|
||||
// TODO: リアクション、Renote、返信などをしたノートは除外する
|
||||
export async function injectFeatured(timeline, user) {
|
||||
if (timeline.length < 5) return;
|
||||
if (user) {
|
||||
const profile = await UserProfiles.findOneByOrFail({
|
||||
userId: user.id
|
||||
});
|
||||
if (!profile.injectFeaturedNote) return;
|
||||
}
|
||||
const max = 30;
|
||||
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||
const query = Notes.createQueryBuilder("note").addSelect("note.score").where("note.userHost IS NULL").andWhere("note.score > 0").andWhere("note.createdAt > :date", {
|
||||
date: new Date(Date.now() - day)
|
||||
}).andWhere(`note.visibility = 'public'`).innerJoinAndSelect("note.user", "user");
|
||||
if (user) {
|
||||
query.andWhere("note.userId != :userId", {
|
||||
userId: user.id
|
||||
});
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
const reactionQuery = NoteReactions.createQueryBuilder("reaction").select("reaction.noteId").where("reaction.userId = :userId", {
|
||||
userId: user.id
|
||||
});
|
||||
query.andWhere(`note.id NOT IN (${reactionQuery.getQuery()})`);
|
||||
}
|
||||
const notes = await query.orderBy("note.score", "DESC").take(max).getMany();
|
||||
if (notes.length === 0) return;
|
||||
// Pick random one
|
||||
const featured = notes[Math.floor(Math.random() * notes.length)];
|
||||
featured._featuredId_ = rndstr("a-z0-9", 8);
|
||||
// Inject featured
|
||||
timeline.splice(3, 0, featured);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import rndstr from "rndstr";
|
||||
import { PromoReads, PromoNotes, Notes, Users } from "../../../models/index.js";
|
||||
import { shouldHideEUsersFor } from "./generate-minor-badge-visibility-query.js";
|
||||
import { readPromo } from "./read-promo.js";
|
||||
const systemTags = new Set([
|
||||
"adservice",
|
||||
"videoservice",
|
||||
"audioservice",
|
||||
"imageservice",
|
||||
"karaokeservice",
|
||||
"lua4frozen"
|
||||
]);
|
||||
function isExplicitAd(note) {
|
||||
return note.tags.includes("explicit") || note.user?.minorBadges?.includes("E") === true;
|
||||
}
|
||||
export async function injectPromo(timeline, user, preferredTag) {
|
||||
// TODO: readやexpireフィルタはクエリ側でやる
|
||||
const readDay = new Date().toISOString().slice(0, 10);
|
||||
const reads = user ? await PromoReads.findBy({
|
||||
userId: user.id,
|
||||
readDay
|
||||
}) : [];
|
||||
let promos = await PromoNotes.find();
|
||||
promos = promos.filter((n)=>n.expiresAt.getTime() > Date.now());
|
||||
promos = promos.filter((n)=>n.remainingCredits > 0);
|
||||
promos = promos.filter((n)=>!reads.map((r)=>r.noteId).includes(n.noteId));
|
||||
if (promos.length === 0) return;
|
||||
const candidates = [];
|
||||
for (const promo of promos){
|
||||
const note = await Notes.findOneBy({
|
||||
id: promo.noteId
|
||||
});
|
||||
if (!note?.tags.includes("adservice")) continue;
|
||||
note.user = await Users.findOneByOrFail({
|
||||
id: note.userId
|
||||
});
|
||||
if (user && shouldHideEUsersFor(user) && isExplicitAd(note)) continue;
|
||||
candidates.push(note);
|
||||
}
|
||||
if (candidates.length === 0) return;
|
||||
const normalizedPreferredTag = preferredTag?.trim().toLowerCase().replace(/^#/, "");
|
||||
const priority = normalizedPreferredTag && !systemTags.has(normalizedPreferredTag) ? candidates.filter((note)=>note.tags.includes(normalizedPreferredTag)) : [];
|
||||
const pool = priority.length > 0 ? priority : candidates;
|
||||
// Pick random promo
|
||||
const note = pool[Math.floor(Math.random() * pool.length)];
|
||||
const promo = promos.find((promo)=>promo.noteId === note.id);
|
||||
note._prId_ = rndstr("a-z0-9", 8);
|
||||
if (user && promo) {
|
||||
await readPromo(note, promo, user);
|
||||
}
|
||||
// Inject promo
|
||||
timeline.splice(Math.min(3, timeline.length), 0, note);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default ((token)=>token.length === 16);
|
||||
@@ -0,0 +1,42 @@
|
||||
export function makePaginationQuery(q, sinceId, untilId, sinceDate, untilDate) {
|
||||
if (sinceId && untilId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, {
|
||||
sinceId: sinceId
|
||||
});
|
||||
q.andWhere(`${q.alias}.id < :untilId`, {
|
||||
untilId: untilId
|
||||
});
|
||||
q.orderBy(`${q.alias}.id`, "DESC");
|
||||
} else if (sinceId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, {
|
||||
sinceId: sinceId
|
||||
});
|
||||
q.orderBy(`${q.alias}.id`, "ASC");
|
||||
} else if (untilId) {
|
||||
q.andWhere(`${q.alias}.id < :untilId`, {
|
||||
untilId: untilId
|
||||
});
|
||||
q.orderBy(`${q.alias}.id`, "DESC");
|
||||
} else if (sinceDate && untilDate) {
|
||||
q.andWhere(`${q.alias}.createdAt > :sinceDate`, {
|
||||
sinceDate: new Date(sinceDate)
|
||||
});
|
||||
q.andWhere(`${q.alias}.createdAt < :untilDate`, {
|
||||
untilDate: new Date(untilDate)
|
||||
});
|
||||
q.orderBy(`${q.alias}.createdAt`, "DESC");
|
||||
} else if (sinceDate) {
|
||||
q.andWhere(`${q.alias}.createdAt > :sinceDate`, {
|
||||
sinceDate: new Date(sinceDate)
|
||||
});
|
||||
q.orderBy(`${q.alias}.createdAt`, "ASC");
|
||||
} else if (untilDate) {
|
||||
q.andWhere(`${q.alias}.createdAt < :untilDate`, {
|
||||
untilDate: new Date(untilDate)
|
||||
});
|
||||
q.orderBy(`${q.alias}.createdAt`, "DESC");
|
||||
} else {
|
||||
q.orderBy(`${q.alias}.id`, "DESC");
|
||||
}
|
||||
return q;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { publishMainStream, publishGroupMessagingStream } from "../../../services/stream.js";
|
||||
import { publishMessagingStream } from "../../../services/stream.js";
|
||||
import { publishMessagingIndexStream } from "../../../services/stream.js";
|
||||
import { pushNotification } from "../../../services/push-notification.js";
|
||||
import { MessagingMessages, UserGroupJoinings, Users } from "../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { IdentifiableError } from "../../../misc/identifiable-error.js";
|
||||
import { toArray } from "../../../prelude/array.js";
|
||||
import { renderReadActivity } from "../../../remote/activitypub/renderer/read.js";
|
||||
import { renderActivity } from "../../../remote/activitypub/renderer/index.js";
|
||||
import { deliver } from "../../../queue/index.js";
|
||||
import orderedCollection from "../../../remote/activitypub/renderer/ordered-collection.js";
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/ export async function readUserMessagingMessage(userId, otherpartyId, messageIds) {
|
||||
if (messageIds.length === 0) return;
|
||||
const messages = await MessagingMessages.findBy({
|
||||
id: In(messageIds)
|
||||
});
|
||||
for (const message of messages){
|
||||
if (message.recipientId !== userId) {
|
||||
throw new IdentifiableError("e140a4bf-49ce-4fb6-b67c-b78dadf6b52f", "Access denied (user).");
|
||||
}
|
||||
}
|
||||
// Update documents
|
||||
await MessagingMessages.update({
|
||||
id: In(messageIds),
|
||||
userId: otherpartyId,
|
||||
recipientId: userId,
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
// Publish event
|
||||
publishMessagingStream(otherpartyId, userId, "read", messageIds);
|
||||
publishMessagingIndexStream(userId, "read", messageIds);
|
||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, "readAllMessagingMessages");
|
||||
pushNotification(userId, "readAllMessagingMessages", undefined);
|
||||
} else {
|
||||
// そのユーザーとのメッセージで未読がなければイベント発行
|
||||
const count = await MessagingMessages.count({
|
||||
where: {
|
||||
userId: otherpartyId,
|
||||
recipientId: userId,
|
||||
isRead: false
|
||||
},
|
||||
take: 1
|
||||
});
|
||||
if (!count) {
|
||||
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
|
||||
userId: otherpartyId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Mark messages as read
|
||||
*/ export async function readGroupMessagingMessage(userId, groupId, messageIds) {
|
||||
if (messageIds.length === 0) return;
|
||||
// check joined
|
||||
const joining = await UserGroupJoinings.findOneBy({
|
||||
userId: userId,
|
||||
userGroupId: groupId
|
||||
});
|
||||
if (joining == null) {
|
||||
throw new IdentifiableError("930a270c-714a-46b2-b776-ad27276dc569", "Access denied (group).");
|
||||
}
|
||||
const messages = await MessagingMessages.findBy({
|
||||
id: In(messageIds)
|
||||
});
|
||||
const reads = [];
|
||||
for (const message of messages){
|
||||
if (message.userId === userId) continue;
|
||||
if (message.reads.includes(userId)) continue;
|
||||
// Update document
|
||||
await MessagingMessages.createQueryBuilder().update().set({
|
||||
reads: ()=>`array_append("reads", '${joining.userId}')`
|
||||
}).where("id = :id", {
|
||||
id: message.id
|
||||
}).execute();
|
||||
reads.push(message.id);
|
||||
}
|
||||
// Publish event
|
||||
publishGroupMessagingStream(groupId, "read", {
|
||||
ids: reads,
|
||||
userId: userId
|
||||
});
|
||||
publishMessagingIndexStream(userId, "read", reads);
|
||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
publishMainStream(userId, "readAllMessagingMessages");
|
||||
pushNotification(userId, "readAllMessagingMessages", undefined);
|
||||
} else {
|
||||
// そのグループにおいて未読がなければイベント発行
|
||||
const unreadExist = await MessagingMessages.createQueryBuilder("message").where("message.groupId = :groupId", {
|
||||
groupId: groupId
|
||||
}).andWhere("message.userId != :userId", {
|
||||
userId: userId
|
||||
}).andWhere("NOT (:userId = ANY(message.reads))", {
|
||||
userId: userId
|
||||
}).andWhere("message.createdAt > :joinedAt", {
|
||||
joinedAt: joining.createdAt
|
||||
}) // 自分が加入する前の会話については、未読扱いしない
|
||||
.getOne().then((x)=>x != null);
|
||||
if (!unreadExist) {
|
||||
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
|
||||
groupId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function deliverReadActivity(user, recipient, messages) {
|
||||
messages = toArray(messages).filter((x)=>x.uri);
|
||||
const contents = messages.map((x)=>renderReadActivity(user, x));
|
||||
if (contents.length > 1) {
|
||||
const collection = orderedCollection(null, contents.length, undefined, undefined, contents);
|
||||
deliver(user, renderActivity(collection), recipient.inbox);
|
||||
} else {
|
||||
for (const content of contents){
|
||||
deliver(user, renderActivity(content), recipient.inbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { In } from "typeorm";
|
||||
import { publishMainStream } from "../../../services/stream.js";
|
||||
import { pushNotification } from "../../../services/push-notification.js";
|
||||
import { Notifications, Users } from "../../../models/index.js";
|
||||
export async function readNotification(userId, notificationIds) {
|
||||
if (notificationIds.length === 0) return;
|
||||
// Update documents
|
||||
const result = await Notifications.update({
|
||||
notifieeId: userId,
|
||||
id: In(notificationIds),
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
if (result.affected === 0) return;
|
||||
if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId);
|
||||
else return postReadNotifications(userId, notificationIds);
|
||||
}
|
||||
export async function readNotificationByQuery(userId, query) {
|
||||
const notificationIds = await Notifications.findBy({
|
||||
...query,
|
||||
notifieeId: userId,
|
||||
isRead: false
|
||||
}).then((notifications)=>notifications.map((notification)=>notification.id));
|
||||
return readNotification(userId, notificationIds);
|
||||
}
|
||||
function postReadAllNotifications(userId) {
|
||||
publishMainStream(userId, "readAllNotifications");
|
||||
return pushNotification(userId, "readAllNotifications", undefined);
|
||||
}
|
||||
function postReadNotifications(userId, notificationIds) {
|
||||
publishMainStream(userId, "readNotifications", notificationIds);
|
||||
return pushNotification(userId, "readNotifications", {
|
||||
notificationIds
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PromoNotes, PromoReads } from "../../../models/index.js";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
export async function readPromo(note, promo, user) {
|
||||
if (promo.expiresAt.getTime() <= Date.now() || promo.remainingCredits <= 0 || promo.userId === user.id || note.userId === user.id || user.isAdmin || user.isModerator || user.isBot) {
|
||||
return;
|
||||
}
|
||||
const readDay = new Date().toISOString().slice(0, 10);
|
||||
const result = await PromoReads.createQueryBuilder().insert().values({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
readDay
|
||||
}).orIgnore().returning("id").execute();
|
||||
if (result.raw.length === 0) {
|
||||
return;
|
||||
}
|
||||
await PromoNotes.createQueryBuilder().update().set({
|
||||
remainingCredits: ()=>`"remainingCredits" - 1`
|
||||
}).where(`"noteId" = :noteId`, {
|
||||
noteId: note.id
|
||||
}).andWhere(`"remainingCredits" > 0`).execute();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import config from "../../../config/index.js";
|
||||
import { Signins } from "../../../models/index.js";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
import { publishMainStream } from "../../../services/stream.js";
|
||||
export default function(ctx, user, redirect = false) {
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
ctx.cookies.set("igi", user.token, {
|
||||
path: "/",
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
secure: config.url.startsWith("https"),
|
||||
httpOnly: false
|
||||
});
|
||||
//#endregion
|
||||
ctx.redirect(config.url);
|
||||
} else {
|
||||
ctx.body = {
|
||||
id: user.id,
|
||||
i: user.token
|
||||
};
|
||||
ctx.status = 200;
|
||||
}
|
||||
(async ()=>{
|
||||
// Append signin history
|
||||
const record = await Signins.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
ip: ctx.ip,
|
||||
headers: ctx.headers,
|
||||
success: true
|
||||
}).then((x)=>Signins.findOneByOrFail(x.identifiers[0]));
|
||||
// Publish signin event
|
||||
publishMainStream(user.id, "signin", await Signins.pack(record));
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { generateKeyPair } from "node:crypto";
|
||||
import generateUserToken from "./generate-native-user-token.js";
|
||||
import { User } from "../../../models/entities/user.js";
|
||||
import { Users, UsedUsernames } from "../../../models/index.js";
|
||||
import { UserProfile } from "../../../models/entities/user-profile.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { genId } from "../../../misc/gen-id.js";
|
||||
import { toPunyNullable } from "../../../misc/convert-host.js";
|
||||
import { UserKeypair } from "../../../models/entities/user-keypair.js";
|
||||
import { usersChart } from "../../../services/chart/index.js";
|
||||
import { UsedUsername } from "../../../models/entities/used-username.js";
|
||||
import { db } from "../../../db/postgre.js";
|
||||
import config from "../../../config/index.js";
|
||||
import { hashPassword } from "../../../misc/password.js";
|
||||
import { fetchMeta } from "../../../misc/fetch-meta.js";
|
||||
import follow from "../../../services/following/create.js";
|
||||
export async function signup(opts) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
const userCount = await Users.countBy({
|
||||
host: IsNull()
|
||||
});
|
||||
if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
|
||||
throw new Error("MAX_USERS_REACHED");
|
||||
}
|
||||
// Validate username
|
||||
if (!Users.validateLocalUsername(username)) {
|
||||
throw new Error("INVALID_USERNAME");
|
||||
}
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword(password)) {
|
||||
throw new Error("INVALID_PASSWORD");
|
||||
}
|
||||
// Generate hash of password
|
||||
hash = await hashPassword(password);
|
||||
}
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
// Check username duplication
|
||||
if (await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull()
|
||||
})) {
|
||||
throw new Error("DUPLICATED_USERNAME");
|
||||
}
|
||||
// Check deleted username duplication
|
||||
if (await UsedUsernames.findOneBy({
|
||||
username: username.toLowerCase()
|
||||
})) {
|
||||
throw new Error("USED_USERNAME");
|
||||
}
|
||||
const keyPair = await new Promise((res, rej)=>generateKeyPair("rsa", {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: "spki",
|
||||
format: "pem"
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
cipher: undefined,
|
||||
passphrase: undefined
|
||||
}
|
||||
}, (err, publicKey, privateKey)=>err ? rej(err) : res([
|
||||
publicKey,
|
||||
privateKey
|
||||
])));
|
||||
const exist = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull()
|
||||
});
|
||||
if (exist) throw new Error("The username is already in use");
|
||||
// Prepare objects
|
||||
const user = new User({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
username: username,
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: toPunyNullable(host),
|
||||
token: secret,
|
||||
isAdmin: await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true
|
||||
}) === 0
|
||||
});
|
||||
const userKeypair = new UserKeypair({
|
||||
publicKey: keyPair[0],
|
||||
privateKey: keyPair[1],
|
||||
userId: user.id
|
||||
});
|
||||
const userProfile = new UserProfile({
|
||||
userId: user.id,
|
||||
autoAcceptFollowed: true,
|
||||
allowCalls: false,
|
||||
password: hash
|
||||
});
|
||||
const usedUsername = new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase()
|
||||
});
|
||||
// Save the objects atomically using a db transaction, note that we should never run any code in a transaction block directly
|
||||
await db.transaction(async (transactionalEntityManager)=>{
|
||||
await transactionalEntityManager.save(user);
|
||||
await transactionalEntityManager.save(userKeypair);
|
||||
await transactionalEntityManager.save(userProfile);
|
||||
await transactionalEntityManager.save(usedUsername);
|
||||
});
|
||||
const account = await Users.findOneByOrFail({
|
||||
id: user.id
|
||||
});
|
||||
const meta = await fetchMeta();
|
||||
// If an autofollow account exists, follow it
|
||||
if (meta.autofollowedAccount) {
|
||||
const autofollowedAccount = await Users.findOneByOrFail({
|
||||
usernameLower: meta.autofollowedAccount.toLowerCase(),
|
||||
host: IsNull()
|
||||
});
|
||||
if (autofollowedAccount) {
|
||||
await follow(account, autofollowedAccount);
|
||||
}
|
||||
}
|
||||
usersChart.update(account, true);
|
||||
return {
|
||||
account,
|
||||
secret
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as cp___custom_emojis from "./endpoints/compatibility/custom-emojis.js";
|
||||
import * as ep___instance_peers from "./endpoints/compatibility/peers.js";
|
||||
const cps = [
|
||||
[
|
||||
"v1/custom_emojis",
|
||||
cp___custom_emojis
|
||||
],
|
||||
[
|
||||
"v1/instance/peers",
|
||||
ep___instance_peers
|
||||
]
|
||||
];
|
||||
const compatibility = cps.map(([name, cp])=>{
|
||||
return {
|
||||
name: name,
|
||||
exec: cp.default,
|
||||
meta: cp.meta || {},
|
||||
params: cp.paramDef
|
||||
};
|
||||
});
|
||||
export default compatibility;
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as fs from "node:fs";
|
||||
import Ajv from "ajv";
|
||||
import { ApiError } from "./error.js";
|
||||
const ajv = new Ajv({
|
||||
useDefaults: true
|
||||
});
|
||||
ajv.addFormat("misskey:id", /^[a-zA-Z0-9]+$/);
|
||||
export default function(meta, paramDef, cb) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
return (params, user, token, file, ip, headers)=>{
|
||||
let cleanup = undefined;
|
||||
if (meta.requireFile) {
|
||||
cleanup = ()=>{
|
||||
fs.unlink(file.path, ()=>{});
|
||||
};
|
||||
if (file == null) return Promise.reject(new ApiError({
|
||||
message: "File required.",
|
||||
code: "FILE_REQUIRED",
|
||||
id: "4267801e-70d1-416a-b011-4ee502885d8b"
|
||||
}));
|
||||
}
|
||||
const valid = validate(params);
|
||||
if (!valid) {
|
||||
if (file) cleanup();
|
||||
const errors = validate.errors;
|
||||
const err = new ApiError({
|
||||
message: "Invalid param.",
|
||||
code: "INVALID_PARAM",
|
||||
id: "3d81ceae-475f-4600-b2a8-2bc116157532"
|
||||
}, {
|
||||
param: errors[0].schemaPath,
|
||||
reason: errors[0].message
|
||||
});
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return cb(params, user, token, file, cleanup, ip, headers);
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
import define from "../../define.js";
|
||||
import { AbuseUserReports } from "../../../../models/index.js";
|
||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
format: "date-time"
|
||||
},
|
||||
comment: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
optional: false
|
||||
},
|
||||
resolved: {
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
example: false
|
||||
},
|
||||
reporterId: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
format: "id"
|
||||
},
|
||||
targetUserId: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
format: "id"
|
||||
},
|
||||
assigneeId: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
optional: false,
|
||||
format: "id"
|
||||
},
|
||||
reporter: {
|
||||
type: "object",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
ref: "User"
|
||||
},
|
||||
targetUser: {
|
||||
type: "object",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
ref: "User"
|
||||
},
|
||||
assignee: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
optional: true,
|
||||
ref: "User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
state: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null
|
||||
},
|
||||
reporterOrigin: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"combined",
|
||||
"local",
|
||||
"remote"
|
||||
],
|
||||
default: "combined"
|
||||
},
|
||||
targetUserOrigin: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"combined",
|
||||
"local",
|
||||
"remote"
|
||||
],
|
||||
default: "combined"
|
||||
},
|
||||
forwarded: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const query = makePaginationQuery(AbuseUserReports.createQueryBuilder("report"), ps.sinceId, ps.untilId);
|
||||
switch(ps.state){
|
||||
case "resolved":
|
||||
query.andWhere("report.resolved = TRUE");
|
||||
break;
|
||||
case "unresolved":
|
||||
query.andWhere("report.resolved = FALSE");
|
||||
break;
|
||||
}
|
||||
switch(ps.reporterOrigin){
|
||||
case "local":
|
||||
query.andWhere("report.reporterHost IS NULL");
|
||||
break;
|
||||
case "remote":
|
||||
query.andWhere("report.reporterHost IS NOT NULL");
|
||||
break;
|
||||
}
|
||||
switch(ps.targetUserOrigin){
|
||||
case "local":
|
||||
query.andWhere("report.targetUserHost IS NULL");
|
||||
break;
|
||||
case "remote":
|
||||
query.andWhere("report.targetUserHost IS NOT NULL");
|
||||
break;
|
||||
}
|
||||
const reports = await query.take(ps.limit).getMany();
|
||||
return await AbuseUserReports.packMany(reports);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import define from "../../../define.js";
|
||||
import { Users } from "../../../../../models/index.js";
|
||||
import { signup } from "../../../common/signup.js";
|
||||
import { IsNull } from "typeorm";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "User",
|
||||
properties: {
|
||||
token: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
username: Users.localUsernameSchema,
|
||||
password: Users.passwordSchema
|
||||
},
|
||||
required: [
|
||||
"username",
|
||||
"password"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, _me)=>{
|
||||
const me = _me ? await Users.findOneByOrFail({
|
||||
id: _me.id
|
||||
}) : null;
|
||||
const noUsers = await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true
|
||||
}) === 0;
|
||||
if (!(noUsers || me?.isAdmin)) throw new Error("access denied");
|
||||
const { account, secret } = await signup({
|
||||
username: ps.username,
|
||||
password: ps.password
|
||||
});
|
||||
const res = await Users.pack(account, account, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
});
|
||||
res.token = secret;
|
||||
return res;
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import define from "../../../define.js";
|
||||
import { Users } from "../../../../../models/index.js";
|
||||
import { doPostSuspend } from "../../../../../services/suspend-user.js";
|
||||
import { publishUserEvent } from "../../../../../services/stream.js";
|
||||
import { createDeleteAccountJob } from "../../../../../queue/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
throw new Error("cannot suspend admin");
|
||||
}
|
||||
if (user.isModerator) {
|
||||
throw new Error("cannot suspend moderator");
|
||||
}
|
||||
if (Users.isLocalUser(user)) {
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await doPostSuspend(user).catch((e)=>{});
|
||||
createDeleteAccountJob(user, {
|
||||
soft: false
|
||||
});
|
||||
} else {
|
||||
createDeleteAccountJob(user, {
|
||||
soft: true
|
||||
});
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isDeleted: true
|
||||
});
|
||||
if (Users.isLocalUser(user)) {
|
||||
// Terminate streaming
|
||||
publishUserEvent(user.id, "terminate", {});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import config from "../../../../../config/index.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
import define from "../../../define.js";
|
||||
import { Metas } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const hostedConfig = config.isManagedHosting;
|
||||
const hosted = hostedConfig != null && hostedConfig === true;
|
||||
if (hosted) {
|
||||
const set = {};
|
||||
if (config.deepl.managed != null && config.deepl.managed === true) {
|
||||
if (typeof config.deepl.authKey === "boolean") {
|
||||
set.deeplAuthKey = config.deepl.authKey;
|
||||
}
|
||||
if (typeof config.deepl.isPro === "boolean") {
|
||||
set.deeplIsPro = config.deepl.isPro;
|
||||
}
|
||||
}
|
||||
if (config.libreTranslate.managed != null && config.libreTranslate.managed === true) {
|
||||
if (typeof config.libreTranslate.apiUrl === "string") {
|
||||
set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
|
||||
}
|
||||
if (typeof config.libreTranslate.apiKey === "string") {
|
||||
set.libreTranslateApiKey = config.libreTranslate.apiKey;
|
||||
}
|
||||
}
|
||||
if (config.email.managed != null && config.email.managed === true) {
|
||||
set.enableEmail = true;
|
||||
if (typeof config.email.address === "string") {
|
||||
set.email = config.email.address;
|
||||
}
|
||||
if (typeof config.email.host === "string") {
|
||||
set.smtpHost = config.email.host;
|
||||
}
|
||||
if (typeof config.email.port === "number") {
|
||||
set.smtpPort = config.email.port;
|
||||
}
|
||||
if (typeof config.email.user === "string") {
|
||||
set.smtpUser = config.email.user;
|
||||
}
|
||||
if (typeof config.email.pass === "string") {
|
||||
set.smtpPass = config.email.pass;
|
||||
}
|
||||
if (typeof config.email.useImplicitSslTls === "boolean") {
|
||||
set.smtpSecure = config.email.useImplicitSslTls;
|
||||
}
|
||||
}
|
||||
if (config.objectStorage.managed != null && config.objectStorage.managed === true) {
|
||||
set.useObjectStorage = true;
|
||||
if (typeof config.objectStorage.baseUrl === "string") {
|
||||
set.objectStorageBaseUrl = config.objectStorage.baseUrl;
|
||||
}
|
||||
if (typeof config.objectStorage.bucket === "string") {
|
||||
set.objectStorageBucket = config.objectStorage.bucket;
|
||||
}
|
||||
if (typeof config.objectStorage.prefix === "string") {
|
||||
set.objectStoragePrefix = config.objectStorage.prefix;
|
||||
}
|
||||
if (typeof config.objectStorage.endpoint === "string") {
|
||||
set.objectStorageEndpoint = config.objectStorage.endpoint;
|
||||
}
|
||||
if (typeof config.objectStorage.region === "string") {
|
||||
set.objectStorageRegion = config.objectStorage.region;
|
||||
}
|
||||
if (typeof config.objectStorage.accessKey === "string") {
|
||||
set.objectStorageAccessKey = config.objectStorage.accessKey;
|
||||
}
|
||||
if (typeof config.objectStorage.secretKey === "string") {
|
||||
set.objectStorageSecretKey = config.objectStorage.secretKey;
|
||||
}
|
||||
if (typeof config.objectStorage.useSsl === "boolean") {
|
||||
set.objectStorageUseSSL = config.objectStorage.useSsl;
|
||||
}
|
||||
if (typeof config.objectStorage.connnectOverProxy === "boolean") {
|
||||
set.objectStorageUseProxy = config.objectStorage.connnectOverProxy;
|
||||
}
|
||||
if (typeof config.objectStorage.setPublicReadOnUpload === "boolean") {
|
||||
set.objectStorageSetPublicRead = config.objectStorage.setPublicReadOnUpload;
|
||||
}
|
||||
if (typeof config.objectStorage.s3ForcePathStyle === "boolean") {
|
||||
set.objectStorageS3ForcePathStyle = config.objectStorage.s3ForcePathStyle;
|
||||
}
|
||||
}
|
||||
if (config.summalyProxyUrl !== undefined) {
|
||||
set.summalyProxy = config.summalyProxyUrl;
|
||||
}
|
||||
const meta = await Metas.findOne({
|
||||
where: {},
|
||||
order: {
|
||||
id: "DESC"
|
||||
}
|
||||
});
|
||||
if (meta) await Metas.update(meta.id, set);
|
||||
else await Metas.save(set);
|
||||
insertModerationLog(me, "updateMeta");
|
||||
}
|
||||
return hosted;
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import define from "../../../define.js";
|
||||
import { Announcements } from "../../../../../models/index.js";
|
||||
import { genId } from "../../../../../misc/gen-id.js";
|
||||
import { publishBroadcastStream } from "../../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "date-time"
|
||||
},
|
||||
updatedAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "date-time"
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
imageUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
imageUrl: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
minLength: 1
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean"
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"title",
|
||||
"text",
|
||||
"imageUrl"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const announcement = await Announcements.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: null,
|
||||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
showPopup: ps.showPopup ?? false,
|
||||
isGoodNews: ps.isGoodNews ?? false
|
||||
}).then((x)=>Announcements.findOneByOrFail(x.identifiers[0]));
|
||||
publishBroadcastStream("announcementAdded", announcement);
|
||||
return Object.assign({}, announcement, {
|
||||
createdAt: announcement.createdAt.toISOString(),
|
||||
updatedAt: null
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import define from "../../../define.js";
|
||||
import { Announcements } from "../../../../../models/index.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { publishBroadcastStream } from "../../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchAnnouncement: {
|
||||
message: "No such announcement.",
|
||||
code: "NO_SUCH_ANNOUNCEMENT",
|
||||
id: "ecad8040-a276-4e85-bda9-015a708d291e"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"id"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const announcement = await Announcements.findOneBy({
|
||||
id: ps.id
|
||||
});
|
||||
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
publishBroadcastStream("announcementDeleted", announcement.id);
|
||||
await Announcements.delete(announcement.id);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Announcements, AnnouncementReads } from "../../../../../models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "date-time"
|
||||
},
|
||||
updatedAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "date-time"
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
imageUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
reads: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const query = makePaginationQuery(Announcements.createQueryBuilder("announcement"), ps.sinceId, ps.untilId);
|
||||
const announcements = await query.take(ps.limit).getMany();
|
||||
const reads = new Map();
|
||||
for (const announcement of announcements){
|
||||
reads.set(announcement, await AnnouncementReads.countBy({
|
||||
announcementId: announcement.id
|
||||
}));
|
||||
}
|
||||
return announcements.map((announcement)=>({
|
||||
id: announcement.id,
|
||||
createdAt: announcement.createdAt.toISOString(),
|
||||
updatedAt: announcement.updatedAt?.toISOString() ?? null,
|
||||
title: announcement.title,
|
||||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
reads: reads.get(announcement),
|
||||
showPopup: announcement.showPopup,
|
||||
isGoodNews: announcement.isGoodNews
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import define from "../../../define.js";
|
||||
import { Announcements } from "../../../../../models/index.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchAnnouncement: {
|
||||
message: "No such announcement.",
|
||||
code: "NO_SUCH_ANNOUNCEMENT",
|
||||
id: "d3aae5a7-6372-4cb4-b61c-f511ffc2d7cc"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
minLength: 1
|
||||
},
|
||||
imageUrl: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
minLength: 1
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean"
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"id",
|
||||
"title",
|
||||
"text",
|
||||
"imageUrl"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const announcement = await Announcements.findOneBy({
|
||||
id: ps.id
|
||||
});
|
||||
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
await Announcements.update(announcement.id, {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
showPopup: ps.showPopup ?? false,
|
||||
isGoodNews: ps.isGoodNews ?? false
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import * as fs from "node:fs";
|
||||
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import archiver from "archiver";
|
||||
import config from "../../../../config/index.js";
|
||||
import define from "../../define.js";
|
||||
import { ApiError } from "../../error.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { createTempDir } from "../../../../misc/create-temp.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
size: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
includedMedia: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
backupFailed: {
|
||||
message: "Failed to create backup.",
|
||||
code: "BACKUP_FAILED",
|
||||
id: "7498ab9f-4e1d-40d0-96d8-d8d1d82bd621"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../../..");
|
||||
function timestamp() {
|
||||
const now = new Date();
|
||||
const pad = (value)=>value.toString().padStart(2, "0");
|
||||
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
||||
}
|
||||
function run(command, args, env) {
|
||||
return new Promise((resolvePromise, reject)=>{
|
||||
const child = spawn(command, args, {
|
||||
stdio: [
|
||||
"ignore",
|
||||
"ignore",
|
||||
"pipe"
|
||||
],
|
||||
env
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk)=>{
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code)=>{
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
} else {
|
||||
reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function archiveDirectory(sourceDir, outFile) {
|
||||
return new Promise((resolvePromise, reject)=>{
|
||||
const output = fs.createWriteStream(outFile);
|
||||
const archive = archiver("tar", {
|
||||
gzip: true,
|
||||
gzipOptions: {
|
||||
level: 6
|
||||
}
|
||||
});
|
||||
output.on("close", ()=>resolvePromise());
|
||||
archive.on("error", reject);
|
||||
archive.pipe(output);
|
||||
archive.directory(sourceDir, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
async function pathExists(path) {
|
||||
return stat(path).then(()=>true).catch(()=>false);
|
||||
}
|
||||
export default define(meta, paramDef, async (_ps, me)=>{
|
||||
const [workDir, cleanup] = await createTempDir();
|
||||
const fileName = `iceshrimp-full-${timestamp()}.tar.gz`;
|
||||
const outDir = resolve(process.env.ICESHRIMP_BACKUP_DIR ?? `${rootDir}/backups`);
|
||||
const outFile = resolve(outDir, fileName);
|
||||
const dbDumpPath = resolve(workDir, "database.dump");
|
||||
const mediaDir = resolve(config.mediaDir);
|
||||
let includedMedia = false;
|
||||
try {
|
||||
await mkdir(outDir, {
|
||||
recursive: true
|
||||
});
|
||||
const env = {
|
||||
...process.env,
|
||||
PGHOST: config.db.host,
|
||||
PGPORT: String(config.db.port),
|
||||
PGDATABASE: config.db.db,
|
||||
PGUSER: config.db.user,
|
||||
PGPASSWORD: config.db.pass
|
||||
};
|
||||
await run("pg_dump", [
|
||||
"--format=custom",
|
||||
"--blobs",
|
||||
"--no-owner",
|
||||
"--file",
|
||||
dbDumpPath,
|
||||
config.db.db
|
||||
], env);
|
||||
const configDir = resolve(workDir, "config");
|
||||
await mkdir(configDir, {
|
||||
recursive: true
|
||||
});
|
||||
const configFiles = [
|
||||
process.env.ICESHRIMP_CONFIG ? resolve(process.env.ICESHRIMP_CONFIG) : resolve(rootDir, ".config/default.yml"),
|
||||
...process.env.ICESHRIMP_SECRETS ? [
|
||||
resolve(process.env.ICESHRIMP_SECRETS)
|
||||
] : []
|
||||
];
|
||||
for (const configFile of configFiles){
|
||||
if (await pathExists(configFile)) {
|
||||
fs.copyFileSync(configFile, resolve(configDir, configFile.split("/").pop()));
|
||||
}
|
||||
}
|
||||
if (await pathExists(mediaDir) && !outDir.startsWith(mediaDir + "/") && outDir !== mediaDir) {
|
||||
fs.cpSync(mediaDir, resolve(workDir, "files"), {
|
||||
recursive: true,
|
||||
dereference: false,
|
||||
errorOnExist: false
|
||||
});
|
||||
includedMedia = true;
|
||||
}
|
||||
await writeFile(resolve(workDir, "manifest.json"), `${JSON.stringify({
|
||||
type: "iceshrimp-full-backup",
|
||||
version: config.version,
|
||||
createdAt: new Date().toISOString(),
|
||||
host: config.host,
|
||||
database: config.db.db,
|
||||
included: {
|
||||
database: true,
|
||||
config: true,
|
||||
media: includedMedia
|
||||
},
|
||||
restore: "yarn full:restore <this archive>"
|
||||
}, null, 2)}\n`, "utf8");
|
||||
await archiveDirectory(workDir, outFile);
|
||||
const outStat = await stat(outFile);
|
||||
await insertModerationLog(me, "createBackup", {
|
||||
path: outFile,
|
||||
size: outStat.size,
|
||||
includedMedia
|
||||
});
|
||||
return {
|
||||
path: outFile,
|
||||
fileName,
|
||||
size: outStat.size,
|
||||
includedMedia
|
||||
};
|
||||
} catch (e) {
|
||||
await rm(outFile, {
|
||||
force: true
|
||||
}).catch(()=>{});
|
||||
throw new ApiError(meta.errors.backupFailed, {
|
||||
message: e instanceof Error ? e.message : String(e)
|
||||
});
|
||||
} finally{
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { deleteAccount } from "../../../../services/delete-account.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
res: {}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const user = await Users.findOneByOrFail({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user.isDeleted) {
|
||||
return;
|
||||
}
|
||||
await deleteAccount(user);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import define from "../../define.js";
|
||||
import { deleteFile } from "../../../../services/drive/delete-file.js";
|
||||
import { DriveFiles } from "../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const files = await DriveFiles.findBy({
|
||||
userId: ps.userId
|
||||
});
|
||||
for (const file of files){
|
||||
deleteFile(file);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import define from "../../define.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { publishInternalEvent } from "../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
overrideMb: {
|
||||
type: "number",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId",
|
||||
"overrideMb"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (!Users.isLocalUser(user)) {
|
||||
throw new Error("user is not local user");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
driveCapacityOverrideMb: ps.overrideMb
|
||||
});
|
||||
publishInternalEvent("localUserUpdated", {
|
||||
id: user.id
|
||||
});
|
||||
insertModerationLog(me, "change-drive-capacity-override", {
|
||||
targetId: user.id
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import define from "../../../define.js";
|
||||
import { createCleanRemoteFilesJob } from "../../../../../queue/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
createCleanRemoteFilesJob();
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { IsNull } from "typeorm";
|
||||
import define from "../../../define.js";
|
||||
import { deleteFile } from "../../../../../services/drive/delete-file.js";
|
||||
import { DriveFiles } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const files = await DriveFiles.findBy({
|
||||
userId: IsNull()
|
||||
});
|
||||
for (const file of files){
|
||||
deleteFile(file);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { DriveFiles } from "../../../../../models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: false,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "DriveFile"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id",
|
||||
nullable: true
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1)
|
||||
},
|
||||
origin: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"combined",
|
||||
"local",
|
||||
"remote"
|
||||
],
|
||||
default: "local"
|
||||
},
|
||||
hostname: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: "The local host is represented with `null`."
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const query = makePaginationQuery(DriveFiles.createQueryBuilder("file"), ps.sinceId, ps.untilId);
|
||||
if (ps.userId) {
|
||||
query.andWhere("file.userId = :userId", {
|
||||
userId: ps.userId
|
||||
});
|
||||
} else {
|
||||
if (ps.origin === "local") {
|
||||
query.andWhere("file.userHost IS NULL");
|
||||
} else if (ps.origin === "remote") {
|
||||
query.andWhere("file.userHost IS NOT NULL");
|
||||
}
|
||||
if (ps.hostname) {
|
||||
query.andWhere("file.userHost = :hostname", {
|
||||
hostname: ps.hostname
|
||||
});
|
||||
}
|
||||
}
|
||||
if (ps.type) {
|
||||
if (ps.type.endsWith("/*")) {
|
||||
query.andWhere("file.type like :type", {
|
||||
type: `${ps.type.replace("/*", "/")}%`
|
||||
});
|
||||
} else {
|
||||
query.andWhere("file.type = :type", {
|
||||
type: ps.type
|
||||
});
|
||||
}
|
||||
}
|
||||
const files = await query.take(ps.limit).getMany();
|
||||
return await DriveFiles.packMany(files, {
|
||||
detail: true,
|
||||
withUser: true,
|
||||
self: true
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { DriveFiles } from "../../../../../models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: "No such file.",
|
||||
code: "NO_SUCH_FILE",
|
||||
id: "caf3ca38-c6e5-472e-a30c-b05377dcc240"
|
||||
}
|
||||
},
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "date-time"
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
userHost: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
description: "The local host is represented with `null`."
|
||||
},
|
||||
md5: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "md5",
|
||||
example: "15eca7fba0480996e2245f5185bf39f2"
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: "lenna.jpg"
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: "image/jpeg"
|
||||
},
|
||||
size: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: 51469
|
||||
},
|
||||
comment: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
blurhash: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
properties: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
width: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: 1280
|
||||
},
|
||||
height: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: 720
|
||||
},
|
||||
avgColor: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
example: "rgb(40,65,87)"
|
||||
}
|
||||
}
|
||||
},
|
||||
storedInternal: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
example: true
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "url"
|
||||
},
|
||||
thumbnailUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "url"
|
||||
},
|
||||
webpublicUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "url"
|
||||
},
|
||||
accessKey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
thumbnailAccessKey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
webpublicAccessKey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
uri: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
src: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
folderId: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
format: "id",
|
||||
example: "xxxxxxxxxx"
|
||||
},
|
||||
isSensitive: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
isLink: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
fileId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"fileId"
|
||||
]
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
url: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"url"
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const file = ps.fileId ? await DriveFiles.findOneBy({
|
||||
id: ps.fileId
|
||||
}) : await DriveFiles.findOne({
|
||||
where: [
|
||||
{
|
||||
url: ps.url
|
||||
},
|
||||
{
|
||||
thumbnailUrl: ps.url
|
||||
},
|
||||
{
|
||||
webpublicUrl: ps.url
|
||||
}
|
||||
]
|
||||
});
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
if (!me.isAdmin) {
|
||||
file.requestIp = undefined;
|
||||
file.requestHeaders = undefined;
|
||||
}
|
||||
return file;
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids",
|
||||
"aliases"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids)
|
||||
});
|
||||
for (const emoji of emojis){
|
||||
await Emojis.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: [
|
||||
...new Set(emoji.aliases.concat(ps.aliases))
|
||||
]
|
||||
});
|
||||
}
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis, DriveFiles } from "../../../../../models/index.js";
|
||||
import { genId } from "../../../../../misc/gen-id.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import rndstr from "rndstr";
|
||||
import { publishBroadcastStream } from "../../../../../services/stream.js";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
import { getEmojiSize } from "../../../../../misc/emoji-meta.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: "No such file.",
|
||||
code: "MO_SUCH_FILE",
|
||||
id: "fc46b5a4-6b92-4c33-ac66-b806659bb5cf"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"fileId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: ps.fileId
|
||||
});
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
const name = file.name.split(".")[0].match(/^[a-z0-9_]+$/) ? file.name.split(".")[0] : `_${rndstr("a-z0-9", 8)}_`;
|
||||
const size = await getEmojiSize(file.url);
|
||||
const emoji = await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: name,
|
||||
category: null,
|
||||
host: null,
|
||||
aliases: [],
|
||||
originalUrl: file.url,
|
||||
publicUrl: file.webpublicUrl ?? file.url,
|
||||
type: file.webpublicType ?? file.type,
|
||||
license: null,
|
||||
glyph: file.type === "image/svg+xml",
|
||||
width: size.width || null,
|
||||
height: size.height || null
|
||||
}).then((x)=>Emojis.findOneByOrFail(x.identifiers[0]));
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
publishBroadcastStream("emojiAdded", {
|
||||
emoji: await Emojis.pack(emoji.id)
|
||||
});
|
||||
insertModerationLog(me, "addEmoji", {
|
||||
emojiId: emoji.id
|
||||
});
|
||||
return {
|
||||
id: emoji.id
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { genId } from "../../../../../misc/gen-id.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { uploadFromUrl } from "../../../../../services/drive/upload-from-url.js";
|
||||
import { publishBroadcastStream } from "../../../../../services/stream.js";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
import { getEmojiSize } from "../../../../../misc/emoji-meta.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: "No such emoji.",
|
||||
code: "NO_SUCH_EMOJI",
|
||||
id: "e2785b66-dca3-4087-9cac-b93c541cc425"
|
||||
}
|
||||
},
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
emojiId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"emojiId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const emoji = await Emojis.findOneBy({
|
||||
id: ps.emojiId
|
||||
});
|
||||
if (emoji == null) {
|
||||
throw new ApiError(meta.errors.noSuchEmoji);
|
||||
}
|
||||
let driveFile;
|
||||
try {
|
||||
// Create file
|
||||
driveFile = await uploadFromUrl({
|
||||
url: emoji.originalUrl,
|
||||
user: null,
|
||||
force: true
|
||||
});
|
||||
} catch (e) {
|
||||
throw new ApiError();
|
||||
}
|
||||
const size = await getEmojiSize(driveFile.url);
|
||||
const copied = await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emoji.name,
|
||||
host: null,
|
||||
aliases: [],
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
license: emoji.license,
|
||||
glyph: emoji.glyph,
|
||||
width: size.width || null,
|
||||
height: size.height || null
|
||||
}).then((x)=>Emojis.findOneByOrFail(x.identifiers[0]));
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
publishBroadcastStream("emojiAdded", {
|
||||
emoji: await Emojis.pack(copied.id)
|
||||
});
|
||||
return {
|
||||
id: copied.id
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids)
|
||||
});
|
||||
for (const emoji of emojis){
|
||||
await Emojis.delete(emoji.id);
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
insertModerationLog(me, "deleteEmoji", {
|
||||
emoji: emoji
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: "No such emoji.",
|
||||
code: "NO_SUCH_EMOJI",
|
||||
id: "be83669b-773a-44b7-b1f8-e5e5170ac3c2"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"id"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const emoji = await Emojis.findOneBy({
|
||||
id: ps.id
|
||||
});
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
await Emojis.delete(emoji.id);
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
insertModerationLog(me, "deleteEmoji", {
|
||||
emoji: emoji
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import define from "../../../define.js";
|
||||
import { createImportCustomEmojisJob } from "../../../../../queue/index.js";
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"fileId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
createImportCustomEmojisJob(user, ps.fileId);
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { toPuny } from "../../../../../misc/convert-host.js";
|
||||
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
|
||||
import { sqlLikeEscape } from "../../../../../misc/sql-like-escape.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
host: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
description: "The local host is represented with `null`."
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
license: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
glyph: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
glyphUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
width: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
height: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null
|
||||
},
|
||||
host: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: "Use `null` to represent the local host."
|
||||
},
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const q = makePaginationQuery(Emojis.createQueryBuilder("emoji"), ps.sinceId, ps.untilId);
|
||||
if (ps.host == null) {
|
||||
q.andWhere("emoji.host IS NOT NULL");
|
||||
} else {
|
||||
q.andWhere("emoji.host = :host", {
|
||||
host: toPuny(ps.host)
|
||||
});
|
||||
}
|
||||
if (ps.query) {
|
||||
q.andWhere("emoji.name like :query", {
|
||||
query: `%${sqlLikeEscape(ps.query)}%`
|
||||
});
|
||||
}
|
||||
const emojis = await q.orderBy("emoji.id", "DESC").take(ps.limit).getMany();
|
||||
return Emojis.packMany(emojis);
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
|
||||
//import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
host: {
|
||||
type: "null",
|
||||
optional: false,
|
||||
description: "The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files."
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
license: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
glyph: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
glyphUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
width: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
height: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null
|
||||
},
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const q = makePaginationQuery(Emojis.createQueryBuilder("emoji"), ps.sinceId, ps.untilId).andWhere("emoji.host IS NULL");
|
||||
let emojis;
|
||||
if (ps.query) {
|
||||
//q.andWhere('emoji.name ILIKE :q', { q: `%${sqlLikeEscape(ps.query)}%` });
|
||||
//const emojis = await q.take(ps.limit).getMany();
|
||||
emojis = await q.getMany();
|
||||
emojis = emojis.filter((emoji)=>emoji.name.includes(ps.query) || emoji.aliases.some((a)=>a.includes(ps.query)) || emoji.category?.includes(ps.query));
|
||||
emojis.splice(ps.limit + 1);
|
||||
} else {
|
||||
emojis = await q.take(ps.limit).getMany();
|
||||
}
|
||||
return Emojis.packMany(emojis);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids",
|
||||
"aliases"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids)
|
||||
});
|
||||
for (const emoji of emojis){
|
||||
await Emojis.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
aliases: emoji.aliases.filter((x)=>!ps.aliases.includes(x))
|
||||
});
|
||||
}
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids",
|
||||
"aliases"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
await Emojis.update({
|
||||
id: In(ps.ids)
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
aliases: ps.aliases
|
||||
});
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "Use `null` to reset the category."
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
await Emojis.update({
|
||||
id: In(ps.ids)
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
category: ps.category
|
||||
});
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
ids: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
license: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "Use `null` to reset the license."
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"ids"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
await Emojis.update({
|
||||
id: In(ps.ids)
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
license: ps.license
|
||||
});
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import define from "../../../define.js";
|
||||
import { Emojis } from "../../../../../models/index.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { db } from "../../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: "No such emoji.",
|
||||
code: "NO_SUCH_EMOJI",
|
||||
id: "684dec9d-a8c2-4364-9aa8-456c49cb1dc8"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
name: {
|
||||
type: "string"
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "Use `null` to reset the category."
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
license: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
glyph: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"id",
|
||||
"name",
|
||||
"aliases"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const emoji = await Emojis.findOneBy({
|
||||
id: ps.id
|
||||
});
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
await Emojis.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
name: ps.name,
|
||||
category: ps.category,
|
||||
aliases: ps.aliases,
|
||||
license: ps.license,
|
||||
...typeof ps.glyph === "boolean" ? {
|
||||
glyph: ps.glyph
|
||||
} : {}
|
||||
});
|
||||
await db.queryResultCache.remove([
|
||||
"meta_emojis"
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import define from "../../../define.js";
|
||||
import { deleteFile } from "../../../../../services/drive/delete-file.js";
|
||||
import { DriveFiles } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
host: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"host"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const files = await DriveFiles.findBy({
|
||||
userHost: ps.host
|
||||
});
|
||||
for (const file of files){
|
||||
deleteFile(file);
|
||||
}
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import define from "../../../define.js";
|
||||
import { Instances } from "../../../../../models/index.js";
|
||||
import { toPuny } from "../../../../../misc/convert-host.js";
|
||||
import { fetchInstanceMetadata } from "../../../../../services/fetch-instance-metadata.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
host: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"host"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const instance = await Instances.findOneBy({
|
||||
host: toPuny(ps.host)
|
||||
});
|
||||
if (instance == null) {
|
||||
throw new Error("instance not found");
|
||||
}
|
||||
fetchInstanceMetadata(instance, true);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import define from "../../../define.js";
|
||||
import deleteFollowing from "../../../../../services/following/delete.js";
|
||||
import { Followings, Users } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
host: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"host"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const followings = await Followings.findBy({
|
||||
followerHost: ps.host
|
||||
});
|
||||
const pairs = await Promise.all(followings.map((f)=>Promise.all([
|
||||
Users.findOneByOrFail({
|
||||
id: f.followerId
|
||||
}),
|
||||
Users.findOneByOrFail({
|
||||
id: f.followeeId
|
||||
})
|
||||
])));
|
||||
for (const pair of pairs){
|
||||
deleteFollowing(pair[0], pair[1]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import define from "../../../define.js";
|
||||
import { Instances } from "../../../../../models/index.js";
|
||||
import { toPuny } from "../../../../../misc/convert-host.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
host: {
|
||||
type: "string"
|
||||
},
|
||||
isSuspended: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"host",
|
||||
"isSuspended"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const instance = await Instances.findOneBy({
|
||||
host: toPuny(ps.host)
|
||||
});
|
||||
if (instance == null) {
|
||||
throw new Error("instance not found");
|
||||
}
|
||||
Instances.update({
|
||||
host: toPuny(ps.host)
|
||||
}, {
|
||||
isSuspended: ps.isSuspended
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import define from "../../define.js";
|
||||
import { db } from "../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
tags: [
|
||||
"admin"
|
||||
]
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async ()=>{
|
||||
const stats = await db.query("SELECT * FROM pg_indexes;").then((recs)=>{
|
||||
const res = [];
|
||||
for (const rec of recs){
|
||||
res.push(rec);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
return stats;
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { db } from "../../../../db/postgre.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: {
|
||||
migrations: {
|
||||
count: 66,
|
||||
size: 32768
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async ()=>{
|
||||
const sizes = await db.query(`
|
||||
SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size"
|
||||
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND C.relkind <> 'i'
|
||||
AND nspname !~ '^pg_toast';`).then((recs)=>{
|
||||
const res = {};
|
||||
for (const rec of recs){
|
||||
res[rec.table] = {
|
||||
count: parseInt(rec.count, 10),
|
||||
size: parseInt(rec.size, 10)
|
||||
};
|
||||
}
|
||||
return res;
|
||||
});
|
||||
return sizes;
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { UserIps } from "../../../../models/index.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const ips = await UserIps.find({
|
||||
where: {
|
||||
userId: ps.userId
|
||||
},
|
||||
order: {
|
||||
createdAt: "DESC"
|
||||
},
|
||||
take: 30
|
||||
});
|
||||
return ips.map((x)=>({
|
||||
ip: x.ip,
|
||||
createdAt: x.createdAt.toISOString()
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import rndstr from "rndstr";
|
||||
import define from "../../define.js";
|
||||
import { RegistrationTickets } from "../../../../models/index.js";
|
||||
import { genId } from "../../../../misc/gen-id.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: "2ERUA5VR",
|
||||
maxLength: 8,
|
||||
minLength: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async ()=>{
|
||||
const code = rndstr({
|
||||
length: 8,
|
||||
chars: "2-9A-HJ-NP-Z"
|
||||
});
|
||||
await RegistrationTickets.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
code
|
||||
});
|
||||
return {
|
||||
code
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,503 @@
|
||||
import config from "../../../../config/index.js";
|
||||
import { fetchMeta } from "../../../../misc/fetch-meta.js";
|
||||
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "../../../../const.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"meta"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
driveCapacityPerLocalUserMb: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
driveCapacityPerRemoteUserMb: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
lua4frozenDatabaseCapacityMb: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
cacheRemoteFiles: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
emailRequiredForSignup: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
enableHcaptcha: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
hcaptchaSiteKey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
enableRecaptcha: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
recaptchaSiteKey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
swPublickey: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
mascotImageUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
default: "/twemoji/1f440.svg"
|
||||
},
|
||||
bannerUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
errorImageUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
default: "/twemoji/1f480.svg"
|
||||
},
|
||||
iconUrl: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
maxNoteTextLength: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
maxCaptionTextLength: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
emojis: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
aliases: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
host: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enableEmail: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
translatorAvailable: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
recommendedInstances: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
pinnedUsers: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
customMOTD: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
customSplashIcons: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
hiddenTags: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
blockedHosts: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
silencedHosts: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
allowedHosts: {
|
||||
type: "array",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
},
|
||||
privateMode: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
secureMode: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
hcaptchaSecretKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
recaptchaSecretKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
summaryProxy: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
smtpSecure: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
smtpHost: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
smtpPort: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
smtpUser: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
smtpPass: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
swPrivateKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
useObjectStorage: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
objectStorageBaseUrl: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageBucket: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStoragePrefix: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageEndpoint: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageRegion: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStoragePort: {
|
||||
type: "number",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageAccessKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageSecretKey: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
objectStorageUseSSL: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
objectStorageUseProxy: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
objectStorageSetPublicRead: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
enableIpLogging: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
enableActiveEmailValidation: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false
|
||||
},
|
||||
defaultReaction: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
experimentalFeatures: {
|
||||
type: "object",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
properties: {
|
||||
postImports: {
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
enableServerMachineStats: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
enableIdenticonGeneration: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
donationLink: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
},
|
||||
autofollowedAccount: {
|
||||
type: "string",
|
||||
optional: true,
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const instance = await fetchMeta(true);
|
||||
return {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
version: config.version,
|
||||
name: instance.name,
|
||||
uri: config.url,
|
||||
description: instance.description,
|
||||
langs: instance.langs,
|
||||
tosUrl: instance.ToSUrl,
|
||||
repositoryUrl: instance.repositoryUrl,
|
||||
feedbackUrl: instance.feedbackUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableRecommendedTimeline: instance.disableRecommendedTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||
lua4frozenDatabaseCapacityMb: instance.lua4frozenDatabaseCapacityMb,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
swPublickey: instance.swPublicKey,
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl,
|
||||
bannerUrl: instance.bannerUrl,
|
||||
errorImageUrl: instance.errorImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
|
||||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
translatorAvailable: instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
defaultReaction: instance.defaultReaction,
|
||||
recommendedInstances: instance.recommendedInstances,
|
||||
pinnedUsers: instance.pinnedUsers,
|
||||
customMOTD: instance.customMOTD,
|
||||
customSplashIcons: instance.customSplashIcons,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
silencedHosts: instance.silencedHosts,
|
||||
allowedHosts: instance.allowedHosts,
|
||||
privateMode: instance.privateMode,
|
||||
secureMode: instance.secureMode,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
summalyProxy: instance.summalyProxy,
|
||||
email: instance.email,
|
||||
smtpSecure: instance.smtpSecure,
|
||||
smtpHost: instance.smtpHost,
|
||||
smtpPort: instance.smtpPort,
|
||||
smtpUser: instance.smtpUser,
|
||||
smtpPass: instance.smtpPass,
|
||||
swPrivateKey: instance.swPrivateKey,
|
||||
useObjectStorage: instance.useObjectStorage,
|
||||
objectStorageBaseUrl: instance.objectStorageBaseUrl,
|
||||
objectStorageBucket: instance.objectStorageBucket,
|
||||
objectStoragePrefix: instance.objectStoragePrefix,
|
||||
objectStorageEndpoint: instance.objectStorageEndpoint,
|
||||
objectStorageRegion: instance.objectStorageRegion,
|
||||
objectStoragePort: instance.objectStoragePort,
|
||||
objectStorageAccessKey: instance.objectStorageAccessKey,
|
||||
objectStorageSecretKey: instance.objectStorageSecretKey,
|
||||
objectStorageUseSSL: instance.objectStorageUseSSL,
|
||||
objectStorageUseProxy: instance.objectStorageUseProxy,
|
||||
objectStorageSetPublicRead: instance.objectStorageSetPublicRead,
|
||||
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
|
||||
deeplAuthKey: instance.deeplAuthKey,
|
||||
deeplIsPro: instance.deeplIsPro,
|
||||
libreTranslateApiUrl: instance.libreTranslateApiUrl,
|
||||
libreTranslateApiKey: instance.libreTranslateApiKey,
|
||||
enableIpLogging: instance.enableIpLogging,
|
||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||
experimentalFeatures: instance.experimentalFeatures,
|
||||
enableServerMachineStats: instance.enableServerMachineStats,
|
||||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
donationLink: instance.donationLink,
|
||||
autofollowedAccount: instance.autofollowedAccount
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import define from "../../../define.js";
|
||||
import { Users } from "../../../../../models/index.js";
|
||||
import { publishInternalEvent } from "../../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
throw new Error("cannot mark as moderator if admin user");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isModerator: true
|
||||
});
|
||||
publishInternalEvent("userChangeModeratorState", {
|
||||
id: user.id,
|
||||
isModerator: true
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import define from "../../../define.js";
|
||||
import { Users } from "../../../../../models/index.js";
|
||||
import { publishInternalEvent } from "../../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isModerator: false
|
||||
});
|
||||
publishInternalEvent("userChangeModeratorState", {
|
||||
id: user.id,
|
||||
isModerator: false
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import define from "../../../define.js";
|
||||
import { Plans } from "../../../../../models/index.js";
|
||||
import { genId } from "../../../../../misc/gen-id.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
},
|
||||
icon: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 64
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
maxLength: 512,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"name",
|
||||
"icon"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const name = ps.name.trim();
|
||||
const icon = ps.icon.trim();
|
||||
const description = ps.description.trim();
|
||||
if (name === "") throw new Error("name is empty");
|
||||
if (icon === "") throw new Error("icon is empty");
|
||||
const exists = await Plans.findOneBy({
|
||||
name
|
||||
});
|
||||
if (exists) throw new Error("plan name already exists");
|
||||
const plan = await Plans.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: null,
|
||||
name,
|
||||
icon,
|
||||
description
|
||||
}).then((x)=>Plans.findOneByOrFail(x.identifiers[0]));
|
||||
insertModerationLog(me, "createPlan", {
|
||||
planId: plan.id,
|
||||
name
|
||||
});
|
||||
return await Plans.pack(plan);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import define from "../../../define.js";
|
||||
import { Plans } from "../../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
planId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"planId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const plan = await Plans.findOneByOrFail({
|
||||
id: ps.planId
|
||||
});
|
||||
await Plans.delete(plan.id);
|
||||
insertModerationLog(me, "deletePlan", {
|
||||
planId: plan.id,
|
||||
name: plan.name
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import define from "../../../define.js";
|
||||
import { Plans } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async ()=>{
|
||||
const plans = await Plans.find({
|
||||
order: {
|
||||
createdAt: "ASC"
|
||||
}
|
||||
});
|
||||
return await Plans.packMany(plans);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import define from "../../../define.js";
|
||||
import { Plans } from "../../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
planId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
},
|
||||
icon: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 64
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
maxLength: 512,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"planId",
|
||||
"name",
|
||||
"icon"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const plan = await Plans.findOneByOrFail({
|
||||
id: ps.planId
|
||||
});
|
||||
const name = ps.name.trim();
|
||||
const icon = ps.icon.trim();
|
||||
const description = ps.description.trim();
|
||||
if (name === "") throw new Error("name is empty");
|
||||
if (icon === "") throw new Error("icon is empty");
|
||||
const exists = await Plans.findOneBy({
|
||||
name
|
||||
});
|
||||
if (exists && exists.id !== plan.id) throw new Error("plan name already exists");
|
||||
await Plans.update(plan.id, {
|
||||
updatedAt: new Date(),
|
||||
name,
|
||||
icon,
|
||||
description
|
||||
});
|
||||
insertModerationLog(me, "updatePlan", {
|
||||
planId: plan.id,
|
||||
name
|
||||
});
|
||||
return await Plans.pack(plan.id);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import define from "../../../define.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
import { Notes, PromoNotes } from "../../../../../models/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: "No such note.",
|
||||
code: "NO_SUCH_NOTE",
|
||||
id: "ee449fbe-af2a-453b-9cae-cf2fe7c895fc"
|
||||
},
|
||||
alreadyPromoted: {
|
||||
message: "The note has already promoted.",
|
||||
code: "ALREADY_PROMOTED",
|
||||
id: "ae427aa2-7a41-484f-a18c-2c1104051604"
|
||||
},
|
||||
notAdService: {
|
||||
message: "The note must have #AdService.",
|
||||
code: "NOT_AD_SERVICE",
|
||||
id: "970a4d57-7b67-4540-80ea-f210041724ef"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
noteId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
expiresAt: {
|
||||
type: "integer"
|
||||
},
|
||||
credits: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100000,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"noteId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
const note = await Notes.findOneBy({
|
||||
id: ps.noteId
|
||||
});
|
||||
if (note == null) {
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
}
|
||||
if (!note.tags.includes("adservice")) {
|
||||
throw new ApiError(meta.errors.notAdService);
|
||||
}
|
||||
const expiresAt = ps.expiresAt ? new Date(ps.expiresAt) : new Date(Date.now() + 30 * 86400000);
|
||||
const credits = ps.credits ?? 1;
|
||||
const exist = await PromoNotes.findOneBy({
|
||||
noteId: note.id
|
||||
});
|
||||
if (exist) {
|
||||
await PromoNotes.update(note.id, {
|
||||
expiresAt: exist.expiresAt.getTime() > expiresAt.getTime() ? exist.expiresAt : expiresAt,
|
||||
totalCredits: exist.totalCredits + credits,
|
||||
remainingCredits: exist.remainingCredits + credits
|
||||
});
|
||||
return;
|
||||
}
|
||||
await PromoNotes.insert({
|
||||
noteId: note.id,
|
||||
expiresAt,
|
||||
totalCredits: credits,
|
||||
remainingCredits: credits,
|
||||
userId: note.userId
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import rndstr from "rndstr";
|
||||
import { Notes, PromoNotes } from "../../../../../models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { makePaginationQuery } from "../../../common/make-pagination-query.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).andWhere(`'adservice' = ANY(note.tags)`).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
|
||||
const notes = await query.take(ps.limit).getMany();
|
||||
if (notes.length === 0) return [];
|
||||
const promos = await PromoNotes.findBy(notes.map((note)=>({
|
||||
noteId: note.id
|
||||
})));
|
||||
return Promise.all(notes.map(async (note)=>{
|
||||
const promo = promos.find((item)=>item.noteId === note.id);
|
||||
const expiredCredits = promo && promo.expiresAt.getTime() <= Date.now() ? promo.remainingCredits : 0;
|
||||
note._prId_ = rndstr("a-z0-9", 8);
|
||||
return {
|
||||
id: note.id,
|
||||
note: await Notes.pack(note, user),
|
||||
expiresAt: promo?.expiresAt.toISOString() ?? null,
|
||||
totalCredits: promo?.totalCredits ?? 0,
|
||||
remainingCredits: promo?.remainingCredits ?? 0,
|
||||
expiredCredits
|
||||
};
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import define from "../../../define.js";
|
||||
import { destroy } from "../../../../../queue/index.js";
|
||||
import { insertModerationLog } from "../../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
destroy();
|
||||
insertModerationLog(me, "clearQueue");
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { deliverQueue } from "../../../../../queue/queues.js";
|
||||
import { URL } from "node:url";
|
||||
import define from "../../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
example: [
|
||||
[
|
||||
"example.com",
|
||||
12
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const jobs = await deliverQueue.getJobs([
|
||||
"delayed"
|
||||
]);
|
||||
const res = [];
|
||||
for (const job of jobs){
|
||||
const host = new URL(job.data.to).host;
|
||||
if (res.find((x)=>x[0] === host)) {
|
||||
res.find((x)=>x[0] === host)[1]++;
|
||||
} else {
|
||||
res.push([
|
||||
host,
|
||||
1
|
||||
]);
|
||||
}
|
||||
}
|
||||
res.sort((a, b)=>b[1] - a[1]);
|
||||
return res;
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { URL } from "node:url";
|
||||
import define from "../../../define.js";
|
||||
import { inboxQueue } from "../../../../../queue/queues.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
type: "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
example: [
|
||||
[
|
||||
"example.com",
|
||||
12
|
||||
]
|
||||
]
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const jobs = await inboxQueue.getJobs([
|
||||
"delayed"
|
||||
]);
|
||||
const res = [];
|
||||
for (const job of jobs){
|
||||
const host = new URL(job.data.signature.keyId).host;
|
||||
if (res.find((x)=>x[0] === host)) {
|
||||
res.find((x)=>x[0] === host)[1]++;
|
||||
} else {
|
||||
res.push([
|
||||
host,
|
||||
1
|
||||
]);
|
||||
}
|
||||
}
|
||||
res.sort((a, b)=>b[1] - a[1]);
|
||||
return res;
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from "../../../../../queue/queues.js";
|
||||
import define from "../../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
deliver: {
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "QueueCount"
|
||||
},
|
||||
inbox: {
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "QueueCount"
|
||||
},
|
||||
db: {
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "QueueCount"
|
||||
},
|
||||
objectStorage: {
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "QueueCount"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const deliverJobCounts = await deliverQueue.getJobCounts();
|
||||
const inboxJobCounts = await inboxQueue.getJobCounts();
|
||||
const dbJobCounts = await dbQueue.getJobCounts();
|
||||
const objectStorageJobCounts = await objectStorageQueue.getJobCounts();
|
||||
return {
|
||||
deliver: deliverJobCounts,
|
||||
inbox: inboxJobCounts,
|
||||
db: dbJobCounts,
|
||||
objectStorage: objectStorageJobCounts
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { URL } from "node:url";
|
||||
import define from "../../../define.js";
|
||||
import { addRelay } from "../../../../../services/relay.js";
|
||||
import { ApiError } from "../../../error.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
errors: {
|
||||
invalidUrl: {
|
||||
message: "Invalid URL",
|
||||
code: "INVALID_URL",
|
||||
id: "fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c"
|
||||
}
|
||||
},
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
inbox: {
|
||||
description: "URL of the inbox, must be a https scheme URL",
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "url"
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
default: "requesting",
|
||||
enum: [
|
||||
"requesting",
|
||||
"accepted",
|
||||
"rejected"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
inbox: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"inbox"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
try {
|
||||
if (new URL(ps.inbox).protocol !== "https:") throw new Error("https only");
|
||||
} catch {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
return await addRelay(ps.inbox);
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import define from "../../../define.js";
|
||||
import { listRelay } from "../../../../../services/relay.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
inbox: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "url"
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
default: "requesting",
|
||||
enum: [
|
||||
"requesting",
|
||||
"accepted",
|
||||
"rejected"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
return await listRelay();
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import define from "../../../define.js";
|
||||
import { removeRelay } from "../../../../../services/relay.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
inbox: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"inbox"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, user)=>{
|
||||
return await removeRelay(ps.inbox);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import define from "../../define.js";
|
||||
// import bcrypt from "bcryptjs";
|
||||
import rndstr from "rndstr";
|
||||
import { Users, UserProfiles } from "../../../../models/index.js";
|
||||
import { hashPassword } from "../../../../misc/password.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
password: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
minLength: 8,
|
||||
maxLength: 8
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
throw new Error("cannot reset password of admin");
|
||||
}
|
||||
const passwd = rndstr("a-zA-Z0-9", 8);
|
||||
// Generate hash of password
|
||||
// const hash = bcrypt.hashSync(passwd);
|
||||
const hash = await hashPassword(passwd);
|
||||
await UserProfiles.update({
|
||||
userId: user.id
|
||||
}, {
|
||||
password: hash
|
||||
});
|
||||
return {
|
||||
password: passwd
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import define from "../../define.js";
|
||||
import { AbuseUserReports, Users } from "../../../../models/index.js";
|
||||
import { getInstanceActor } from "../../../../services/instance-actor.js";
|
||||
import { deliver } from "../../../../queue/index.js";
|
||||
import { renderActivity } from "../../../../remote/activitypub/renderer/index.js";
|
||||
import { renderFlag } from "../../../../remote/activitypub/renderer/flag.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
reportId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
forward: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"reportId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const report = await AbuseUserReports.findOneByOrFail({
|
||||
id: ps.reportId
|
||||
});
|
||||
if (report == null) {
|
||||
throw new Error("report not found");
|
||||
}
|
||||
if (ps.forward && report.targetUserHost != null) {
|
||||
const actor = await getInstanceActor();
|
||||
const targetUser = await Users.findOneByOrFail({
|
||||
id: report.targetUserId
|
||||
});
|
||||
deliver(actor, renderActivity(renderFlag(actor, [
|
||||
targetUser.uri
|
||||
], report.comment)), targetUser.inbox);
|
||||
}
|
||||
await AbuseUserReports.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: me.id,
|
||||
forwarded: ps.forward && report.targetUserHost != null
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import define from "../../define.js";
|
||||
import { Users, VerifiedBadgeRequests } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
requestId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
approve: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"requestId",
|
||||
"approve"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const request = await VerifiedBadgeRequests.findOneByOrFail({
|
||||
id: ps.requestId
|
||||
});
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("request already resolved");
|
||||
}
|
||||
if (ps.approve) {
|
||||
await Users.update(request.userId, {
|
||||
isVerified: true
|
||||
});
|
||||
}
|
||||
await VerifiedBadgeRequests.update(request.id, {
|
||||
status: ps.approve ? "approved" : "rejected",
|
||||
resolvedAt: new Date(),
|
||||
resolverId: me.id
|
||||
});
|
||||
insertModerationLog(me, ps.approve ? "approveVerifiedBadgeRequest" : "rejectVerifiedBadgeRequest", {
|
||||
targetId: request.userId,
|
||||
requestId: request.id
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import define from "../../define.js";
|
||||
import { sendEmail } from "../../../../services/send-email.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
to: {
|
||||
type: "string"
|
||||
},
|
||||
subject: {
|
||||
type: "string"
|
||||
},
|
||||
text: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"to",
|
||||
"subject",
|
||||
"text"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
await sendEmail(ps.to, ps.subject, ps.text, ps.text);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as sanitizeHtml from "sanitize-html";
|
||||
import define from "../../define.js";
|
||||
import { Users, UserProfiles } from "../../../../models/index.js";
|
||||
import { ApiError } from "../../error.js";
|
||||
import { sendEmail } from "../../../../services/send-email.js";
|
||||
import { createNotification } from "../../../../services/create-notification.js";
|
||||
import config from "../../../../config/index.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"users"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
description: "Send a moderation notice.",
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: "No such user.",
|
||||
code: "NO_SUCH_USER",
|
||||
id: "1acefcb5-0959-43fd-9685-b48305736cb5"
|
||||
},
|
||||
noEmail: {
|
||||
message: "No email for user.",
|
||||
code: "NO_EMAIL",
|
||||
id: "ac9d2d22-ef73-11ed-a05b-0242ac120003"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
comment: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
maxLength: 2048
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId",
|
||||
"comment"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const [user, profile] = await Promise.all([
|
||||
Users.findOneBy({
|
||||
id: ps.userId
|
||||
}),
|
||||
UserProfiles.findOneBy({
|
||||
userId: ps.userId
|
||||
})
|
||||
]);
|
||||
if (user == null || profile == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
createNotification(user.id, "app", {
|
||||
customBody: ps.comment,
|
||||
customHeader: "Moderation Notice",
|
||||
customIcon: config?.images?.info
|
||||
});
|
||||
setImmediate(async ()=>{
|
||||
const email = profile.email;
|
||||
if (email == null) {
|
||||
throw new ApiError(meta.errors.noEmail);
|
||||
}
|
||||
sendEmail(email, "Moderation notice", sanitizeHtml(ps.comment), sanitizeHtml(ps.comment));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as os from "node:os";
|
||||
import si from "systeminformation";
|
||||
import define from "../../define.js";
|
||||
import { redisClient } from "../../../../db/redis.js";
|
||||
import { db } from "../../../../db/postgre.js";
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
tags: [
|
||||
"admin",
|
||||
"meta"
|
||||
],
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
machine: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
os: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: "linux"
|
||||
},
|
||||
node: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
psql: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
cpu: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
model: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
cores: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false
|
||||
}
|
||||
}
|
||||
},
|
||||
mem: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
total: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "bytes"
|
||||
}
|
||||
}
|
||||
},
|
||||
fs: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
total: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "bytes"
|
||||
},
|
||||
used: {
|
||||
type: "number",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "bytes"
|
||||
}
|
||||
}
|
||||
},
|
||||
net: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
interface: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
example: "eth0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async ()=>{
|
||||
const memStats = await si.mem();
|
||||
const fsStats = await si.fsSize();
|
||||
const netInterface = await si.networkInterfaceDefault();
|
||||
const redisServerInfo = await redisClient.info("Server");
|
||||
const m = redisServerInfo.match(new RegExp("^redis_version:(.*)", "m"));
|
||||
const redis_version = m?.[1];
|
||||
return {
|
||||
machine: os.hostname(),
|
||||
os: os.platform(),
|
||||
node: process.version,
|
||||
psql: await db.query("SHOW server_version").then((x)=>x[0].server_version),
|
||||
redis: redis_version,
|
||||
cpu: {
|
||||
model: os.cpus()[0].model,
|
||||
cores: os.cpus().length
|
||||
},
|
||||
mem: {
|
||||
total: memStats.total
|
||||
},
|
||||
fs: {
|
||||
total: fsStats[0].size,
|
||||
used: fsStats[0].used
|
||||
},
|
||||
net: {
|
||||
interface: netInterface
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import define from "../../define.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
isVerified: {
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId",
|
||||
"isVerified"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isVerified: ps.isVerified
|
||||
});
|
||||
insertModerationLog(me, ps.isVerified ? "markAsVerified" : "unmarkAsVerified", {
|
||||
targetId: user.id
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import define from "../../define.js";
|
||||
import { ModerationLogs } from "../../../../models/index.js";
|
||||
import { makePaginationQuery } from "../../common/make-pagination-query.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
createdAt: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "date-time"
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
info: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
format: "id"
|
||||
},
|
||||
user: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "UserDetailed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
sinceId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
untilId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps)=>{
|
||||
const query = makePaginationQuery(ModerationLogs.createQueryBuilder("report"), ps.sinceId, ps.untilId);
|
||||
const reports = await query.take(ps.limit).getMany();
|
||||
return await ModerationLogs.packMany(reports);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Signins, UserProfiles, Users } from "../../../../models/index.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "object",
|
||||
nullable: false,
|
||||
optional: false
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const [user, profile] = await Promise.all([
|
||||
Users.findOneBy({
|
||||
id: ps.userId
|
||||
}),
|
||||
UserProfiles.findOneBy({
|
||||
userId: ps.userId
|
||||
})
|
||||
]);
|
||||
if (user == null || profile == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
const _me = await Users.findOneByOrFail({
|
||||
id: me.id
|
||||
});
|
||||
if (_me.isModerator && !_me.isAdmin && user.isAdmin) {
|
||||
throw new Error("cannot show info of admin");
|
||||
}
|
||||
if (!_me.isAdmin) {
|
||||
return {
|
||||
isModerator: user.isModerator,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
moderationNote: profile.moderationNote
|
||||
};
|
||||
}
|
||||
const maskedKeys = [
|
||||
"accessToken",
|
||||
"accessTokenSecret",
|
||||
"refreshToken"
|
||||
];
|
||||
Object.keys(profile.integrations).forEach((integration)=>{
|
||||
maskedKeys.forEach((key)=>profile.integrations[integration][key] = "<MASKED>");
|
||||
});
|
||||
const signins = await Signins.findBy({
|
||||
userId: user.id
|
||||
});
|
||||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||
noCrawle: profile.noCrawle,
|
||||
preventAiLearning: profile.preventAiLearning,
|
||||
alwaysMarkNsfw: profile.alwaysMarkNsfw,
|
||||
carefulBot: profile.carefulBot,
|
||||
injectFeaturedNote: profile.injectFeaturedNote,
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
integrations: profile.integrations,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
isModerator: user.isModerator,
|
||||
isVerified: user.isVerified,
|
||||
isSilenced: user.isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
lastActiveDate: user.lastActiveDate,
|
||||
moderationNote: profile.moderationNote,
|
||||
signins
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import define from "../../define.js";
|
||||
import { sqlLikeEscape } from "../../../../misc/sql-like-escape.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
res: {
|
||||
type: "array",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
items: {
|
||||
type: "object",
|
||||
nullable: false,
|
||||
optional: false,
|
||||
ref: "UserDetailed"
|
||||
}
|
||||
}
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 10
|
||||
},
|
||||
offset: {
|
||||
type: "integer",
|
||||
default: 0
|
||||
},
|
||||
sort: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"+follower",
|
||||
"-follower",
|
||||
"+createdAt",
|
||||
"-createdAt",
|
||||
"+updatedAt",
|
||||
"-updatedAt"
|
||||
]
|
||||
},
|
||||
state: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"all",
|
||||
"alive",
|
||||
"available",
|
||||
"admin",
|
||||
"moderator",
|
||||
"adminOrModerator",
|
||||
"silenced",
|
||||
"suspended",
|
||||
"verified"
|
||||
],
|
||||
default: "all"
|
||||
},
|
||||
origin: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"combined",
|
||||
"local",
|
||||
"remote"
|
||||
],
|
||||
default: "combined"
|
||||
},
|
||||
username: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null
|
||||
},
|
||||
hostname: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: "The local host is represented with `null`."
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const query = Users.createQueryBuilder("user");
|
||||
switch(ps.state){
|
||||
case "available":
|
||||
query.where("user.isSuspended = FALSE");
|
||||
break;
|
||||
case "admin":
|
||||
query.where("user.isAdmin = TRUE");
|
||||
break;
|
||||
case "moderator":
|
||||
query.where("user.isModerator = TRUE");
|
||||
break;
|
||||
case "adminOrModerator":
|
||||
query.where("user.isAdmin = TRUE OR user.isModerator = TRUE");
|
||||
break;
|
||||
case "alive":
|
||||
query.where("user.updatedAt > :date", {
|
||||
date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5)
|
||||
});
|
||||
break;
|
||||
case "silenced":
|
||||
query.where("user.isSilenced = TRUE");
|
||||
break;
|
||||
case "suspended":
|
||||
query.where("user.isSuspended = TRUE");
|
||||
break;
|
||||
case "verified":
|
||||
query.where("user.isVerified = TRUE");
|
||||
break;
|
||||
}
|
||||
switch(ps.origin){
|
||||
case "local":
|
||||
query.andWhere("user.host IS NULL");
|
||||
break;
|
||||
case "remote":
|
||||
query.andWhere("user.host IS NOT NULL");
|
||||
break;
|
||||
}
|
||||
if (ps.username) {
|
||||
query.andWhere("user.usernameLower like :username", {
|
||||
username: `${sqlLikeEscape(ps.username.toLowerCase())}%`
|
||||
});
|
||||
}
|
||||
if (ps.hostname) {
|
||||
query.andWhere("user.host = :hostname", {
|
||||
hostname: ps.hostname.toLowerCase()
|
||||
});
|
||||
}
|
||||
switch(ps.sort){
|
||||
case "+follower":
|
||||
query.orderBy("user.followersCount", "DESC");
|
||||
break;
|
||||
case "-follower":
|
||||
query.orderBy("user.followersCount", "ASC");
|
||||
break;
|
||||
case "+createdAt":
|
||||
query.orderBy("user.createdAt", "DESC");
|
||||
break;
|
||||
case "-createdAt":
|
||||
query.orderBy("user.createdAt", "ASC");
|
||||
break;
|
||||
case "+updatedAt":
|
||||
query.orderBy("user.updatedAt", "DESC", "NULLS LAST");
|
||||
break;
|
||||
case "-updatedAt":
|
||||
query.orderBy("user.updatedAt", "ASC", "NULLS FIRST");
|
||||
break;
|
||||
default:
|
||||
query.orderBy("user.id", "ASC");
|
||||
break;
|
||||
}
|
||||
query.take(ps.limit);
|
||||
query.skip(ps.offset);
|
||||
const users = await query.getMany();
|
||||
return await Users.packMany(users, me, {
|
||||
detail: true
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import define from "../../define.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { publishInternalEvent } from "../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
throw new Error("cannot silence admin");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isSilenced: true
|
||||
});
|
||||
publishInternalEvent("userChangeSilencedState", {
|
||||
id: user.id,
|
||||
isSilenced: true
|
||||
});
|
||||
insertModerationLog(me, "silence", {
|
||||
targetId: user.id
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import define from "../../define.js";
|
||||
import deleteFollowing from "../../../../services/following/delete.js";
|
||||
import { Users, Followings, Notifications } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { doPostSuspend } from "../../../../services/suspend-user.js";
|
||||
import { publishUserEvent } from "../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
throw new Error("cannot suspend admin");
|
||||
}
|
||||
if (user.isModerator) {
|
||||
throw new Error("cannot suspend moderator");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isSuspended: true
|
||||
});
|
||||
insertModerationLog(me, "suspend", {
|
||||
targetId: user.id
|
||||
});
|
||||
// Terminate streaming
|
||||
if (Users.isLocalUser(user)) {
|
||||
publishUserEvent(user.id, "terminate", {});
|
||||
}
|
||||
(async ()=>{
|
||||
await doPostSuspend(user).catch((e)=>{});
|
||||
await unFollowAll(user).catch((e)=>{});
|
||||
await readAllNotify(user).catch((e)=>{});
|
||||
})();
|
||||
});
|
||||
async function unFollowAll(follower) {
|
||||
const followings = await Followings.findBy({
|
||||
followerId: follower.id
|
||||
});
|
||||
for (const following of followings){
|
||||
const followee = await Users.findOneBy({
|
||||
id: following.followeeId
|
||||
});
|
||||
if (followee == null) {
|
||||
throw new Error(`Cant find followee ${following.followeeId}`);
|
||||
}
|
||||
await deleteFollowing(follower, followee, true);
|
||||
}
|
||||
}
|
||||
async function readAllNotify(notifier) {
|
||||
await Notifications.update({
|
||||
notifierId: notifier.id,
|
||||
isRead: false
|
||||
}, {
|
||||
isRead: true
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import define from "../../define.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { publishInternalEvent } from "../../../../services/stream.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isSilenced: false
|
||||
});
|
||||
publishInternalEvent("userChangeSilencedState", {
|
||||
id: user.id,
|
||||
isSilenced: false
|
||||
});
|
||||
insertModerationLog(me, "unsilence", {
|
||||
targetId: user.id
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import define from "../../define.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import { doPostUnsuspend } from "../../../../services/unsuspend-user.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
await Users.update(user.id, {
|
||||
isSuspended: false
|
||||
});
|
||||
insertModerationLog(me, "unsuspend", {
|
||||
targetId: user.id
|
||||
});
|
||||
doPostUnsuspend(user);
|
||||
});
|
||||
@@ -0,0 +1,671 @@
|
||||
import { insertModerationLog } from "../../../../services/insert-moderation-log.js";
|
||||
import define from "../../define.js";
|
||||
import { Metas } from "../../../../models/index.js";
|
||||
import { Users } from "../../../../models/index.js";
|
||||
import { IsNull } from "typeorm";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
disableRegistration: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
disableLocalTimeline: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
disableRecommendedTimeline: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
disableGlobalTimeline: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
defaultReaction: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
recommendedInstances: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
pinnedUsers: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
customMOTD: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
customSplashIcons: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
hiddenTags: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
blockedHosts: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
silencedHosts: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
allowedHosts: {
|
||||
type: "array",
|
||||
nullable: true,
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
secureMode: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
privateMode: {
|
||||
type: "boolean",
|
||||
nullable: true
|
||||
},
|
||||
themeColor: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
pattern: "^#[0-9a-fA-F]{6}$"
|
||||
},
|
||||
mascotImageUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
bannerUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
logoImageUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
errorImageUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
iconUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
backgroundImageUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
defaultLightTheme: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
defaultDarkTheme: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
localDriveCapacityMb: {
|
||||
type: "integer"
|
||||
},
|
||||
remoteDriveCapacityMb: {
|
||||
type: "integer"
|
||||
},
|
||||
lua4frozenDatabaseCapacityMb: {
|
||||
type: "integer"
|
||||
},
|
||||
cacheRemoteFiles: {
|
||||
type: "boolean"
|
||||
},
|
||||
emailRequiredForSignup: {
|
||||
type: "boolean"
|
||||
},
|
||||
enableHcaptcha: {
|
||||
type: "boolean"
|
||||
},
|
||||
hcaptchaSiteKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
hcaptchaSecretKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
enableRecaptcha: {
|
||||
type: "boolean"
|
||||
},
|
||||
recaptchaSiteKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
recaptchaSecretKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
maintainerName: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
maintainerEmail: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
pinnedPages: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
pinnedClipId: {
|
||||
type: "string",
|
||||
format: "misskey:id",
|
||||
nullable: true
|
||||
},
|
||||
langs: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
summalyProxy: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
deeplAuthKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
deeplIsPro: {
|
||||
type: "boolean"
|
||||
},
|
||||
libreTranslateApiUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
libreTranslateApiKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
enableGithubIntegration: {
|
||||
type: "boolean"
|
||||
},
|
||||
githubClientId: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
githubClientSecret: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
enableDiscordIntegration: {
|
||||
type: "boolean"
|
||||
},
|
||||
discordClientId: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
discordClientSecret: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
enableEmail: {
|
||||
type: "boolean"
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
smtpSecure: {
|
||||
type: "boolean"
|
||||
},
|
||||
smtpHost: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
smtpPort: {
|
||||
type: "integer",
|
||||
nullable: true
|
||||
},
|
||||
smtpUser: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
smtpPass: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
tosUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
repositoryUrl: {
|
||||
type: "string"
|
||||
},
|
||||
feedbackUrl: {
|
||||
type: "string"
|
||||
},
|
||||
useObjectStorage: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectStorageBaseUrl: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageBucket: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStoragePrefix: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageEndpoint: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageRegion: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStoragePort: {
|
||||
type: "integer",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageAccessKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageSecretKey: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
objectStorageUseSSL: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectStorageUseProxy: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectStorageSetPublicRead: {
|
||||
type: "boolean"
|
||||
},
|
||||
objectStorageS3ForcePathStyle: {
|
||||
type: "boolean"
|
||||
},
|
||||
enableIpLogging: {
|
||||
type: "boolean"
|
||||
},
|
||||
enableActiveEmailValidation: {
|
||||
type: "boolean"
|
||||
},
|
||||
experimentalFeatures: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
properties: {
|
||||
postImports: {
|
||||
type: "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
enableServerMachineStats: {
|
||||
type: "boolean"
|
||||
},
|
||||
enableIdenticonGeneration: {
|
||||
type: "boolean"
|
||||
},
|
||||
donationLink: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
},
|
||||
autofollowedAccount: {
|
||||
type: "string",
|
||||
nullable: true
|
||||
}
|
||||
},
|
||||
required: []
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const set = {};
|
||||
if (typeof ps.disableRegistration === "boolean") {
|
||||
set.disableRegistration = ps.disableRegistration;
|
||||
}
|
||||
if (typeof ps.disableLocalTimeline === "boolean") {
|
||||
set.disableLocalTimeline = ps.disableLocalTimeline;
|
||||
}
|
||||
if (typeof ps.disableRecommendedTimeline === "boolean") {
|
||||
set.disableRecommendedTimeline = ps.disableRecommendedTimeline;
|
||||
}
|
||||
if (typeof ps.disableGlobalTimeline === "boolean") {
|
||||
set.disableGlobalTimeline = ps.disableGlobalTimeline;
|
||||
}
|
||||
if (typeof ps.defaultReaction === "string") {
|
||||
set.defaultReaction = ps.defaultReaction;
|
||||
}
|
||||
if (Array.isArray(ps.pinnedUsers)) {
|
||||
set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(ps.customMOTD)) {
|
||||
set.customMOTD = ps.customMOTD.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(ps.customSplashIcons)) {
|
||||
set.customSplashIcons = ps.customSplashIcons.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(ps.recommendedInstances)) {
|
||||
set.recommendedInstances = ps.recommendedInstances.filter(Boolean);
|
||||
if (set.recommendedInstances?.length > 0) {
|
||||
set.recommendedInstances.forEach((instance, index)=>{
|
||||
if (/^https?:\/\//i.test(instance)) {
|
||||
set.recommendedInstances[index] = instance.replace(/^https?:\/\//i, "").replace(/\/$/, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(ps.hiddenTags)) {
|
||||
set.hiddenTags = ps.hiddenTags.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(ps.blockedHosts)) {
|
||||
let lastValue = "";
|
||||
set.blockedHosts = ps.blockedHosts.sort().filter((h)=>{
|
||||
const lv = lastValue;
|
||||
lastValue = h;
|
||||
return h !== "" && h !== lv;
|
||||
});
|
||||
}
|
||||
if (Array.isArray(ps.silencedHosts)) {
|
||||
let lastValue = "";
|
||||
set.silencedHosts = ps.silencedHosts.sort().filter((h)=>{
|
||||
const lv = lastValue;
|
||||
lastValue = h;
|
||||
return h !== "" && h !== lv;
|
||||
});
|
||||
}
|
||||
if (ps.themeColor !== undefined) {
|
||||
set.themeColor = ps.themeColor;
|
||||
}
|
||||
if (Array.isArray(ps.allowedHosts)) {
|
||||
set.allowedHosts = ps.allowedHosts.filter(Boolean);
|
||||
}
|
||||
if (typeof ps.privateMode === "boolean") {
|
||||
set.privateMode = ps.privateMode;
|
||||
}
|
||||
if (typeof ps.secureMode === "boolean") {
|
||||
set.secureMode = ps.secureMode;
|
||||
}
|
||||
if (ps.mascotImageUrl !== undefined) {
|
||||
set.mascotImageUrl = ps.mascotImageUrl;
|
||||
}
|
||||
if (ps.bannerUrl !== undefined) {
|
||||
set.bannerUrl = ps.bannerUrl;
|
||||
}
|
||||
if (ps.logoImageUrl !== undefined) {
|
||||
set.logoImageUrl = ps.logoImageUrl;
|
||||
}
|
||||
if (ps.iconUrl !== undefined) {
|
||||
set.iconUrl = ps.iconUrl;
|
||||
}
|
||||
if (ps.backgroundImageUrl !== undefined) {
|
||||
set.backgroundImageUrl = ps.backgroundImageUrl;
|
||||
}
|
||||
if (ps.logoImageUrl !== undefined) {
|
||||
set.logoImageUrl = ps.logoImageUrl;
|
||||
}
|
||||
if (ps.name !== undefined) {
|
||||
set.name = ps.name;
|
||||
}
|
||||
if (ps.description !== undefined) {
|
||||
set.description = ps.description;
|
||||
}
|
||||
if (ps.defaultLightTheme !== undefined) {
|
||||
set.defaultLightTheme = ps.defaultLightTheme;
|
||||
}
|
||||
if (ps.defaultDarkTheme !== undefined) {
|
||||
set.defaultDarkTheme = ps.defaultDarkTheme;
|
||||
}
|
||||
if (ps.localDriveCapacityMb !== undefined) {
|
||||
set.localDriveCapacityMb = ps.localDriveCapacityMb;
|
||||
}
|
||||
if (ps.remoteDriveCapacityMb !== undefined) {
|
||||
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
|
||||
}
|
||||
if (ps.lua4frozenDatabaseCapacityMb !== undefined) {
|
||||
set.lua4frozenDatabaseCapacityMb = ps.lua4frozenDatabaseCapacityMb;
|
||||
}
|
||||
if (ps.cacheRemoteFiles !== undefined) {
|
||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||
}
|
||||
if (ps.emailRequiredForSignup !== undefined) {
|
||||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||
}
|
||||
if (ps.enableHcaptcha !== undefined) {
|
||||
set.enableHcaptcha = ps.enableHcaptcha;
|
||||
}
|
||||
if (ps.hcaptchaSiteKey !== undefined) {
|
||||
set.hcaptchaSiteKey = ps.hcaptchaSiteKey;
|
||||
}
|
||||
if (ps.hcaptchaSecretKey !== undefined) {
|
||||
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
|
||||
}
|
||||
if (ps.enableRecaptcha !== undefined) {
|
||||
set.enableRecaptcha = ps.enableRecaptcha;
|
||||
}
|
||||
if (ps.recaptchaSiteKey !== undefined) {
|
||||
set.recaptchaSiteKey = ps.recaptchaSiteKey;
|
||||
}
|
||||
if (ps.recaptchaSecretKey !== undefined) {
|
||||
set.recaptchaSecretKey = ps.recaptchaSecretKey;
|
||||
}
|
||||
if (ps.maintainerName !== undefined) {
|
||||
set.maintainerName = ps.maintainerName;
|
||||
}
|
||||
if (ps.maintainerEmail !== undefined) {
|
||||
set.maintainerEmail = ps.maintainerEmail;
|
||||
}
|
||||
if (Array.isArray(ps.langs)) {
|
||||
set.langs = ps.langs.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(ps.pinnedPages)) {
|
||||
set.pinnedPages = ps.pinnedPages.filter(Boolean);
|
||||
}
|
||||
if (ps.pinnedClipId !== undefined) {
|
||||
set.pinnedClipId = ps.pinnedClipId;
|
||||
}
|
||||
if (ps.summalyProxy !== undefined) {
|
||||
set.summalyProxy = ps.summalyProxy;
|
||||
}
|
||||
if (ps.enableGithubIntegration !== undefined) {
|
||||
set.enableGithubIntegration = ps.enableGithubIntegration;
|
||||
}
|
||||
if (ps.githubClientId !== undefined) {
|
||||
set.githubClientId = ps.githubClientId;
|
||||
}
|
||||
if (ps.githubClientSecret !== undefined) {
|
||||
set.githubClientSecret = ps.githubClientSecret;
|
||||
}
|
||||
if (ps.enableDiscordIntegration !== undefined) {
|
||||
set.enableDiscordIntegration = ps.enableDiscordIntegration;
|
||||
}
|
||||
if (ps.discordClientId !== undefined) {
|
||||
set.discordClientId = ps.discordClientId;
|
||||
}
|
||||
if (ps.discordClientSecret !== undefined) {
|
||||
set.discordClientSecret = ps.discordClientSecret;
|
||||
}
|
||||
if (ps.enableEmail !== undefined) {
|
||||
set.enableEmail = ps.enableEmail;
|
||||
}
|
||||
if (ps.email !== undefined) {
|
||||
set.email = ps.email;
|
||||
}
|
||||
if (ps.smtpSecure !== undefined) {
|
||||
set.smtpSecure = ps.smtpSecure;
|
||||
}
|
||||
if (ps.smtpHost !== undefined) {
|
||||
set.smtpHost = ps.smtpHost;
|
||||
}
|
||||
if (ps.smtpPort !== undefined) {
|
||||
set.smtpPort = ps.smtpPort;
|
||||
}
|
||||
if (ps.smtpUser !== undefined) {
|
||||
set.smtpUser = ps.smtpUser;
|
||||
}
|
||||
if (ps.smtpPass !== undefined) {
|
||||
set.smtpPass = ps.smtpPass;
|
||||
}
|
||||
if (ps.errorImageUrl !== undefined) {
|
||||
set.errorImageUrl = ps.errorImageUrl;
|
||||
}
|
||||
if (ps.tosUrl !== undefined) {
|
||||
set.ToSUrl = ps.tosUrl;
|
||||
}
|
||||
if (ps.repositoryUrl !== undefined) {
|
||||
set.repositoryUrl = ps.repositoryUrl;
|
||||
}
|
||||
if (ps.feedbackUrl !== undefined) {
|
||||
set.feedbackUrl = ps.feedbackUrl;
|
||||
}
|
||||
if (ps.useObjectStorage !== undefined) {
|
||||
set.useObjectStorage = ps.useObjectStorage;
|
||||
}
|
||||
if (ps.objectStorageBaseUrl !== undefined) {
|
||||
set.objectStorageBaseUrl = ps.objectStorageBaseUrl;
|
||||
}
|
||||
if (ps.objectStorageBucket !== undefined) {
|
||||
set.objectStorageBucket = ps.objectStorageBucket;
|
||||
}
|
||||
if (ps.objectStoragePrefix !== undefined) {
|
||||
set.objectStoragePrefix = ps.objectStoragePrefix;
|
||||
}
|
||||
if (ps.objectStorageEndpoint !== undefined) {
|
||||
set.objectStorageEndpoint = ps.objectStorageEndpoint;
|
||||
}
|
||||
if (ps.objectStorageRegion !== undefined) {
|
||||
set.objectStorageRegion = ps.objectStorageRegion;
|
||||
}
|
||||
if (ps.objectStoragePort !== undefined) {
|
||||
set.objectStoragePort = ps.objectStoragePort;
|
||||
}
|
||||
if (ps.objectStorageAccessKey !== undefined) {
|
||||
set.objectStorageAccessKey = ps.objectStorageAccessKey;
|
||||
}
|
||||
if (ps.objectStorageSecretKey !== undefined) {
|
||||
set.objectStorageSecretKey = ps.objectStorageSecretKey;
|
||||
}
|
||||
if (ps.objectStorageUseSSL !== undefined) {
|
||||
set.objectStorageUseSSL = ps.objectStorageUseSSL;
|
||||
}
|
||||
if (ps.objectStorageUseProxy !== undefined) {
|
||||
set.objectStorageUseProxy = ps.objectStorageUseProxy;
|
||||
}
|
||||
if (ps.objectStorageSetPublicRead !== undefined) {
|
||||
set.objectStorageSetPublicRead = ps.objectStorageSetPublicRead;
|
||||
}
|
||||
if (ps.objectStorageS3ForcePathStyle !== undefined) {
|
||||
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
|
||||
}
|
||||
if (ps.deeplAuthKey !== undefined) {
|
||||
if (ps.deeplAuthKey === "") {
|
||||
set.deeplAuthKey = null;
|
||||
} else {
|
||||
set.deeplAuthKey = ps.deeplAuthKey;
|
||||
}
|
||||
}
|
||||
if (ps.deeplIsPro !== undefined) {
|
||||
set.deeplIsPro = ps.deeplIsPro;
|
||||
}
|
||||
if (ps.libreTranslateApiUrl !== undefined) {
|
||||
if (ps.libreTranslateApiUrl === "") {
|
||||
set.libreTranslateApiUrl = null;
|
||||
} else {
|
||||
set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
|
||||
}
|
||||
}
|
||||
if (ps.libreTranslateApiKey !== undefined) {
|
||||
if (ps.libreTranslateApiKey === "") {
|
||||
set.libreTranslateApiKey = null;
|
||||
} else {
|
||||
set.libreTranslateApiKey = ps.libreTranslateApiKey;
|
||||
}
|
||||
}
|
||||
if (ps.enableIpLogging !== undefined) {
|
||||
set.enableIpLogging = ps.enableIpLogging;
|
||||
}
|
||||
if (ps.enableActiveEmailValidation !== undefined) {
|
||||
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
||||
}
|
||||
if (ps.experimentalFeatures !== undefined) {
|
||||
set.experimentalFeatures = ps.experimentalFeatures || undefined;
|
||||
}
|
||||
if (ps.enableServerMachineStats !== undefined) {
|
||||
set.enableServerMachineStats = ps.enableServerMachineStats;
|
||||
}
|
||||
if (ps.enableIdenticonGeneration !== undefined) {
|
||||
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
|
||||
}
|
||||
if (ps.donationLink !== undefined) {
|
||||
set.donationLink = ps.donationLink;
|
||||
if (set.donationLink && !/^https?:\/\//i.test(set.donationLink)) {
|
||||
set.donationLink = `https://${set.donationLink}`;
|
||||
}
|
||||
}
|
||||
if (ps.autofollowedAccount !== undefined) {
|
||||
if (ps.autofollowedAccount === null) {
|
||||
set.autofollowedAccount = null;
|
||||
} else {
|
||||
// Verify account exists and is a local account
|
||||
const user = await Users.findOneBy({
|
||||
username: ps.autofollowedAccount,
|
||||
host: IsNull()
|
||||
});
|
||||
if (user !== null) {
|
||||
set.autofollowedAccount = user.username;
|
||||
} else {
|
||||
set.autofollowedAccount = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
const meta = await Metas.findOne({
|
||||
where: {},
|
||||
order: {
|
||||
id: "DESC"
|
||||
}
|
||||
});
|
||||
if (meta) await Metas.update(meta.id, set);
|
||||
else await Metas.save(set);
|
||||
insertModerationLog(me, "updateMeta");
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { UserProfiles, Users } from "../../../../models/index.js";
|
||||
import define from "../../define.js";
|
||||
export const meta = {
|
||||
tags: [
|
||||
"admin"
|
||||
],
|
||||
requireCredential: true,
|
||||
requireModerator: true
|
||||
};
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
userId: {
|
||||
type: "string",
|
||||
format: "misskey:id"
|
||||
},
|
||||
text: {
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
"userId",
|
||||
"text"
|
||||
]
|
||||
};
|
||||
export default define(meta, paramDef, async (ps, me)=>{
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId
|
||||
});
|
||||
if (user == null) {
|
||||
throw new Error("user not found");
|
||||
}
|
||||
await UserProfiles.update({
|
||||
userId: user.id
|
||||
}, {
|
||||
moderationNote: ps.text
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user