Merge branch 'develop' into feat-12997

This commit is contained in:
kakkokari-gtyih 2024-08-03 20:25:23 +09:00
commit 1581c21d00
685 changed files with 27095 additions and 10179 deletions

View file

@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { DI } from '@/di-symbols.js';
import type { UserProfilesRepository } from '@/models/_.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
correlationCheckEmail: {
message: 'If "method" is email, "userId" must be set.',
code: 'CORRELATION_CHECK_EMAIL',
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
httpStatusCode: 400,
},
correlationCheckWebhook: {
message: 'If "method" is webhook, "systemWebhookId" must be set.',
code: 'CORRELATION_CHECK_WEBHOOK',
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
httpStatusCode: 400,
},
emailAddressNotSet: {
message: 'Email address is not set.',
code: 'EMAIL_ADDRESS_NOT_SET',
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
httpStatusCode: 400,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
method: {
type: 'string',
enum: ['email', 'webhook'],
},
userId: {
type: 'string',
format: 'misskey:id',
},
systemWebhookId: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'isActive',
'name',
'method',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.method === 'email') {
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
if (!ps.userId || !userProfile) {
throw new ApiError(meta.errors.correlationCheckEmail);
}
if (!userProfile.email || !userProfile.emailVerified) {
throw new ApiError(meta.errors.emailAddressNotSet);
}
}
if (ps.method === 'webhook' && !ps.systemWebhookId) {
throw new ApiError(meta.errors.correlationCheckWebhook);
}
const userId = ps.method === 'email' ? ps.userId : null;
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
const result = await this.abuseReportNotificationService.createRecipient(
{
isActive: ps.isActive,
name: ps.name,
method: ps.method,
userId: userId ?? null,
systemWebhookId: systemWebhookId ?? null,
},
me,
);
return this.abuseReportNotificationRecipientEntityService.pack(result);
});
}
}

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
await this.abuseReportNotificationService.deleteRecipient(
ps.id,
me,
);
});
}
}

View file

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:abuse-report:notification-recipient',
res: {
type: 'array',
items: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
method: {
type: 'array',
items: {
type: 'string',
enum: ['email', 'webhook'],
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps) => {
const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method });
return this.abuseReportNotificationRecipientEntityService.packMany(recipients);
});
}
}

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'read:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
noSuchRecipient: {
message: 'No such recipient.',
code: 'NO_SUCH_RECIPIENT',
id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps) => {
const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] });
if (recipients.length === 0) {
throw new ApiError(meta.errors.noSuchRecipient);
}
return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]);
});
}
}

View file

@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import {
AbuseReportNotificationRecipientEntityService,
} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { DI } from '@/di-symbols.js';
import type { UserProfilesRepository } from '@/models/_.js';
export const meta = {
tags: ['admin', 'abuse-report', 'notification-recipient'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:abuse-report:notification-recipient',
res: {
type: 'object',
ref: 'AbuseReportNotificationRecipient',
},
errors: {
correlationCheckEmail: {
message: 'If "method" is email, "userId" must be set.',
code: 'CORRELATION_CHECK_EMAIL',
id: '348bb8ae-575a-6fe9-4327-5811999def8f',
httpStatusCode: 400,
},
correlationCheckWebhook: {
message: 'If "method" is webhook, "systemWebhookId" must be set.',
code: 'CORRELATION_CHECK_WEBHOOK',
id: 'b0c15051-de2d-29ef-260c-9585cddd701a',
httpStatusCode: 400,
},
emailAddressNotSet: {
message: 'Email address is not set.',
code: 'EMAIL_ADDRESS_NOT_SET',
id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f',
httpStatusCode: 400,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
method: {
type: 'string',
enum: ['email', 'webhook'],
},
userId: {
type: 'string',
format: 'misskey:id',
},
systemWebhookId: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
'isActive',
'name',
'method',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private abuseReportNotificationService: AbuseReportNotificationService,
private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.method === 'email') {
const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId });
if (!ps.userId || !userProfile) {
throw new ApiError(meta.errors.correlationCheckEmail);
}
if (!userProfile.email || !userProfile.emailVerified) {
throw new ApiError(meta.errors.emailAddressNotSet);
}
}
if (ps.method === 'webhook' && !ps.systemWebhookId) {
throw new ApiError(meta.errors.correlationCheckWebhook);
}
const userId = ps.method === 'email' ? ps.userId : null;
const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null;
const result = await this.abuseReportNotificationService.updateRecipient(
{
id: ps.id,
isActive: ps.isActive,
name: ps.name,
method: ps.method,
userId: userId ?? null,
systemWebhookId: systemWebhookId ?? null,
},
me,
);
return this.abuseReportNotificationRecipientEntityService.pack(result);
});
}
}

