refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

View File

@ -0,0 +1,134 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { AbuseUserReports } from '@/models/index';
import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
state: {
validator: $.optional.nullable.str,
default: null,
},
reporterOrigin: {
validator: $.optional.str.or([
'combined',
'local',
'remote',
]),
default: 'combined'
},
targetUserOrigin: {
validator: $.optional.str.or([
'combined',
'local',
'remote',
]),
default: 'combined'
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'date-time',
},
comment: {
type: 'string' as const,
nullable: false as const, optional: false as const,
},
resolved: {
type: 'boolean' as const,
nullable: false as const, optional: false as const,
example: false
},
reporterId: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
},
targetUserId: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id',
},
assigneeId: {
type: 'string' as const,
nullable: true as const, optional: false as const,
format: 'id',
},
reporter: {
type: 'object' as const,
nullable: false as const, optional: false as const,
ref: 'User'
},
targetUser: {
type: 'object' as const,
nullable: false as const, optional: false as const,
ref: 'User'
},
assignee: {
type: 'object' as const,
nullable: true as const, optional: true as const,
ref: 'User'
}
}
}
}
};
export default define(meta, 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);
});

View File

@ -0,0 +1,51 @@
import define from '../../../define';
import { Users } from '@/models/index';
import { signup } from '../../../common/signup';
export const meta = {
tags: ['admin'],
params: {
username: {
validator: Users.validateLocalUsername,
},
password: {
validator: Users.validatePassword,
}
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User',
properties: {
token: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
}
}
};
export default define(meta, async (ps, _me) => {
const me = _me ? await Users.findOneOrFail(_me.id) : null;
const noUsers = (await Users.count({
host: null,
})) === 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 as any).token = secret;
return res;
});

View File

@ -0,0 +1,58 @@
import $ from 'cafy';
import define from '../../../define';
import { Users } from '@/models/index';
import { doPostSuspend } from '@/services/suspend-user';
import { publishUserEvent } from '@/services/stream';
import { createDeleteAccountJob } from '@/queue';
import { ID } from '@/misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(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 // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
});
}
await Users.update(user.id, {
isDeleted: true,
});
if (Users.isLocalUser(user)) {
// Terminate streaming
publishUserEvent(user.id, 'terminate', {});
}
});

View File

@ -0,0 +1,49 @@
import $ from 'cafy';
import define from '../../../define';
import { Ads } from '@/models/index';
import { genId } from '@/misc/gen-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
url: {
validator: $.str.min(1)
},
memo: {
validator: $.str
},
place: {
validator: $.str
},
priority: {
validator: $.str
},
ratio: {
validator: $.num.int().min(0)
},
expiresAt: {
validator: $.num.int()
},
imageUrl: {
validator: $.str.min(1)
}
},
};
export default define(meta, async (ps) => {
await Ads.insert({
id: genId(),
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
});
});

View File

@ -0,0 +1,34 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Ads } from '@/models/index';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
}
},
errors: {
noSuchAd: {
message: 'No such ad.',
code: 'NO_SUCH_AD',
id: 'ccac9863-3a03-416e-b899-8a64041118b1'
}
}
};
export default define(meta, async (ps, me) => {
const ad = await Ads.findOne(ps.id);
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
await Ads.delete(ad.id);
});

View File

@ -0,0 +1,36 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { Ads } from '@/models/index';
import { makePaginationQuery } from '../../../common/make-pagination-query';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
},
};
export default define(meta, async (ps) => {
const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
.andWhere('ad.expiresAt > :now', { now: new Date() });
const ads = await query.take(ps.limit!).getMany();
return ads;
});

View File

@ -0,0 +1,63 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Ads } from '@/models/index';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
},
memo: {
validator: $.str
},
url: {
validator: $.str.min(1)
},
imageUrl: {
validator: $.str.min(1)
},
place: {
validator: $.str
},
priority: {
validator: $.str
},
ratio: {
validator: $.num.int().min(0)
},
expiresAt: {
validator: $.num.int()
},
},
errors: {
noSuchAd: {
message: 'No such ad.',
code: 'NO_SUCH_AD',
id: 'b7aa1727-1354-47bc-a182-3a9c3973d300'
}
}
};
export default define(meta, async (ps, me) => {
const ad = await Ads.findOne(ps.id);
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
await Ads.update(ad.id, {
url: ps.url,
place: ps.place,
priority: ps.priority,
ratio: ps.ratio,
memo: ps.memo,
imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt),
});
});

View File

@ -0,0 +1,71 @@
import $ from 'cafy';
import define from '../../../define';
import { Announcements } from '@/models/index';
import { genId } from '@/misc/gen-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
title: {
validator: $.str.min(1)
},
text: {
validator: $.str.min(1)
},
imageUrl: {
validator: $.nullable.str.min(1)
}
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
updatedAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time',
},
title: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
text: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
imageUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
}
}
}
};
export default define(meta, async (ps) => {
const announcement = await Announcements.save({
id: genId(),
createdAt: new Date(),
updatedAt: null,
title: ps.title,
text: ps.text,
imageUrl: ps.imageUrl,
});
return announcement;
});

