Merge tag '13.11.0' into io

# Conflicts:
#	packages/backend/src/server/ServerService.ts
#	packages/backend/src/server/api/endpoints/notes/timeline.ts
This commit is contained in:
和風ドレッシング 2023-04-08 22:01:55 +09:00
commit 0e4fe59d57
No known key found for this signature in database
GPG key ID: 82A8B5D42246F02D
650 changed files with 32472 additions and 9221 deletions

View file

@ -61,11 +61,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.usersRepository.update(user.id, {
isDeleted: true,
});
if (this.userEntityService.isLocalUser(user)) {
// Terminate streaming
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
});
}
}

View file

@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -26,38 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
});
}
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
});
}
}

View file

@ -56,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
category: null,
aliases: [],
host: null,
license: null,
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {

View file

@ -87,10 +87,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
type: driveFile.webpublicType ?? driveFile.type,
license: emoji.license,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(copied.id),
});

View file

@ -1,11 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -24,38 +19,14 @@ export const paramDef = {
required: ['ids'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.delete(emoji.id);
await this.db.queryResultCache!.remove(['meta_emojis']);
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
}
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: await this.emojiEntityService.packDetailedMany(emojis),
});
await this.customEmojiService.deleteBulk(ps.ids);
});
}
}

View file

@ -1,12 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApiError } from '../../../error.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -31,38 +25,14 @@ export const paramDef = {
required: ['id'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private moderationLogService: ModerationLogService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await this.emojisRepository.delete(emoji.id);
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
emoji: emoji,
});
await this.customEmojiService.delete(ps.id);
});
}
}

View file

@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -26,38 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emojis = await this.emojisRepository.findBy({
id: In(ps.ids),
});
for (const emoji of emojis) {
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
});
}
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
});
}
}

View file

@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -26,34 +22,14 @@ export const paramDef = {
required: ['ids', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
aliases: ps.aliases,
});
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
});
}
}

View file

@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
@ -28,34 +24,14 @@ export const paramDef = {
required: ['ids'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
await this.emojisRepository.update({
id: In(ps.ids),
}, {
updatedAt: new Date(),
category: ps.category,
});
await this.db.queryResultCache!.remove(['meta_emojis']);
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
});
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
});
}
}

View file

@ -1,10 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -19,6 +15,11 @@ export const meta = {
code: 'NO_SUCH_EMOJI',
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
},
sameNameEmojiExists: {
message: 'Emoji that have same name already exists.',
code: 'SAME_NAME_EMOJI_EXISTS',
id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8',
},
},
} as const;
@ -26,7 +27,7 @@ export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
category: {
type: 'string',
nullable: true,
@ -35,54 +36,24 @@ export const paramDef = {
aliases: { type: 'array', items: {
type: 'string',
} },
license: { type: 'string', nullable: true },
},
required: ['id', 'name', 'aliases'],
} as const;
// TODO: ロジックをサービスに切り出す
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.db)
private db: DataSource,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
await this.customEmojiService.update(ps.id, {
name: ps.name,
category: ps.category,
category: ps.category ?? null,
aliases: ps.aliases,
license: ps.license ?? null,
});
await this.db.queryResultCache!.remove(['meta_emojis']);
const updated = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === ps.name) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [updated],
});
} else {
this.globalEventService.publishBroadcastStream('emojiDeleted', {
emojis: [await this.emojiEntityService.packDetailed(emoji)],
});
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: updated,
});
}
});
}
}

View file

@ -110,6 +110,14 @@ export const meta = {
optional: false, nullable: false,
},
},
sensitiveWords: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hcaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
@ -231,6 +239,14 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
enableChartsForRemoteUser: {
type: 'boolean',
optional: false, nullable: false,
},
enableChartsForFederatedInstances: {
type: 'boolean',
optional: false, nullable: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
@ -266,7 +282,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.ToSUrl,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
disableRegistration: instance.disableRegistration,
@ -290,13 +306,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
sensitiveWords: instance.sensitiveWords,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey,
@ -330,6 +344,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
};
});

View file

@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
type: { type: 'string', enum: ['deliver', 'inbox'] },
},
required: ['type'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
let delayedQueues;
switch (ps.type) {
case 'deliver':
delayedQueues = await this.queueService.deliverQueue.getDelayed();
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
const queue = delayedQueues[queueIndex];
await queue.promote();
}
break;
case 'inbox':
delayedQueues = await this.queueService.inboxQueue.getDelayed();
for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) {
const queue = delayedQueues[queueIndex];
await queue.promote();
}
break;
}
this.moderationLogService.insertModerationLog(me, 'promoteQueue');
});
}
}