View file

@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const ad = await this.adsRepository.insert({
const ad = await this.adsRepository.insertOne({
id: this.idService.gen(),
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
});
this.moderationLogService.log(me, 'createAd', {
adId: ad.id,

View file

@ -40,7 +40,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
},
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'],
required: ['id'],
} as const;
@Injectable()
@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
memo: ps.memo,
imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
});

View file

@ -69,6 +69,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
},
required: [],
} as const;
@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
query.andWhere('announcement.isActive = true');
if (ps.status === 'archived') {
query.andWhere('announcement.isActive = false');
} else if (ps.status === 'active') {
query.andWhere('announcement.isActive = true');
}
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {

View file

@ -61,7 +61,7 @@ export const meta = {
name: {
type: 'string',
optional: false, nullable: false,
example: 'lenna.jpg',
example: '192.jpg',
},
type: {
type: 'string',

View file

@ -46,12 +46,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
const isSuspendedBefore = instance.suspensionState !== 'none';
let suspensionState: undefined | 'manuallySuspended' | 'none';
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
}
await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended,
suspensionState,
moderationNote: ps.moderationNote,
});
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,

View file

@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const ticketsPromises = [];
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insert({
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
}));
}
const tickets = await Promise.all(ticketsPromises);

View file

@ -132,6 +132,16 @@ export const meta = {
nullable: false,
},
},
mediaSilencedHosts: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
@ -431,6 +441,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
inquiryUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
@ -517,6 +531,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
canSkipInitialTutorial: instance.canSkipInitialTutorial,
@ -552,6 +567,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
super(meta, paramDef, async (ps, me) => {
const deliverJobCounts = await this.deliverQueue.getJobCounts();

View file

@ -5,12 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { QueueService } from '@/core/QueueService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import type { AbuseUserReportsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '@/server/api/error.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
export const meta = {
tags: ['admin'],
@ -18,6 +16,16 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
errors: {
noSuchAbuseReport: {
message: 'No such abuse report.',
code: 'NO_SUCH_ABUSE_REPORT',
id: 'ac3794dd-2ce4-d878-e546-73c60c06b398',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
@ -29,47 +37,20 @@ export const paramDef = {
required: ['reportId'],
} as const;
// TODO: ロジックをサービスに切り出す
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private queueService: QueueService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
private moderationLogService: ModerationLogService,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
if (report == null) {
throw new Error('report not found');
if (!report) {
throw new ApiError(meta.errors.noSuchAbuseReport);
}
if (ps.forward && report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
}
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: me.id,
forwarded: ps.forward && report.targetUserHost != null,
});
this.moderationLogService.log(me, 'resolveAbuseReport', {
reportId: report.id,
report: report,
forwarded: ps.forward && report.targetUserHost != null,
});
await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
});
}
}

View file

@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RolesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { RoleService } from '@/core/RoleService.js';
@ -50,19 +49,6 @@ export const paramDef = {
},
required: [
'roleId',
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'displayOrder',
'policies',
],
} as const;

View file

@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});

View file