View File

@ -0,0 +1,34 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Announcements } from '@/models/index';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
}
},
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'ecad8040-a276-4e85-bda9-015a708d291e'
}
}
};
export default define(meta, async (ps, me) => {
const announcement = await Announcements.findOne(ps.id);
if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);
await Announcements.delete(announcement.id);
});

View File

@ -0,0 +1,84 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { Announcements, AnnouncementReads } from '@/models/index';
import { makePaginationQuery } from '../../../common/make-pagination-query';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
updatedAt: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'date-time',
},
text: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
title: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
imageUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
reads: {
type: 'number' as const,
optional: false as const, nullable: false as const,
}
}
}
}
};
export default define(meta, async (ps) => {
const query = makePaginationQuery(Announcements.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const announcements = await query.take(ps.limit!).getMany();
for (const announcement of announcements) {
(announcement as any).reads = await AnnouncementReads.count({
announcementId: announcement.id
});
}
return announcements;
});

View File

@ -0,0 +1,48 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Announcements } from '@/models/index';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
},
title: {
validator: $.str.min(1)
},
text: {
validator: $.str.min(1)
},
imageUrl: {
validator: $.nullable.str.min(1)
}
},
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'd3aae5a7-6372-4cb4-b61c-f511ffc2d7cc'
}
}
};
export default define(meta, async (ps, me) => {
const announcement = await Announcements.findOne(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,
});
});

View File

@ -0,0 +1,28 @@
import $ from 'cafy';
import define from '../../define';
import { deleteFile } from '@/services/drive/delete-file';
import { DriveFiles } from '@/models/index';
import { ID } from '@/misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userId: ps.userId
});
for (const file of files) {
deleteFile(file);
}
});

View File

@ -0,0 +1,13 @@
import define from '../../define';
import { Logs } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
};
export default define(meta, async (ps) => {
await Logs.clear(); // TRUNCATE
});

View File

@ -0,0 +1,13 @@
import define from '../../../define';
import { createCleanRemoteFilesJob } from '@/queue/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
};
export default define(meta, async (ps, me) => {
createCleanRemoteFilesJob();
});

View File

@ -0,0 +1,21 @@
import { IsNull } from 'typeorm';
import define from '../../../define';
import { deleteFile } from '@/services/drive/delete-file';
import { DriveFiles } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userId: IsNull()
});
for (const file of files) {
deleteFile(file);
}
});

View File

@ -0,0 +1,81 @@
import $ from 'cafy';
import define from '../../../define';
import { DriveFiles } from '@/models/index';
import { makePaginationQuery } from '../../../common/make-pagination-query';
import { ID } from '@/misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: false as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
type: {
validator: $.optional.nullable.str.match(/^[a-zA-Z0-9\/\-*]+$/)
},
origin: {
validator: $.optional.str.or([
'combined',
'local',
'remote',
]),
default: 'local'
},
hostname: {
validator: $.optional.nullable.str,
default: null
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'DriveFile'
}
}
};
export default define(meta, async (ps, me) => {
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
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 });
});

View File

@ -0,0 +1,180 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { DriveFiles } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
fileId: {
validator: $.optional.type(ID),
},
url: {
validator: $.optional.str,
},
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: 'caf3ca38-c6e5-472e-a30c-b05377dcc240'
}
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time',
},
userId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
userHost: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
md5: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'md5',
example: '15eca7fba0480996e2245f5185bf39f2'
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'lenna.jpg'
},
type: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'image/jpeg'
},
size: {
type: 'number' as const,
optional: false as const, nullable: false as const,
example: 51469
},
comment: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
blurhash: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
properties: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
width: {
type: 'number' as const,
optional: false as const, nullable: false as const,
example: 1280
},
height: {
type: 'number' as const,
optional: false as const, nullable: false as const,
example: 720
},
avgColor: {
type: 'string' as const,
optional: true as const, nullable: false as const,
example: 'rgb(40,65,87)'
}
}
},
storedInternal: {
type: 'boolean' as const,
optional: false as const, nullable: true as const,
example: true
},
url: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
},
thumbnailUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
},
webpublicUrl: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'url',
},
accessKey: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
thumbnailAccessKey: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
webpublicAccessKey: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
uri: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
src: {
type: 'string' as const,
optional: false as const, nullable: true as const
},
folderId: {
type: 'string' as const,
optional: false as const, nullable: true as const,
format: 'id',
example: 'xxxxxxxxxx',
},
isSensitive: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
},
isLink: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
}
}
}
};
export default define(meta, async (ps, me) => {
const file = ps.fileId ? await DriveFiles.findOne(ps.fileId) : await DriveFiles.findOne({
where: [{
url: ps.url
}, {
thumbnailUrl: ps.url
}, {
webpublicUrl: ps.url
}]
});
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
return file;
});

View File