View file

@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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);
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
}
await this.abuseUserReportsRepository.update(report.id, {

View file

@ -27,6 +27,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
type: 'object',
},
@ -43,6 +44,7 @@ export const paramDef = {
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'displayOrder',
'policies',
],
} as const;
@ -76,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isModerator: ps.isModerator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));

View file

@ -35,6 +35,7 @@ export const paramDef = {
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
policies: {
type: 'object',
},
@ -52,6 +53,7 @@ export const paramDef = {
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'displayOrder',
'policies',
],
} as const;
@ -85,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isAdministrator: ps.isAdministrator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
});
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });

View file

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js';
import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -36,9 +36,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
private userEntityService: UserEntityService,
private userFollowingService: UserFollowingService,
private userSuspendService: UserSuspendService,
@ -65,15 +62,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
targetId: user.id,
});
// Terminate streaming
if (this.userEntityService.isLocalUser(user)) {
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
}
(async () => {
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
await this.readAllNotify(user).catch(e => {});
})();
});
}
@ -96,14 +87,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userFollowingService.unfollow(follower, followee, true);
}
}
@bindThis
private async readAllNotify(notifier: User) {
await this.notificationsRepository.update({
notifierId: notifier.id,
isRead: false,
}, {
isRead: true,
});
}
}

View file

@ -17,7 +17,6 @@ export const paramDef = {
type: 'object',
properties: {
disableRegistration: { type: 'boolean', nullable: true },
useStarForReactionFallback: { type: 'boolean', nullable: true },
pinnedUsers: { type: 'array', nullable: true, items: {
type: 'string',
} },
@ -27,6 +26,9 @@ export const paramDef = {
blockedHosts: { type: 'array', nullable: true, items: {
type: 'string',
} },
sensitiveWords: { type: 'array', nullable: true, items: {
type: 'string',
} },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@ -56,10 +58,6 @@ export const paramDef = {
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
maintainerName: { type: 'string', nullable: true },
maintainerEmail: { type: 'string', nullable: true },
pinnedPages: { type: 'array', items: {
type: 'string',
} },
pinnedClipId: { type: 'string', format: 'misskey:id', nullable: true },
langs: { type: 'array', items: {
type: 'string',
} },
@ -94,6 +92,8 @@ export const paramDef = {
objectStorageS3ForcePathStyle: { type: 'boolean' },
enableIpLogging: { type: 'boolean' },
enableActiveEmailValidation: { type: 'boolean' },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
},
required: [],
} as const;
@ -115,10 +115,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.disableRegistration = ps.disableRegistration;
}
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
if (Array.isArray(ps.pinnedUsers)) {
set.pinnedUsers = ps.pinnedUsers.filter(Boolean);
}
@ -131,6 +127,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
}
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}
@ -247,14 +247,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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;
}
@ -304,7 +296,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
if (ps.tosUrl !== undefined) {
set.ToSUrl = ps.tosUrl;
set.termsOfServiceUrl = ps.tosUrl;
}
if (ps.repositoryUrl !== undefined) {
@ -387,6 +379,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
}
if (ps.enableChartsForRemoteUser !== undefined) {
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
}
if (ps.enableChartsForFederatedInstances !== undefined) {
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta');
});

View file

@ -79,6 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) {
throw new Error('invalid param');
}
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
@ -99,9 +103,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
}
const now = new Date();
const antenna = await this.antennasRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
createdAt: now,
lastUsedAt: now,
userId: me.id,
name: ps.name,
src: ps.src,

View file