@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:show-users',
kind: 'read:admin:show-user',
res: {
type: 'array',

View file

@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
url: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
},
required: [
'isActive',
'name',
'on',
'url',
'secret',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.systemWebhookService.createSystemWebhook(
{
isActive: ps.isActive,
name: ps.name,
on: ps.on,
url: ps.url,
secret: ps.secret,
},
me,
);
return this.systemWebhookEntityService.pack(result);
});
}
}

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: [
'id',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
) {
super(meta, paramDef, async (ps, me) => {
await this.systemWebhookService.deleteSystemWebhook(
ps.id,
me,
);
});
}
}

View file

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'array',
items: {
type: 'object',
ref: 'SystemWebhook',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
isActive: {
type: 'boolean',
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps) => {
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({
isActive: ps.isActive,
on: ps.on,
});
return this.systemWebhookEntityService.packMany(webhooks);
});
}
}

View file

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
errors: {
noSuchSystemWebhook: {
message: 'No such SystemWebhook.',
code: 'NO_SUCH_SYSTEM_WEBHOOK',
id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d',
kind: 'server',
httpStatusCode: 404,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps) => {
const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] });
if (webhooks.length === 0) {
throw new ApiError(meta.errors.noSuchSystemWebhook);
}
return this.systemWebhookEntityService.pack(webhooks[0]);
});
}
}

View file

@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js';
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
export const meta = {
tags: ['admin', 'system-webhook'],
requireCredential: true,
requireModerator: true,
secure: true,
kind: 'write:admin:system-webhook',
res: {
type: 'object',
ref: 'SystemWebhook',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
isActive: {
type: 'boolean',
},
name: {
type: 'string',
minLength: 1,
maxLength: 255,
},
on: {
type: 'array',
items: {
type: 'string',
enum: systemWebhookEventTypes,
},
},
url: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
},
},
required: [
'id',
'isActive',
'name',
'on',
'url',
'secret',
],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private systemWebhookService: SystemWebhookService,
private systemWebhookEntityService: SystemWebhookEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.systemWebhookService.updateSystemWebhook(
{
id: ps.id,
isActive: ps.isActive,
name: ps.name,
on: ps.on,
url: ps.url,
secret: ps.secret,
},
me,
);
return this.systemWebhookEntityService.pack(result);
});
}
}

View file

@ -108,6 +108,7 @@ export const paramDef = {
feedbackUrl: { type: 'string', nullable: true },
impressumUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
inquiryUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true },
@ -150,6 +151,13 @@ export const paramDef = {
type: 'string',
},
},
mediaSilencedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
summalyProxy: {
type: 'string', nullable: true,
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@ -203,6 +211,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (Array.isArray(ps.mediaSilencedHosts)) {
let lastValue = '';
set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}
@ -427,6 +443,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
if (ps.inquiryUrl !== undefined) {
set.inquiryUrl = ps.inquiryUrl;
}
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}

View file

@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { DI } from '@/di-symbols.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
import type { AnnouncementsRepository } from '@/models/_.js';
export const meta = {
tags: ['meta'],
@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
private queryService: QueryService,
private announcementService: AnnouncementService,
private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const announcements = await query.limit(ps.limit).getMany();
return this.announcementService.packMany(announcements, me);
return this.announcementEntityService.packMany(announcements, me);
});
}
}

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { EntityNotFoundError } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['meta'],
requireCredential: false,
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Announcement',
},
errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
announcementId: { type: 'string', format: 'misskey:id' },
},
required: ['announcementId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
try {
return await this.announcementService.getAnnouncement(ps.announcementId, me);
} catch (err) {
if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
throw err;
}
});
}
}

View file

@ -67,9 +67,8 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@Injectable()
@ -94,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
@ -113,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const now = new Date();
const antenna = await this.antennasRepository.insert({
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
@ -128,8 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
notify: ps.notify,
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);

View file

@ -66,7 +66,6 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
notify: { type: 'boolean' },
},
required: ['antennaId'],
} as const;
@ -124,7 +123,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
notify: ps.notify,
isActive: true,
lastUsedAt: new Date(),
});