@ -0,0 +1,64 @@
import $ from 'cafy';
import define from '../../../define';
import { Emojis, DriveFiles } from '@/models/index';
import { genId } from '@/misc/gen-id';
import { getConnection } from 'typeorm';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { ApiError } from '../../../error';
import { ID } from '@/misc/cafy-id';
import rndstr from 'rndstr';
import { publishBroadcastStream } from '@/services/stream';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
fileId: {
validator: $.type(ID)
},
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'MO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf'
}
}
};
export default define(meta, async (ps, me) => {
const file = await DriveFiles.findOne(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 emoji = await Emojis.save({
id: genId(),
updatedAt: new Date(),
name: name,
category: null,
host: null,
aliases: [],
url: file.url,
type: file.type,
});
await getConnection().queryResultCache!.remove(['meta_emojis']);
publishBroadcastStream('emojiAdded', {
emoji: await Emojis.pack(emoji.id)
});
insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id
});
return {
id: emoji.id
};
});

View File

@ -0,0 +1,81 @@
import $ from 'cafy';
import define from '../../../define';
import { Emojis } from '@/models/index';
import { genId } from '@/misc/gen-id';
import { getConnection } from 'typeorm';
import { ApiError } from '../../../error';
import { DriveFile } from '@/models/entities/drive-file';
import { ID } from '@/misc/cafy-id';
import uploadFromUrl from '@/services/drive/upload-from-url';
import { publishBroadcastStream } from '@/services/stream';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
emojiId: {
validator: $.type(ID)
},
},
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: 'e2785b66-dca3-4087-9cac-b93c541cc425'
}
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
}
}
}
};
export default define(meta, async (ps, me) => {
const emoji = await Emojis.findOne(ps.emojiId);
if (emoji == null) {
throw new ApiError(meta.errors.noSuchEmoji);
}
let driveFile: DriveFile;
try {
// Create file
driveFile = await uploadFromUrl(emoji.url, null, null, null, false, true);
} catch (e) {
throw new ApiError();
}
const copied = await Emojis.insert({
id: genId(),
updatedAt: new Date(),
name: emoji.name,
host: null,
aliases: [],
url: driveFile.url,
type: driveFile.type,
fileId: driveFile.id,
}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
await getConnection().queryResultCache!.remove(['meta_emojis']);
publishBroadcastStream('emojiAdded', {
emoji: await Emojis.pack(copied.id)
});
return {
id: copied.id
};
});

View File

@ -0,0 +1,99 @@
import $ from 'cafy';
import define from '../../../define';
import { Emojis } from '@/models/index';
import { toPuny } from '@/misc/convert-host';
import { makePaginationQuery } from '../../../common/make-pagination-query';
import { ID } from '@/misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
query: {
validator: $.optional.nullable.str,
default: null
},
host: {
validator: $.optional.nullable.str,
default: null
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
}
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
aliases: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
category: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
host: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
url: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
}
}
}
};
export default define(meta, 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: '%' + ps.query + '%' });
}
const emojis = await q
.orderBy('emoji.id', 'DESC')
.take(ps.limit!)
.getMany();
return Emojis.packMany(emojis);
});

View File

@ -0,0 +1,98 @@
import $ from 'cafy';
import define from '../../../define';
import { Emojis } from '@/models/index';
import { makePaginationQuery } from '../../../common/make-pagination-query';
import { ID } from '@/misc/cafy-id';
import { Emoji } from '@/models/entities/emoji';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
query: {
validator: $.optional.nullable.str,
default: null
},
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
}
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id',
},
aliases: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'string' as const,
optional: false as const, nullable: false as const
}
},
name: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
category: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
host: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
url: {
type: 'string' as const,
optional: false as const, nullable: false as const,
}
}
}
}
};
export default define(meta, async (ps) => {
const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
.andWhere(`emoji.host IS NULL`);
let emojis: Emoji[];
if (ps.query) {
//q.andWhere('emoji.name ILIKE :q', { q: `%${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);
});

View File

@ -0,0 +1,42 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection } from 'typeorm';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
}
},
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: 'be83669b-773a-44b7-b1f8-e5e5170ac3c2'
}
}
};
export default define(meta, async (ps, me) => {
const emoji = await Emojis.findOne(ps.id);
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await Emojis.delete(emoji.id);
await getConnection().queryResultCache!.remove(['meta_emojis']);
insertModerationLog(me, 'removeEmoji', {
emoji: emoji
});
});

View File

@ -0,0 +1,54 @@
import $ from 'cafy';
import define from '../../../define';
import { ID } from '@/misc/cafy-id';
import { Emojis } from '@/models/index';
import { getConnection } from 'typeorm';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
id: {
validator: $.type(ID)
},
name: {
validator: $.str
},
category: {
validator: $.optional.nullable.str
},
aliases: {
validator: $.arr($.str)
}
},
errors: {
noSuchEmoji: {
message: 'No such emoji.',
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8'
}
}
};
export default define(meta, async (ps) => {
const emoji = await Emojis.findOne(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,
});
await getConnection().queryResultCache!.remove(['meta_emojis']);
});

View File

@ -0,0 +1,27 @@
import $ from 'cafy';
import define from '../../../define';
import { deleteFile } from '@/services/drive/delete-file';
import { DriveFiles } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
host: {
validator: $.str
}
}
};
export default define(meta, async (ps, me) => {
const files = await DriveFiles.find({
userHost: ps.host
});
for (const file of files) {
deleteFile(file);
}
});