@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -50,15 +52,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private noteReadService: NoteReadService,
@ -73,34 +76,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchAntenna);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
const noteIdsRes = await this.redisClient.xrevrange(
`antennaTimeline:${antenna.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
if (noteIdsRes.length === 0) {
return [];
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];
}
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
const notes = await query
.take(ps.limit)
.getMany();
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
if (notes.length > 0) {
this.noteReadService.read(me.id, notes);
}
this.antennasRepository.update(antenna.id, {
lastUsedAt: new Date(),
});
return await this.noteEntityService.packMany(notes, me);
});
}

View file

@ -5,7 +5,7 @@ import type { UsersRepository, NotesRepository } from '@/models/index.js';
import type { Note } from '@/models/entities/Note.js';
import type { LocalUser, User } from '@/models/entities/User.js';
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/schema.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { MetaService } from '@/core/MetaService.js';

View file

@ -0,0 +1,61 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
requireCredential: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '4938f5f3-6167-4c04-9149-6607b7542861',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await this.channelFavoritesRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
channelId: channel.id,
});
});
}
}

View file

@ -41,7 +41,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private channelFollowingsRepository: ChannelFollowingsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
@ -58,8 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
followerId: me.id,
followeeId: channel.id,
});
this.globalEventService.publishUserEvent(me.id, 'followChannel', channel);
});
}
}

View file

@ -0,0 +1,54 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['channels', 'account'],
requireCredential: true,
kind: 'read:channels',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Channel',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
private channelEntityService: ChannelEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.channelFavoritesRepository.createQueryBuilder('favorite')
.andWhere('favorite.userId = :meId', { meId: me.id })
.leftJoinAndSelect('favorite.channel', 'channel');
const favorites = await query
.getMany();
return await Promise.all(favorites.map(x => this.channelEntityService.pack(x.channel!, me)));
});
}
}

View file

@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchChannel);
}
return await this.channelEntityService.pack(channel, me);
return await this.channelEntityService.pack(channel, me, true);
});
}
}

View file

@ -1,10 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
import type { ChannelsRepository, Note, NotesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -48,12 +50,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private activeUsersChart: ActiveUsersChart,
@ -67,30 +73,60 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchChannel);
}
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId = :channelId', { channelId: channel.id })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.leftJoinAndSelect('note.channel', 'channel');
let timeline: Note[] = [];
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
const noteIdsRes = await this.redisClient.xrevrange(
`channelTimeline:${channel.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
if (noteIdsRes.length === 0) {
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId = :channelId', { channelId: channel.id })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
//#endregion
timeline = await query.take(ps.limit).getMany();
} else {
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];
}
//#region Construct query
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
//#endregion
timeline = await query.getMany();
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
}
//#endregion
const timeline = await query.take(ps.limit).getMany();
if (me) this.activeUsersChart.read(me);

View file

@ -0,0 +1,56 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelFavoritesRepository, ChannelsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['channels'],
requireCredential: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: '353c68dd-131a-476c-aa99-88a345e83668',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
@Inject(DI.channelFavoritesRepository)
private channelFavoritesRepository: ChannelFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
await this.channelFavoritesRepository.delete({
userId: me.id,
channelId: channel.id,
});
});
}
}

View file

@ -38,8 +38,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
@ -54,8 +52,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
followerId: me.id,
followeeId: channel.id,
});
this.globalEventService.publishUserEvent(me.id, 'unfollowChannel', channel);
});
}
}

View file

@ -3,6 +3,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/index.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -46,6 +47,12 @@ export const paramDef = {
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
pinnedNoteIds: {
type: 'array',
items: {
type: 'string', format: 'misskey:id',
},
},
},
required: ['channelId'],
} as const;
@ -61,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private channelEntityService: ChannelEntityService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const channel = await this.channelsRepository.findOneBy({
@ -71,7 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchChannel);
}
if (channel.userId !== me.id) {
const iAmModerator = await this.roleService.isModerator(me);
if (channel.userId !== me.id && !iAmModerator) {
throw new ApiError(meta.errors.accessDenied);
}
@ -93,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.channelsRepository.update(channel.id, {
...(ps.name !== undefined ? { name: ps.name } : {}),
...(ps.description !== undefined ? { description: ps.description } : {}),
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
...(banner ? { bannerId: banner.id } : {}),
});

View file

@ -106,6 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
noteId: note.id,
clipId: clip.id,
});
await this.clipsRepository.update(clip.id, {
lastClippedAt: new Date(),
});
});
}
}

View file

@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
description: ps.description,
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
return await this.clipEntityService.pack(clip);
return await this.clipEntityService.pack(clip, me);
});
}
}

View file