View file

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
// Create account
const app = await this.appsRepository.insert({
const app = await this.appsRepository.insertOne({
id: this.idService.gen(),
userId: me ? me.id : null,
name: ps.name,
@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission,
callbackUrl: ps.callbackUrl,
secret: secret,
}).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.appEntityService.pack(app, null, {
detail: true,

View file

@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const token = randomUUID();
// Create session token document
const doc = await this.authSessionsRepository.insert({
const doc = await this.authSessionsRepository.insertOne({
id: this.idService.gen(),
appId: app.id,
token: token,
}).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0]));
});
return {
token: doc.token,

View file

@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const channel = await this.channelsRepository.insert({
const channel = await this.channelsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}),
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
} as MiChannel);
return await this.channelEntityService.pack(channel, me);
});

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ClipService } from '@/core/ClipService.js';
@ -41,7 +41,7 @@ export const paramDef = {
isPublic: { type: 'boolean' },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
},
required: ['clipId', 'name'],
required: ['clipId'],
} as const;
@Injectable()

View file

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(),
});
return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
return await this.driveFileEntityService.packMany(files, { self: true });
});
}
}

View file

@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create folder
const folder = await this.driveFoldersRepository.insert({
const folder = await this.driveFoldersRepository.insertOne({
id: this.idService.gen(),
name: ps.name,
parentId: parent !== null ? parent.id : null,
userId: me.id,
}).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0]));
});
const folderObj = await this.driveFolderEntityService.pack(folder);

View file

@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check if the circular reference will occur
const checkCircle = async (folderId: string): Promise<boolean> => {
// Fetch folder
const folder2 = await this.driveFoldersRepository.findOneBy({
const folder2 = await this.driveFoldersRepository.findOneByOrFail({
id: folderId,
});
if (folder2!.id === folder!.id) {
if (folder2.id === folder.id) {
return true;
} else if (folder2!.parentId) {
return await checkCircle(folder2!.parentId);
} else if (folder2.parentId) {
return await checkCircle(folder2.parentId);
} else {
return false;
}

View file

@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.suspended === 'boolean') {
if (ps.suspended) {
query.andWhere('instance.isSuspended = TRUE');
query.andWhere('instance.suspensionState != \'none\'');
} else {
query.andWhere('instance.isSuspended = FALSE');
query.andWhere('instance.suspensionState = \'none\'');
}
}

View file

@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.insert({
const flash = await this.flashsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
updatedAt: new Date(),
@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
script: ps.script,
permissions: ps.permissions,
visibility: ps.visibility,
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.flashEntityService.pack(flash);
});

View file

@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
return await this.followRequestEntityService.packMany(requests, me);
});
}
}

View file

@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['gallery'],
@ -70,13 +69,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId,
userId: me.id,
}),
))).filter(isNotNull);
))).filter(x => x != null);
if (files.length === 0) {
throw new Error();
}
const post = await this.galleryPostsRepository.insert(new MiGalleryPost({
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@ -84,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id,
isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id),
})).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.galleryPostEntityService.pack(post, me);
});

View file

@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
import type { MiDriveFile } from '@/models/DriveFile.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['gallery'],
@ -48,7 +47,7 @@ export const paramDef = {
} },
isSensitive: { type: 'boolean', default: false },
},
required: ['postId', 'title', 'fileIds'],
required: ['postId'],
} as const;
@Injectable()
@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private galleryPostEntityService: GalleryPostEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const files = (await Promise.all(ps.fileIds.map(fileId =>
this.driveFilesRepository.findOneBy({
id: fileId,
userId: me.id,
}),
))).filter(isNotNull);
let files: Array<MiDriveFile> | undefined;
if (files.length === 0) {
throw new Error();
if (ps.fileIds) {
files = (await Promise.all(ps.fileIds.map(fileId =>
this.driveFilesRepository.findOneBy({
id: fileId,
userId: me.id,
}),
))).filter(x => x != null);
if (files.length === 0) {
throw new Error();
}
}
await this.galleryPostsRepository.update({
@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
title: ps.title,
description: ps.description,
isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id),
fileIds: files ? files.map(file => file.id) : undefined,
});
const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId });

View file

@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
this.queueService.createImportAntennasJob(me, antennas);

View file

@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
@ -256,8 +256,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profileUpdates = {} as Partial<MiUserProfile>;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
let policies: RolePolicies | null = null;
if (ps.name !== undefined) updates.name = ps.name;
if (ps.name !== undefined) {
if (ps.name === null) {
updates.name = null;
} else {
const trimmedName = ps.name.trim();
updates.name = trimmedName === '' ? null : trimmedName;
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
@ -289,14 +297,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.mutedWords !== undefined) {
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.hardMutedWords !== undefined) {
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
policies ??= await this.roleService.getUserPolicies(user.id);
checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
@ -315,13 +325,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') {
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
policies ??= await this.roleService.getUserPolicies(user.id);
if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
}
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
if (ps.avatarId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@ -337,6 +351,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.bannerId) {
policies ??= await this.roleService.getUserPolicies(user.id);
if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole);
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@ -352,14 +369,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
const myRoles = await this.roleService.getUserRoles(user.id);
const allRoles = await this.roleService.getRoles();
const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.map(d => d.id);
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
id: d.id,
@ -498,26 +516,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
const html = await this.httpRequestService.getHtml(url);
try {
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
const doc = window.document;
const { window } = new JSDOM(html);
const doc = window.document;
const myLink = `${this.config.url}/@${user.username}`;
const myLink = `${this.config.url}/@${user.username}`;
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
const aEls = Array.from(doc.getElementsByTagName('a'));
const linkEls = Array.from(doc.getElementsByTagName('link'));
const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
const includesMyLink = aEls.some(a => a.href === myLink);
const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
if (includesMyLink || includesRelMeLinks) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
}
window.close();
} catch (err) {
// なにもしない
}
}
}

View file

@ -85,18 +85,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id,
});
if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks);
}
const webhook = await this.webhooksRepository.insert({
const webhook = await this.webhooksRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
}).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('webhookCreated', webhook);

View file

@ -34,13 +34,13 @@ export const paramDef = {
webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', maxLength: 1024, default: '' },
secret: { type: 'string', nullable: true, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
} },
active: { type: 'boolean' },
},
required: ['webhookId', 'name', 'url', 'on', 'active'],
required: ['webhookId'],
} as const;
// TODO: ロジックをサービスに切り出す
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.webhooksRepository.update(webhook.id, {
name: ps.name,
url: ps.url,
secret: ps.secret,
secret: ps.secret === null ? '' : ps.secret,
on: ps.on,
active: ps.active,
});

View file

@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const ticket = await this.registrationTicketsRepository.insert({
const ticket = await this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.inviteCodeEntityService.pack(ticket, me);
});

View file

@ -139,9 +139,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timelineConfig = [
`homeTimeline:${me.id}`,
'localTimeline',
`localTimelineWithReplyTo:${me.id}`,
];
}
const [
followings,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
]);
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@ -152,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;
},
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
untilId,
sinceId,

View file

@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create vote
const vote = await this.pollVotesRepository.insert({
const vote = await this.pollVotesRepository.insertOne({
id: this.idService.gen(createdAt.getTime()),
noteId: note.id,
userId: me.id,
choice: ps.choice,
}).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0]));
});
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based

View file

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany();
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
return await this.noteReactionEntityService.packMany(reactions, me);
});
}
}

View file

@ -36,6 +36,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
},
cannotReactToRenote: {
message: 'You cannot react to Renote.',
code: 'CANNOT_REACT_TO_RENOTE',
id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
},
},
} as const;
@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.reactionService.create(me, note, ps.reaction).catch(err => {
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
throw err;
});
return;

View file

@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId)) return false;
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;

View file