View File

@ -0,0 +1,28 @@
import $ from 'cafy';
import define from '../../../define';
import { Instances } from '@/models/index';
import { toPuny } from '@/misc/convert-host';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
host: {
validator: $.str
},
}
};
export default define(meta, async (ps, me) => {
const instance = await Instances.findOne({ host: toPuny(ps.host) });
if (instance == null) {
throw new Error('instance not found');
}
fetchInstanceMetadata(instance, true);
});

View File

@ -0,0 +1,32 @@
import $ from 'cafy';
import define from '../../../define';
import deleteFollowing from '@/services/following/delete';
import { Followings, Users } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
host: {
validator: $.str
}
}
};
export default define(meta, async (ps, me) => {
const followings = await Followings.find({
followerHost: ps.host
});
const pairs = await Promise.all(followings.map(f => Promise.all([
Users.findOneOrFail(f.followerId),
Users.findOneOrFail(f.followeeId)
])));
for (const pair of pairs) {
deleteFollowing(pair[0], pair[1]);
}
});

View File

@ -0,0 +1,33 @@
import $ from 'cafy';
import define from '../../../define';
import { Instances } from '@/models/index';
import { toPuny } from '@/misc/convert-host';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
host: {
validator: $.str
},
isSuspended: {
validator: $.bool
},
}
};
export default define(meta, async (ps, me) => {
const instance = await Instances.findOne({ host: toPuny(ps.host) });
if (instance == null) {
throw new Error('instance not found');
}
Instances.update({ host: toPuny(ps.host) }, {
isSuspended: ps.isSuspended
});
});

View File

@ -0,0 +1,26 @@
import define from '../../define';
import { getConnection } from 'typeorm';
export const meta = {
requireCredential: true as const,
requireModerator: true,
tags: ['admin'],
params: {
},
};
export default define(meta, async () => {
const stats = await
getConnection().query(`SELECT * FROM pg_indexes;`)
.then(recs => {
const res = [] as { tablename: string; indexname: string; }[];
for (const rec of recs) {
res.push(rec);
}
return res;
});
return stats;
});

View File

@ -0,0 +1,45 @@
import define from '../../define';
import { getConnection } from 'typeorm';
export const meta = {
requireCredential: true as const,
requireModerator: true,
tags: ['admin'],
params: {
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
example: {
migrations: {
count: 66,
size: 32768
},
}
}
};
export default define(meta, async () => {
const sizes = await
getConnection().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 = {} as Record<string, { count: number; size: number; }>;
for (const rec of recs) {
res[rec.table] = {
count: parseInt(rec.count, 10),
size: parseInt(rec.size, 10),
};
}
return res;
});
return sizes;
});

View File

@ -0,0 +1,44 @@
import rndstr from 'rndstr';
import define from '../../define';
import { RegistrationTickets } from '@/models/index';
import { genId } from '@/misc/gen-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
code: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: '2ERUA5VR',
maxLength: 8,
minLength: 8
}
}
}
};
export default define(meta, async () => {
const code = rndstr({
length: 8,
chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
});
await RegistrationTickets.insert({
id: genId(),
createdAt: new Date(),
code,
});
return {
code,
};
});

View File

@ -0,0 +1,33 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { Users } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireAdmin: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps) => {
const user = await Users.findOne(ps.userId as string);
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
});
});

View File

@ -0,0 +1,29 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { Users } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireAdmin: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
await Users.update(user.id, {
isModerator: false
});
});

View File

@ -0,0 +1,57 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getNote } from '../../../common/getters';
import { PromoNotes } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
noteId: {
validator: $.type(ID),
},
expiresAt: {
validator: $.num.int()
},
},
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'
},
}
};
export default define(meta, async (ps, user) => {
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const exist = await PromoNotes.findOne(note.id);
if (exist != null) {
throw new ApiError(meta.errors.alreadyPromoted);
}
await PromoNotes.insert({
noteId: note.id,
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
userId: note.userId,
});
});

View File

@ -0,0 +1,18 @@
import define from '../../../define';
import { destroy } from '@/queue/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {}
};
export default define(meta, async (ps, me) => {
destroy();
insertModerationLog(me, 'clearQueue');
});

View File

@ -0,0 +1,55 @@
import { deliverQueue } from '@/queue/queues';
import { URL } from 'url';
import define from '../../../define';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
anyOf: [
{
type: 'string' as const,
},
{
type: 'number' as const,
}
]
}
},
example: [[
'example.com',
12
]]
}
};
export default define(meta, async (ps) => {
const jobs = await deliverQueue.getJobs(['delayed']);
const res = [] as [string, number][];
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;
});

View File

@ -0,0 +1,55 @@
import { URL } from 'url';
import define from '../../../define';
import { inboxQueue } from '@/queue/queues';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
anyOf: [
{
type: 'string' as const,
},
{
type: 'number' as const,
}
]
}
},
example: [[
'example.com',
12
]]
}
};
export default define(meta, async (ps) => {
const jobs = await inboxQueue.getJobs(['delayed']);
const res = [] as [string, number][];
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;
});