@ -0,0 +1,76 @@
import { Inject, Injectable } from '@nestjs/common';
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['clip'],
requireCredential: true,
kind: 'write:clip-favorite',
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
},
alreadyFavorited: {
message: 'The clip has already been favorited.',
code: 'ALREADY_FAVORITED',
id: '92658936-c625-4273-8326-2d790129256e',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
clipId: { type: 'string', format: 'misskey:id' },
},
required: ['clipId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
if ((clip.userId !== me.id) && !clip.isPublic) {
throw new ApiError(meta.errors.noSuchClip);
}
const exist = await this.clipFavoritesRepository.findOneBy({
clipId: clip.id,
userId: me.id,
});
if (exist != null) {
throw new ApiError(meta.errors.alreadyFavorited);
}
await this.clipFavoritesRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
clipId: clip.id,
userId: me.id,
});
});
}
}

View file

@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id,
});
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
return await this.clipEntityService.packMany(clips, me);
});
}
}

View file

@ -0,0 +1,52 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ClipFavoritesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
export const meta = {
tags: ['account', 'clip'],
requireCredential: true,
kind: 'read:clip-favorite',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Clip',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
private clipEntityService: ClipEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.clipFavoritesRepository.createQueryBuilder('favorite')
.andWhere('favorite.userId = :meId', { meId: me.id })
.leftJoinAndSelect('favorite.clip', 'clip');
const favorites = await query
.getMany();
return this.clipEntityService.packMany(favorites.map(x => x.clip!), me);
});
}
}

View file

@ -75,16 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
if (me) {

View file

@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchClip);
}
return await this.clipEntityService.pack(clip);
return await this.clipEntityService.pack(clip, me);
});
}
}

View file

@ -0,0 +1,65 @@
import { Inject, Injectable } from '@nestjs/common';
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['clip'],
requireCredential: true,
kind: 'write:clip-favorite',
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: '2603966e-b865-426c-94a7-af4a01241dc1',
},
notFavorited: {
message: 'You have not favorited the clip.',
code: 'NOT_FAVORITED',
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
clipId: { type: 'string', format: 'misskey:id' },
},
required: ['clipId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
@Inject(DI.clipFavoritesRepository)
private clipFavoritesRepository: ClipFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const exist = await this.clipFavoritesRepository.findOneBy({
clipId: clip.id,
userId: me.id,
});
if (exist == null) {
throw new ApiError(meta.errors.notFavorited);
}
await this.clipFavoritesRepository.delete(exist.id);
});
}
}

View file

@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isPublic: ps.isPublic,
});
return await this.clipEntityService.pack(clip.id);
return await this.clipEntityService.pack(clip.id, me);
});
}
}

View file

@ -31,6 +31,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] },
},
required: [],
} as const;
@ -63,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
}
switch (ps.sort) {
case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break;
case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break;
case '+name': query.orderBy('file.name', 'DESC'); break;
case '-name': query.orderBy('file.name', 'ASC'); break;
case '+size': query.orderBy('file.size', 'DESC'); break;
case '-size': query.orderBy('file.size', 'ASC'); break;
}
const files = await query.take(ps.limit).getMany();
return await this.driveFileEntityService.packMany(files, { detail: false, self: true });

View file

@ -0,0 +1,56 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['meta'],
requireCredential: false,
allowGet: true,
cacheSec: 3600,
res: {
type: 'object',
optional: false, nullable: false,
ref: 'EmojiDetailed',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
name: {
type: 'string',
},
},
required: ['name'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const emoji = await this.emojisRepository.findOneOrFail({
where: {
name: ps.name,
host: IsNull(),
},
});
return this.emojiEntityService.packDetailed(emoji);
});
}
}

View file

@ -23,24 +23,7 @@ export const meta = {
items: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
category: {
type: 'string',
optional: false, nullable: true,
},
},
ref: 'EmojiSimple',
},
},
},
@ -75,10 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
category: 'ASC',
name: 'ASC',
},
cache: {
id: 'meta_emojis',
milliseconds: 3600000, // 1 hour
},
});
return {

View file

@ -76,9 +76,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.blocked === 'boolean') {
const meta = await this.metaService.fetch(true);
if (ps.blocked) {
query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
query.andWhere(meta.blockedHosts.length === 0 ? '1=0' : 'instance.host IN (:...blocks)', { blocks: meta.blockedHosts });
} else {
query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
query.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts });
}
}

View file