@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
});
const page = await this.pagesRepository.insert(new MiPage({
const page = await this.pagesRepository.insertOne(new MiPage({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font,
})).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.pageEntityService.pack(page);
});

View file

@ -70,7 +70,7 @@ export const paramDef = {
alignCenter: { type: 'boolean' },
hideTitleWhenPinned: { type: 'boolean' },
},
required: ['pageId', 'title', 'name', 'content', 'variables', 'script'],
required: ['pageId'],
} as const;
@Injectable()
@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.accessDenied);
}
let eyeCatchingImage = null;
if (ps.eyeCatchingImageId != null) {
eyeCatchingImage = await this.driveFilesRepository.findOneBy({
const eyeCatchingImage = await this.driveFilesRepository.findOneBy({
id: ps.eyeCatchingImageId,
userId: me.id,
});
@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
title: ps.title,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: ps.name === undefined ? page.name : ps.name,
name: ps.name,
summary: ps.summary === undefined ? page.summary : ps.summary,
content: ps.content,
variables: ps.variables,
script: ps.script,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
font: ps.font === undefined ? page.font : ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId === null
? null
: ps.eyeCatchingImageId === undefined
? page.eyeCatchingImageId
: eyeCatchingImage!.id,
alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId,
});
});
}

View file

@ -12,7 +12,6 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = {
tags: ['users'],
@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(),
})));
return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' });
return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' });
});
}
}

View file

@ -6,12 +6,11 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { RenoteMutingsRepository } from '@/models/_.js';
import type { MiRenoteMuting } from '@/models/RenoteMuting.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = {
tags: ['account'],
@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService,
private idService: IdService,
private userRenoteMutingService: UserRenoteMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already muting
const exist = await this.renoteMutingsRepository.findOneBy({
muterId: muter.id,
muteeId: mutee.id,
const exist = await this.renoteMutingsRepository.exists({
where: {
muterId: muter.id,
muteeId: mutee.id,
},
});
if (exist != null) {
if (exist === true) {
throw new ApiError(meta.errors.alreadyMuting);
}
// Create mute
await this.renoteMutingsRepository.insert({
id: this.idService.gen(),
muterId: muter.id,
muteeId: mutee.id,
} as MiRenoteMuting);
await this.userRenoteMutingService.mute(muter, mutee);
});
}
}

View file

@ -5,10 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RenoteMutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js";
import type { RenoteMutingsRepository } from '@/models/_.js';
export const meta = {
tags: ['account'],
@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private renoteMutingsRepository: RenoteMutingsRepository,
private getterService: GetterService,
private userRenoteMutingService: UserRenoteMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Delete mute
await this.renoteMutingsRepository.delete({
id: exist.id,
});
await this.userRenoteMutingService.unmute([exist]);
});
}
}

View file

@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}

View file

@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
weight: repliedUsers[user] / peak,
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
weight: repliedUsers[userId] / peak,
})));
return repliesObj;

View file

@ -100,15 +100,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
const users = (await this.userListMembershipsRepository.findBy({
userListId: ps.listId,

View file

@ -61,15 +61,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const currentCount = await this.userListsRepository.countBy({
userId: me.id,
});
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
return await this.userListEntityService.pack(userList);
});

View file

@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
if (!iAmModerator) {
const user = await this.cacheService.findUserById(ps.userId);
@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
throw new ApiError(meta.errors.reactionsNotPublic);
}
// early return if me is blocked by requesting user
if (userIdsWhoBlockingMe.has(ps.userId)) {
return [];
}
}
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('reaction.userId = :userId', { userId: ps.userId })
@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
const reactions = await query
const reactions = (await query
.limit(ps.limit)
.getMany();
.getMany()).filter(reaction => {
if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
return true;
});
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
});

View file