View File

@ -0,0 +1,81 @@
import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
import $ from 'cafy';
import define from '../../../define';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
domain: {
validator: $.str.or(['deliver', 'inbox', 'db', 'objectStorage']),
},
state: {
validator: $.str.or(['active', 'waiting', 'delayed']),
},
limit: {
validator: $.optional.num,
default: 50
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
data: {
type: 'object' as const,
optional: false as const, nullable: false as const
},
attempts: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
maxAttempts: {
type: 'number' as const,
optional: false as const, nullable: false as const
},
timestamp: {
type: 'number' as const,
optional: false as const, nullable: false as const
}
}
}
}
};
export default define(meta, async (ps) => {
const queue =
ps.domain === 'deliver' ? deliverQueue :
ps.domain === 'inbox' ? inboxQueue :
ps.domain === 'db' ? dbQueue :
ps.domain === 'objectStorage' ? objectStorageQueue :
null as never;
const jobs = await queue.getJobs([ps.state], 0, ps.limit!);
return jobs.map(job => {
const data = job.data;
delete data.content;
delete data.user;
return {
id: job.id,
data,
attempts: job.attemptsMade,
maxAttempts: job.opts ? job.opts.attempts : 0,
timestamp: job.timestamp,
};
});
});

View File

@ -0,0 +1,44 @@
import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } from '@/queue/queues';
import define from '../../../define';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
deliver: {
ref: 'QueueCount'
},
inbox: {
ref: 'QueueCount'
},
db: {
ref: 'QueueCount'
},
objectStorage: {
ref: 'QueueCount'
}
}
}
};
export default define(meta, 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,
};
});

View File

@ -0,0 +1,63 @@
import { URL } from 'url';
import $ from 'cafy';
import define from '../../../define';
import { addRelay } from '@/services/relay';
import { ApiError } from '../../../error';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
inbox: {
validator: $.str
},
},
errors: {
invalidUrl: {
message: 'Invalid URL',
code: 'INVALID_URL',
id: 'fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c'
},
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
inbox: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'url'
},
status: {
type: 'string' as const,
optional: false as const, nullable: false as const,
default: 'requesting',
enum: [
'requesting',
'accepted',
'rejected'
]
}
}
}
};
export default define(meta, async (ps, user) => {
try {
if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
} catch {
throw new ApiError(meta.errors.invalidUrl);
}
return await addRelay(ps.inbox);
});

View File

@ -0,0 +1,47 @@
import define from '../../../define';
import { listRelay } from '@/services/relay';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
inbox: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'url'
},
status: {
type: 'string' as const,
optional: false as const, nullable: false as const,
default: 'requesting',
enum: [
'requesting',
'accepted',
'rejected'
]
}
}
}
}
};
export default define(meta, async (ps, user) => {
return await listRelay();
});

View File

@ -0,0 +1,20 @@
import $ from 'cafy';
import define from '../../../define';
import { removeRelay } from '@/services/relay';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
inbox: {
validator: $.str
},
},
};
export default define(meta, async (ps, user) => {
return await removeRelay(ps.inbox);
});

View File

@ -0,0 +1,59 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import * as bcrypt from 'bcryptjs';
import rndstr from 'rndstr';
import { Users, UserProfiles } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
password: {
type: 'string' as const,
optional: false as const, nullable: false as const,
minLength: 8,
maxLength: 8
}
}
}
};
export default define(meta, async (ps) => {
const user = await Users.findOne(ps.userId as string);
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);
await UserProfiles.update({
userId: user.id
}, {
password: hash
});
return {
password: passwd
};
});

View File

@ -0,0 +1,30 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { AbuseUserReports } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
reportId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const report = await AbuseUserReports.findOne(ps.reportId);
if (report == null) {
throw new Error('report not found');
}
await AbuseUserReports.update(report.id, {
resolved: true,
assigneeId: me.id,
});
});

View File

@ -0,0 +1,21 @@
import define from '../../define';
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
};
export default define(meta, async (ps, me) => {
insertModerationLog(me, 'chartResync');
driveChart.resync();
notesChart.resync();
usersChart.resync();
// TODO: ユーザーごとのチャートもキューに入れて更新する
// TODO: インスタンスごとのチャートもキューに入れて更新する
});

View File

@ -0,0 +1,26 @@
import $ from 'cafy';
import define from '../../define';
import { sendEmail } from '@/services/send-email';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
to: {
validator: $.str,
},
subject: {
validator: $.str,
},
text: {
validator: $.str,
},
}
};
export default define(meta, async (ps) => {
await sendEmail(ps.to, ps.subject, ps.text, ps.text);
});

View File