@ -3,6 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../error.js';
export const meta = {
tags: ['account'],
@ -14,6 +15,15 @@ export const meta = {
optional: false, nullable: false,
ref: 'MeDetailed',
},
errors: {
userIsDeleted: {
message: 'User is deleted.',
code: 'USER_IS_DELETED',
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
kind: 'permission',
},
}
} as const;
export const paramDef = {
@ -41,13 +51,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
// 渡ってきている user はキャッシュされていて古い可能性があるので改めて取得
const userProfile = await this.userProfilesRepository.findOneOrFail({
const userProfile = await this.userProfilesRepository.findOne({
where: {
userId: user.id,
},
relations: ['user'],
});
if (userProfile == null) {
throw new ApiError(meta.errors.userIsDeleted);
}
if (!userProfile.loggedInDates.includes(today)) {
this.userProfilesRepository.update({ userId: user.id }, {
loggedInDates: [...userProfile.loggedInDates, today],

View file

@ -0,0 +1,92 @@
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
export const meta = {
tags: ['users'],
secure: true,
requireCredential: true,
limit: {
duration: ms('1day'),
max: 30,
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
},
notRemote: {
message: 'User is not remote. You can only migrate from other instances.',
code: 'NOT_REMOTE',
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
},
uriNull: {
message: 'User ActivityPup URI is null.',
code: 'URI_NULL',
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
alsoKnownAs: { type: 'string' },
},
required: ['alsoKnownAs'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private userEntityService: UserEntityService,
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
// Check parameter
if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
let unfiltered = ps.alsoKnownAs;
const updates = {} as Partial<User>;
if (!unfiltered) {
updates.alsoKnownAs = null;
} else {
// Parse user's input into the old account
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
const userAddress = unfiltered.split('@');
// Retrieve the old account
const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
throw new ApiError(meta.errors.noSuchUser);
});
const toUrl: string | null = knownAs.uri;
if (!toUrl) throw new ApiError(meta.errors.uriNull);
// Only allow moving from a remote account
if (this.userEntityService.isLocalUser(knownAs)) throw new ApiError(meta.errors.notRemote);
updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
}
return await this.accountMoveService.createAlias(me, updates);
});
}
}

View file

@ -0,0 +1,140 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
export const meta = {
tags: ['users'],
secure: true,
requireCredential: true,
limit: {
duration: ms('1day'),
max: 5,
},
errors: {
noSuchMoveTarget: {
message: 'No such move target.',
code: 'NO_SUCH_MOVE_TARGET',
id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
},
remoteAccountForbids: {
message:
'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
code: 'REMOTE_ACCOUNT_FORBIDS',
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
},
notRemote: {
message: 'User is not remote. You can only migrate to other instances.',
code: 'NOT_REMOTE',
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
},
rootForbidden: {
message: 'The root can\'t migrate.',
code: 'NOT_ROOT_FORBIDDEN',
id: '4362e8dc-731f-4ad8-a694-be2a88922a24',
},
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
},
uriNull: {
message: 'User ActivityPup URI is null.',
code: 'URI_NULL',
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
},
localUriNull: {
message: 'Local User ActivityPup URI is null.',
code: 'URI_NULL',
id: '95ba11b9-90e8-43a5-ba16-7acc1ab32e71',
},
alreadyMoved: {
message: 'Account was already moved to another account.',
code: 'ALREADY_MOVED',
id: 'b234a14e-9ebe-4581-8000-074b3c215962',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
moveToAccount: { type: 'string' },
},
required: ['moveToAccount'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.config)
private config: Config,
private userEntityService: UserEntityService,
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService,
private getterService: GetterService,
private apPersonService: ApPersonService,
) {
super(meta, paramDef, async (ps, me) => {
// check parameter
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
// abort if user is the root
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
// abort if user has already moved
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
let unfiltered = ps.moveToAccount;
if (!unfiltered) throw new ApiError(meta.errors.noSuchMoveTarget);
// parse user's input into the destination account
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
const userAddress = unfiltered.split('@');
// retrieve the destination account
let moveTo = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
throw new ApiError(meta.errors.noSuchMoveTarget);
});
const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id);
if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull);
// update local db
await this.apPersonService.updatePerson(remoteMoveTo.uri);
// retrieve updated user
moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri);
// only allow moving to a remote account
if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote);
let allowed = false;
const fromUrl = `${this.config.url}/users/${me.id}`;
// make sure that the user has indicated the old account as an alias
moveTo.alsoKnownAs?.forEach((elem) => {
if (fromUrl.includes(elem)) allowed = true;
});
// abort if unintended
if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids);
return await this.accountMoveService.moveToRemote(me, moveTo);
});
}
}

View file