@ -3,17 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import sanitizeHtml from 'sanitize-html';
import { Inject, Injectable } from '@nestjs/common';
import type { AbuseUserReportsRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { EmailService } from '@/core/EmailService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -57,60 +51,32 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private idService: IdService,
private metaService: MetaService,
private emailService: EmailService,
private getterService: GetterService,
private roleService: RoleService,
private globalEventService: GlobalEventService,
private abuseReportService: AbuseReportService,
) {
super(meta, paramDef, async (ps, me) => {
// Lookup user
const user = await this.getterService.getUser(ps.userId).catch(err => {
const targetUser = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
if (user.id === me.id) {
if (targetUser.id === me.id) {
throw new ApiError(meta.errors.cannotReportYourself);
}
if (await this.roleService.isAdministrator(user)) {
if (await this.roleService.isAdministrator(targetUser)) {
throw new ApiError(meta.errors.cannotReportAdmin);
}
const report = await this.abuseUserReportsRepository.insert({
id: this.idService.gen(),
targetUserId: user.id,
targetUserHost: user.host,
await this.abuseReportService.report([{
targetUserId: targetUser.id,
targetUserHost: targetUser.host,
reporterId: me.id,
reporterHost: null,
comment: ps.comment,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
// Publish event to moderators
setImmediate(async () => {
const moderators = await this.roleService.getModerators();
for (const moderator of moderators) {
this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
id: report.id,
targetUserId: report.targetUserId,
reporterId: report.reporterId,
comment: report.comment,
});
}
const meta = await this.metaService.fetch();
if (meta.email) {
this.emailService.sendEmail(meta.email, 'New abuse report',
sanitizeHtml(ps.comment),
sanitizeHtml(ps.comment));
}
});
}]);
});
}
}

View file

@ -3,15 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { UserSearchService } from '@/core/UserSearchService.js';
export const meta = {
tags: ['users'],
@ -49,89 +43,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private userSearchService: UserSearchService,
) {
super(meta, paramDef, async (ps, me) => {
const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => {
if (ps.username) {
query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' });
}
if (ps.host) {
if (ps.host === this.config.hostname || ps.host === '.') {
query.andWhere('user.host IS NULL');
} else {
query.andWhere('user.host LIKE :host', {
host: sqlLikeEscape(ps.host.toLowerCase()) + '%',
});
}
}
return query;
};
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
let users: MiUser[] = [];
if (me) {
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const query = setUsernameAndHostQuery()
.andWhere(`user.id IN (${ followingQuery.getQuery() })`)
.andWhere('user.id != :meId', { meId: me.id })
.andWhere('user.isSuspended = FALSE')
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}));
query.setParameters(followingQuery.getParameters());
users = await query
.orderBy('user.usernameLower', 'ASC')
.limit(ps.limit)
.getMany();
if (users.length < ps.limit) {
const otherQuery = setUsernameAndHostQuery()
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`)
.andWhere('user.isSuspended = FALSE')
.andWhere('user.updatedAt IS NOT NULL');
otherQuery.setParameters(followingQuery.getParameters());
const otherUsers = await otherQuery
.orderBy('user.updatedAt', 'DESC')
.limit(ps.limit - users.length)
.getMany();
users = users.concat(otherUsers);
}
} else {
const query = setUsernameAndHostQuery()
.andWhere('user.isSuspended = FALSE')
.andWhere('user.updatedAt IS NOT NULL');
users = await query
.orderBy('user.updatedAt', 'DESC')
.limit(ps.limit - users.length)
.getMany();
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
super(meta, paramDef, (ps, me) => {
return this.userSearchService.search({
username: ps.username,
host: ps.host,
}, {
limit: ps.limit,
detail: ps.detail,
}, me);
});
}
}

View file

@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@');
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
if (isUsername) {
const usernameQuery = this.usersRepository.createQueryBuilder('user')
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
usernameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
usernameQuery.andWhere('user.host IS NOT NULL');
}
users = await usernameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username
if (this.userEntityService.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });

View file

@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// リクエストされた通りに並べ替え
// 順番は保持されるけど数は減ってる可能性がある
const _users: MiUser[] = [];
for (const id of ps.userIds) {
_users.push(users.find(x => x.id === id)!);
const user = users.find(x => x.id === id);
if (user != null) _users.push(user);
}
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
schema: 'UserDetailed',
})));
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') {