@ -0,0 +1,119 @@
import * as os from 'os';
import * as si from 'systeminformation';
import { getConnection } from 'typeorm';
import define from '../../define';
import { redisClient } from '../../../../db/redis';
export const meta = {
requireCredential: true as const,
requireModerator: true,
tags: ['admin', 'meta'],
params: {
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
machine: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
os: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'linux'
},
node: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
psql: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
cpu: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
model: {
type: 'string' as const,
optional: false as const, nullable: false as const,
},
cores: {
type: 'number' as const,
optional: false as const, nullable: false as const,
}
}
},
mem: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
total: {
type: 'number' as const,
optional: false as const, nullable: false as const,
format: 'bytes',
}
}
},
fs: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
total: {
type: 'number' as const,
optional: false as const, nullable: false as const,
format: 'bytes',
},
used: {
type: 'number' as const,
optional: false as const, nullable: false as const,
format: 'bytes',
}
}
},
net: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
interface: {
type: 'string' as const,
optional: false as const, nullable: false as const,
example: 'eth0'
}
}
}
}
}
};
export default define(meta, async () => {
const memStats = await si.mem();
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
return {
machine: os.hostname(),
os: os.platform(),
node: process.version,
psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
redis: redisClient.server_info.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
}
};
});

View File

@ -0,0 +1,74 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { ModerationLogs } from '@/models/index';
import { makePaginationQuery } from '../../common/make-pagination-query';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
sinceId: {
validator: $.optional.type(ID),
},
untilId: {
validator: $.optional.type(ID),
},
},
res: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
type: 'object' as const,
optional: false as const, nullable: false as const,
properties: {
id: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
createdAt: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'date-time'
},
type: {
type: 'string' as const,
optional: false as const, nullable: false as const
},
info: {
type: 'object' as const,
optional: false as const, nullable: false as const
},
userId: {
type: 'string' as const,
optional: false as const, nullable: false as const,
format: 'id'
},
user: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User'
}
}
}
}
};
export default define(meta, 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);
});

View File

@ -0,0 +1,177 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { Users } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
},
res: {
type: 'object' as const,
nullable: false as const, optional: false as const,
properties: {
id: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'id'
},
createdAt: {
type: 'string' as const,
nullable: false as const, optional: false as const,
format: 'date-time'
},
updatedAt: {
type: 'string' as const,
nullable: true as const, optional: false as const,
format: 'date-time'
},
lastFetchedAt: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
username: {
type: 'string' as const,
nullable: false as const, optional: false as const
},
name: {
type: 'string' as const,
nullable: false as const, optional: false as const
},
folowersCount: {
type: 'number' as const,
nullable: false as const, optional: false as const
},
followingCount: {
type: 'number' as const,
nullable: false as const, optional: false as const
},
notesCount: {
type: 'number' as const,
nullable: false as const, optional: false as const
},
avatarId: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
bannerId: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
tags: {
type: 'array' as const,
nullable: false as const, optional: false as const,
items: {
type: 'string' as const,
nullable: false as const, optional: false as const
}
},
avatarUrl: {
type: 'string' as const,
nullable: true as const, optional: false as const,
format: 'url'
},
bannerUrl: {
type: 'string' as const,
nullable: true as const, optional: false as const,
format: 'url'
},
avatarBlurhash: {
type: 'any' as const,
nullable: true as const, optional: false as const,
default: null
},
bannerBlurhash: {
type: 'any' as const,
nullable: true as const, optional: false as const,
default: null
},
isSuspended: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
isSilenced: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
isLocked: {
type: 'boolean' as const,
nullable: false as const, optional: false as const,
},
isBot: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
isCat: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
isAdmin: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
isModerator: {
type: 'boolean' as const,
nullable: false as const, optional: false as const
},
emojis: {
type: 'array' as const,
nullable: false as const, optional: false as const,
items: {
type: 'string' as const,
nullable: false as const, optional: false as const
}
},
host: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
inbox: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
sharedInbox: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
featured: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
uri: {
type: 'string' as const,
nullable: true as const, optional: false as const
},
token: {
type: 'string' as const,
nullable: false as const, optional: false as const,
default: '<MASKED>'
}
}
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
if ((me.isModerator && !me.isAdmin) && user.isAdmin) {
throw new Error('cannot show info of admin');
}
return {
...user,
token: user.token != null ? '<MASKED>' : user.token,
};
});

View File

@ -0,0 +1,119 @@
import $ from 'cafy';
import define from '../../define';
import { Users } from '@/models/index';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
limit: {
validator: $.optional.num.range(1, 100),
default: 10
},
offset: {
validator: $.optional.num.min(0),
default: 0
},
sort: {
validator: $.optional.str.or([
'+follower',
'-follower',
'+createdAt',
'-createdAt',
'+updatedAt',
'-updatedAt',
]),
},
state: {
validator: $.optional.str.or([
'all',
'available',
'admin',
'moderator',
'adminOrModerator',
'silenced',
'suspended',
]),
default: 'all'
},
origin: {
validator: $.optional.str.or([
'combined',
'local',
'remote',
]),
default: 'local'
},
username: {
validator: $.optional.str,
default: null
},
hostname: {
validator: $.optional.str,
default: null
}
},
res: {
type: 'array' as const,
nullable: false as const, optional: false as const,
items: {
type: 'object' as const,
nullable: false as const, optional: false as const,
ref: 'User'
}
}
};
export default define(meta, 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;
}
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: ps.username.toLowerCase() + '%' });
}
if (ps.hostname) {
query.andWhere('user.host like :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 });
});