@ -1,6 +1,7 @@
import { Brackets } from 'typeorm';
import { Brackets, In } from 'typeorm';
import Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js';
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
@ -8,6 +9,8 @@ import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { Notification } from '@/models/entities/Notification.js';
export const meta = {
tags: ['account', 'notifications'],
@ -38,8 +41,6 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
following: { type: 'boolean', default: false },
unreadOnly: { type: 'boolean', default: false },
markAsRead: { type: 'boolean', default: true },
// 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
@ -56,21 +57,22 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private idService: IdService,
private notificationEntityService: NotificationEntityService,
private notificationService: NotificationService,
private queryService: QueryService,
@ -89,85 +91,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
const notificationsRes = await this.redisClient.xrevrange(
`notificationTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-',
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
const suspendedQuery = this.usersRepository.createQueryBuilder('users')
.select('users.id')
.where('users.isSuspended = TRUE');
const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
.andWhere('notification.notifieeId = :meId', { meId: me.id })
.leftJoinAndSelect('notification.notifier', 'notifier')
.leftJoinAndSelect('notification.note', 'note')
.leftJoinAndSelect('notifier.avatar', 'notifierAvatar')
.leftJoinAndSelect('notifier.banner', 'notifierBanner')
.leftJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
// muted users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
query.setParameters(mutingQuery.getParameters());
// muted instances
query.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
query.setParameters(mutingInstanceQuery.getParameters());
// suspended users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
if (ps.following) {
query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id });
query.setParameters(followingQuery.getParameters());
if (notificationsRes.length === 0) {
return [];
}
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
if (includeTypes && includeTypes.length > 0) {
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
} else if (excludeTypes && excludeTypes.length > 0) {
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
if (ps.unreadOnly) {
query.andWhere('notification.isRead = false');
if (notifications.length === 0) {
return [];
}
const notifications = await query.take(ps.limit).getMany();
// Mark all as read
if (notifications.length > 0 && ps.markAsRead) {
this.notificationService.readNotification(me.id, notifications.map(x => x.id));
if (ps.markAsRead) {
this.notificationService.readAllNotification(me.id);
}
const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
const noteIds = notifications
.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type))
.map(notification => notification.noteId!);
if (notes.length > 0) {
if (noteIds.length > 0) {
const notes = await this.notesRepository.findBy({ id: In(noteIds) });
this.noteReadService.read(me.id, notes);
}

View file

@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const freshUser = await this.usersRepository.findOneByOrFail({ id: me.id });
const oldToken = freshUser.token;
const oldToken = freshUser.token!;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
@ -54,11 +54,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Publish event
this.globalEventService.publishInternalEvent('userTokenRegenerated', { id: me.id, oldToken, newToken });
this.globalEventService.publishMainStream(me.id, 'myTokenRegenerated');
// Terminate streaming
setTimeout(() => {
this.globalEventService.publishUserEvent(me.id, 'terminate', {});
}, 5000);
});
}
}

View file

@ -35,9 +35,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
id: ps.tokenId,
userId: me.id,
});
// Terminate streaming
this.globalEventService.publishUserEvent(me.id, 'terminate');
}
});
}

View file

@ -18,6 +18,8 @@ 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 { CacheService } from '@/core/CacheService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -147,11 +149,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private pagesRepository: PagesRepository,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
private accountUpdateService: AccountUpdateService,
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
@ -168,8 +172,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (ps.mutedWords !== undefined) {
// TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length;
@ -215,6 +217,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
}
if (ps.bannerId) {
@ -222,6 +228,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
}
if (ps.pinnedPageId) {
@ -276,9 +286,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
includeSecrets: isSecure,
});
const updatedProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
this.cacheService.userProfileCache.set(user.id, updatedProfile);
// Publish meUpdated event
this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneByOrFail({ userId: user.id }));
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
if (user.isLocked && ps.isLocked === false) {

View file

@ -276,7 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.ToSUrl,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
disableRegistration: instance.disableRegistration,
@ -315,8 +315,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
mediaProxy: this.config.mediaProxy,
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
requireSetup: (await this.usersRepository.countBy({
host: IsNull(),

View file

@ -1,12 +1,10 @@
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 { MutingsRepository } from '@/models/index.js';
import type { Muting } from '@/models/entities/Muting.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -62,9 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private globalEventService: GlobalEventService,
private getterService: GetterService,
private idService: IdService,
private userMutingService: UserMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
@ -94,16 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return;
}
// Create mute
await this.mutingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
muterId: muter.id,
muteeId: mutee.id,
} as Muting);
this.globalEventService.publishUserEvent(me.id, 'mute', mutee);
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
});
}
}

View file

@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private globalEventService: GlobalEventService,
private userMutingService: UserMutingService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@ -76,12 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.notMuting);
}
// Delete mute
await this.mutingsRepository.delete({
id: exist.id,
});
this.globalEventService.publishUserEvent(me.id, 'unmute', mutee);
await this.userMutingService.unmute([exist]);
});
}
}

View file

@ -49,16 +49,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('note.visibility = \'public\'')
.andWhere('note.localOnly = FALSE')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
if (ps.local) {
query.andWhere('note.userHost IS NULL');

View file

@ -57,16 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}));
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) {

View file

@ -4,8 +4,8 @@ import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['clips', 'notes'],
@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isPublic: true,
});
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
return await this.clipEntityService.packMany(clips, me);
});
}
}

View file

@ -97,6 +97,7 @@ export const paramDef = {
} },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
@ -110,7 +111,7 @@ export const paramDef = {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false
nullable: false,
},
fileIds: {
type: 'array',
@ -280,6 +281,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUsers,
channel,

View file

@ -53,16 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
.andWhere('note.visibility = \'public\'')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
@ -71,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let notes = await query
.orderBy('note.score', 'DESC')
.take(50)
.take(100)
.getMany();
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());

View file

@ -73,22 +73,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateRepliesQuery(query, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
if (ps.withFiles) {

View file

@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -69,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private metaService: MetaService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
@ -83,22 +85,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere(new Brackets(qb => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: me.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
@ -107,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {

View file

@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -65,6 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private metaService: MetaService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
@ -75,19 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.createdAt > :minDate', { minDate: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) // 30日前まで
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
@ -95,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View file

@ -60,16 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`);
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);

View file

@ -8,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
import { PollService } from '@/core/PollService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { ApiError } from '../../../error.js';
@ -89,7 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private pollService: PollService,
private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private createNotificationService: CreateNotificationService,
private userBlockingService: UserBlockingService,
) {
super(meta, paramDef, async (ps, me) => {
@ -161,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (note.userHost != null) {
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as RemoteUser;
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox);
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false);
}
// リモートフォロワーにUpdate配信

View file

@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
order: {
id: -1,
},
relations: ['user', 'user.avatar', 'user.banner', 'note'],
relations: ['user', 'note'],
});
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));

View file

@ -4,8 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
@ -62,16 +62,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.renoteId = :renoteId', { renoteId: note.id })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);

View file

@ -46,16 +46,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);

View file

@ -71,16 +71,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);

View file

@ -6,6 +6,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
@ -23,6 +25,11 @@ export const meta = {
},
errors: {
unavailable: {
message: 'Search of notes unavailable.',
code: 'UNAVAILABLE',
id: '0b44998d-77aa-4427-80d0-d2c9b8523011',
},
},
} as const;
@ -59,8 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!policies.canSearchNotes) {
throw new ApiError(meta.errors.unavailable);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
if (ps.userId) {
@ -72,16 +85,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query
.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);

View file

@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.noteThreadMutingsRepository.count({
where: {
userId: me.id,
threadId: note.threadId || note.id,
threadId: note.threadId ?? note.id,
},
take: 1,
}),

View file

@ -6,7 +6,7 @@ import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { getTimeId } from '@/misc/id/aid.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['notes'],
@ -57,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const followees = await this.followingsRepository.createQueryBuilder('following')
@ -68,18 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId })
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
@ -95,6 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {

View file

@ -84,16 +84,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(this.userListJoiningsRepository.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
this.queryService.generateVisibilityQuery(query, me);

View file

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications'],
@ -27,10 +27,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private createNotificationService: CreateNotificationService,
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, user, token) => {
this.createNotificationService.createNotification(user.id, 'app', {
this.notificationService.createNotification(user.id, 'app', {
appAccessTokenId: token ? token.id : null,
customBody: ps.body,
customHeader: ps.header,

View file

@ -1,9 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import type { NotificationsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { DI } from '@/di-symbols.js';
import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications', 'account'],
@ -23,24 +21,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.notificationsRepository)
private notificationsRepository: NotificationsRepository,
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
// Update documents
await this.notificationsRepository.update({
notifieeId: me.id,
isRead: false,
}, {
isRead: true,
});
// 全ての通知を読みましたよというイベントを発行
this.globalEventService.publishMainStream(me.id, 'readAllNotifications');
this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined);
this.notificationService.readAllNotification(me.id, true);
});
}
}

View file

@ -1,57 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications', 'account'],
requireCredential: true,
kind: 'write:notifications',
description: 'Mark a notification as read.',
errors: {
noSuchNotification: {
message: 'No such notification.',
code: 'NO_SUCH_NOTIFICATION',
id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e',
},
},
} as const;
export const paramDef = {
oneOf: [
{
type: 'object',
properties: {
notificationId: { type: 'string', format: 'misskey:id' },
},
required: ['notificationId'],
},
{
type: 'object',
properties: {
notificationIds: {
type: 'array',
items: { type: 'string', format: 'misskey:id' },
maxItems: 100,
},
},
required: ['notificationIds'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
if ('notificationId' in ps) return this.notificationService.readNotification(me.id, [ps.notificationId]);
return this.notificationService.readNotification(me.id, ps.notificationIds);
});
}
}