View File

@ -0,0 +1,38 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { Users } from '@/models/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
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
});
insertModerationLog(me, 'silence', {
targetId: user.id,
});
});

View File

@ -0,0 +1,84 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import deleteFollowing from '@/services/following/delete';
import { Users, Followings, Notifications } from '@/models/index';
import { User } from '@/models/entities/user';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { doPostSuspend } from '@/services/suspend-user';
import { publishUserEvent } from '@/services/stream';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
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: User) {
const followings = await Followings.find({
followerId: follower.id
});
for (const following of followings) {
const followee = await Users.findOne({
id: following.followeeId
});
if (followee == null) {
throw `Cant find followee ${following.followeeId}`;
}
await deleteFollowing(follower, followee, true);
}
}
async function readAllNotify(notifier: User) {
await Notifications.update({
notifierId: notifier.id,
isRead: false,
}, {
isRead: true
});
}

View File

@ -0,0 +1,34 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { Users } from '@/models/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
await Users.update(user.id, {
isSilenced: false
});
insertModerationLog(me, 'unsilence', {
targetId: user.id,
});
});

View File

@ -0,0 +1,37 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import define from '../../define';
import { Users } from '@/models/index';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { doPostUnsuspend } from '@/services/unsuspend-user';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
throw new Error('user not found');
}
await Users.update(user.id, {
isSuspended: false
});
insertModerationLog(me, 'unsuspend', {
targetId: user.id,
});
doPostUnsuspend(user);
});

View File