View file

@ -0,0 +1,97 @@
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/index.js';
import type { RenoteMuting } from '@/models/entities/RenoteMuting.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
requireCredential: true,
kind: 'write:mutes',
limit: {
duration: ms('1hour'),
max: 20,
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5e0a5dff-1e94-4202-87ae-4d9c89eb2271',
},
muteeIsYourself: {
message: 'Mutee is yourself.',
code: 'MUTEE_IS_YOURSELF',
id: '37285718-52f7-4aef-b7de-c38b8e8a8420',
},
alreadyMuting: {
message: 'You are already muting that user.',
code: 'ALREADY_MUTING',
id: 'ccfecbe4-1f1c-4fc2-8a3d-c3ffee61cb7b',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private globalEventService: GlobalEventService,
private getterService: GetterService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
// 自分自身
if (me.id === ps.userId) {
throw new ApiError(meta.errors.muteeIsYourself);
}
// Get mutee
const mutee = await getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
// Check if already muting
const exist = await this.renoteMutingsRepository.findOneBy({
muterId: muter.id,
muteeId: mutee.id,
});
if (exist != null) {
throw new ApiError(meta.errors.alreadyMuting);
}
// Create mute
await this.renoteMutingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
muterId: muter.id,
muteeId: mutee.id,
} as RenoteMuting);
});
}
}

View file

@ -0,0 +1,85 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RenoteMutingsRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
requireCredential: true,
kind: 'write:mutes',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '9b6728cf-638c-4aa1-bedb-e07d8101474d',
},
muteeIsYourself: {
message: 'Mutee is yourself.',
code: 'MUTEE_IS_YOURSELF',
id: '619b1314-0850-4597-a242-e245f3da42af',
},
notMuting: {
message: 'You are not muting that user.',
code: 'NOT_MUTING',
id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private globalEventService: GlobalEventService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
// Check if the mutee is yourself
if (me.id === ps.userId) {
throw new ApiError(meta.errors.muteeIsYourself);
}
// Get mutee
const mutee = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
// Check not muting
const exist = await this.renoteMutingsRepository.findOneBy({
muterId: muter.id,
muteeId: mutee.id,
});
if (exist == null) {
throw new ApiError(meta.errors.notMuting);
}
// Delete mute
await this.renoteMutingsRepository.delete({
id: exist.id,
});
});
}
}

View file

@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RenoteMutingsRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['account'],
requireCredential: true,
kind: 'read:mutes',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'RenoteMuting',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,
private renoteMutingEntityService: RenoteMutingEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
.andWhere('muting.muterId = :meId', { meId: me.id });
const mutings = await query
.take(ps.limit)
.getMany();
return await this.renoteMutingEntityService.packMany(mutings, me);
});
}
}

View file

@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.take(ps.limit)
.getMany();
return await this.clipEntityService.packMany(clips);
return await this.clipEntityService.packMany(clips, me);
});
}
}

View file

@ -74,16 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.userId = :userId', { userId: user.id })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
if (me) {

View file

@ -50,6 +50,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isRenoteMuted: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
{
@ -91,6 +95,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isRenoteMuted: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
},

View file

@ -48,6 +48,7 @@ export const meta = {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '4362f8dc-731f-4ad8-a694-be5a88922a24',
httpStatusCode: 404,
},
},
} as const;