@ -0,0 +1,608 @@
import $ from 'cafy';
import define from '../../define';
import { getConnection } from 'typeorm';
import { Meta } from '@/models/entities/meta';
import { insertModerationLog } from '@/services/insert-moderation-log';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
import { ID } from '@/misc/cafy-id';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireAdmin: true,
params: {
disableRegistration: {
validator: $.optional.nullable.bool,
},
disableLocalTimeline: {
validator: $.optional.nullable.bool,
},
disableGlobalTimeline: {
validator: $.optional.nullable.bool,
},
useStarForReactionFallback: {
validator: $.optional.nullable.bool,
},
pinnedUsers: {
validator: $.optional.nullable.arr($.str),
},
hiddenTags: {
validator: $.optional.nullable.arr($.str),
},
blockedHosts: {
validator: $.optional.nullable.arr($.str),
},
mascotImageUrl: {
validator: $.optional.nullable.str,
},
bannerUrl: {
validator: $.optional.nullable.str,
},
errorImageUrl: {
validator: $.optional.nullable.str,
},
iconUrl: {
validator: $.optional.nullable.str,
},
backgroundImageUrl: {
validator: $.optional.nullable.str,
},
logoImageUrl: {
validator: $.optional.nullable.str,
},
name: {
validator: $.optional.nullable.str,
},
description: {
validator: $.optional.nullable.str,
},
maxNoteTextLength: {
validator: $.optional.num.min(0).max(DB_MAX_NOTE_TEXT_LENGTH),
},
localDriveCapacityMb: {
validator: $.optional.num.min(0),
},
remoteDriveCapacityMb: {
validator: $.optional.num.min(0),
},
cacheRemoteFiles: {
validator: $.optional.bool,
},
proxyRemoteFiles: {
validator: $.optional.bool,
},
emailRequiredForSignup: {
validator: $.optional.bool,
},
enableHcaptcha: {
validator: $.optional.bool,
},
hcaptchaSiteKey: {
validator: $.optional.nullable.str,
},
hcaptchaSecretKey: {
validator: $.optional.nullable.str,
},
enableRecaptcha: {
validator: $.optional.bool,
},
recaptchaSiteKey: {
validator: $.optional.nullable.str,
},
recaptchaSecretKey: {
validator: $.optional.nullable.str,
},
proxyAccountId: {
validator: $.optional.nullable.type(ID),
},
maintainerName: {
validator: $.optional.nullable.str,
},
maintainerEmail: {
validator: $.optional.nullable.str,
},
pinnedPages: {
validator: $.optional.arr($.str),
},
pinnedClipId: {
validator: $.optional.nullable.type(ID),
},
langs: {
validator: $.optional.arr($.str),
},
summalyProxy: {
validator: $.optional.nullable.str,
},
deeplAuthKey: {
validator: $.optional.nullable.str,
},
deeplIsPro: {
validator: $.optional.bool,
},
enableTwitterIntegration: {
validator: $.optional.bool,
},
twitterConsumerKey: {
validator: $.optional.nullable.str,
},
twitterConsumerSecret: {
validator: $.optional.nullable.str,
},
enableGithubIntegration: {
validator: $.optional.bool,
},
githubClientId: {
validator: $.optional.nullable.str,
},
githubClientSecret: {
validator: $.optional.nullable.str,
},
enableDiscordIntegration: {
validator: $.optional.bool,
},
discordClientId: {
validator: $.optional.nullable.str,
},
discordClientSecret: {
validator: $.optional.nullable.str,
},
enableEmail: {
validator: $.optional.bool,
},
email: {
validator: $.optional.nullable.str,
},
smtpSecure: {
validator: $.optional.bool,
},
smtpHost: {
validator: $.optional.nullable.str,
},
smtpPort: {
validator: $.optional.nullable.num,
},
smtpUser: {
validator: $.optional.nullable.str,
},
smtpPass: {
validator: $.optional.nullable.str,
},
enableServiceWorker: {
validator: $.optional.bool,
},
swPublicKey: {
validator: $.optional.nullable.str,
},
swPrivateKey: {
validator: $.optional.nullable.str,
},
tosUrl: {
validator: $.optional.nullable.str,
},
repositoryUrl: {
validator: $.optional.str,
},
feedbackUrl: {
validator: $.optional.str,
},
useObjectStorage: {
validator: $.optional.bool
},
objectStorageBaseUrl: {
validator: $.optional.nullable.str
},
objectStorageBucket: {
validator: $.optional.nullable.str
},
objectStoragePrefix: {
validator: $.optional.nullable.str
},
objectStorageEndpoint: {
validator: $.optional.nullable.str
},
objectStorageRegion: {
validator: $.optional.nullable.str
},
objectStoragePort: {
validator: $.optional.nullable.num
},
objectStorageAccessKey: {
validator: $.optional.nullable.str
},
objectStorageSecretKey: {
validator: $.optional.nullable.str
},
objectStorageUseSSL: {
validator: $.optional.bool
},
objectStorageUseProxy: {
validator: $.optional.bool
},
objectStorageSetPublicRead: {
validator: $.optional.bool
},
objectStorageS3ForcePathStyle: {
validator: $.optional.bool
},
}
};
export default define(meta, async (ps, me) => {
const set = {} as Partial<Meta>;
if (typeof ps.disableRegistration === 'boolean') {
set.disableRegistration = ps.disableRegistration;
}
if (typeof ps.disableLocalTimeline === 'boolean') {
set.disableLocalTimeline = ps.disableLocalTimeline;
}
if (typeof ps.disableGlobalTimeline === 'boolean') {
set.disableGlobalTimeline = ps.disableGlobalTimeline;
}
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
if (Array.isArray(ps.pinnedUsers)) {
set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
}
if (Array.isArray(ps.hiddenTags)) {
set.hiddenTags = ps.hiddenTags.filter(Boolean);
}
if (Array.isArray(ps.blockedHosts)) {
set.blockedHosts = ps.blockedHosts.filter(Boolean);
}
if (ps.mascotImageUrl !== undefined) {
set.mascotImageUrl = ps.mascotImageUrl;
}
if (ps.bannerUrl !== undefined) {
set.bannerUrl = ps.bannerUrl;
}
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.maxNoteTextLength) {
set.maxNoteTextLength = ps.maxNoteTextLength;
}
if (ps.localDriveCapacityMb !== undefined) {
set.localDriveCapacityMb = ps.localDriveCapacityMb;
}
if (ps.remoteDriveCapacityMb !== undefined) {
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
}
if (ps.cacheRemoteFiles !== undefined) {
set.cacheRemoteFiles = ps.cacheRemoteFiles;
}
if (ps.proxyRemoteFiles !== undefined) {
set.proxyRemoteFiles = ps.proxyRemoteFiles;
}
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.proxyAccountId !== undefined) {
set.proxyAccountId = ps.proxyAccountId;
}
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.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration;
}
if (ps.twitterConsumerKey !== undefined) {
set.twitterConsumerKey = ps.twitterConsumerKey;
}
if (ps.twitterConsumerSecret !== undefined) {
set.twitterConsumerSecret = ps.twitterConsumerSecret;
}
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.enableServiceWorker !== undefined) {
set.enableServiceWorker = ps.enableServiceWorker;
}
if (ps.swPublicKey !== undefined) {
set.swPublicKey = ps.swPublicKey;
}
if (ps.swPrivateKey !== undefined) {
set.swPrivateKey = ps.swPrivateKey;
}
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;
}
await getConnection().transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, {
order: {
id: 'DESC'
}
});
if (meta) {
await transactionalEntityManager.update(Meta, meta.id, set);
} else {
await transactionalEntityManager.save(Meta, set);
}
});
insertModerationLog(me, 'updateMeta');
});

View File

@ -0,0 +1,36 @@
import $ from 'cafy';
import define from '../../define';
import { getConnection } from 'typeorm';
import { insertModerationLog } from '@/services/insert-moderation-log';
export const meta = {
tags: ['admin'],
requireCredential: true as const,
requireModerator: true,
params: {
full: {
validator: $.bool,
},
analyze: {
validator: $.bool,
},
}
};
export default define(meta, async (ps, me) => {
const params: string[] = [];
if (ps.full) {
params.push('FULL');
}
if (ps.analyze) {
params.push('ANALYZE');
}
getConnection().query('VACUUM ' + params.join(' '));
insertModerationLog(me, 'vacuum', ps);
});