Merge tag '2023.9.2' into merge-upstream
This commit is contained in:
@@ -320,7 +320,13 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
|
||||
if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
|
||||
const policies = await this.roleService.getUserPolicies(user!.id);
|
||||
if (!policies[ep.meta.requireRolePolicy]) {
|
||||
let denied: boolean;
|
||||
if (Array.isArray(ep.meta.requireRolePolicy)) {
|
||||
denied = ep.meta.requireRolePolicy.some(policy => !policies[policy]);
|
||||
} else {
|
||||
denied = !policies[ep.meta.requireRolePolicy];
|
||||
}
|
||||
if (denied) {
|
||||
throw new ApiError({
|
||||
message: 'You are not assigned to a required role.',
|
||||
code: 'ROLE_PERMISSION_DENIED',
|
||||
|
@@ -262,6 +262,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||
import * as ep___notes_update from './endpoints/notes/update.js';
|
||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||
@@ -614,6 +615,7 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
|
||||
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
|
||||
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
|
||||
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
|
||||
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
|
||||
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
|
||||
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
|
||||
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
|
||||
@@ -970,6 +972,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$notes_conversation,
|
||||
$notes_create,
|
||||
$notes_delete,
|
||||
$notes_update,
|
||||
$notes_favorites_create,
|
||||
$notes_favorites_delete,
|
||||
$notes_featured,
|
||||
@@ -1320,6 +1323,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$notes_conversation,
|
||||
$notes_create,
|
||||
$notes_delete,
|
||||
$notes_update,
|
||||
$notes_favorites_create,
|
||||
$notes_favorites_delete,
|
||||
$notes_featured,
|
||||
|
@@ -262,6 +262,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
|
||||
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
|
||||
import * as ep___notes_create from './endpoints/notes/create.js';
|
||||
import * as ep___notes_delete from './endpoints/notes/delete.js';
|
||||
import * as ep___notes_update from './endpoints/notes/update.js';
|
||||
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
|
||||
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
|
||||
import * as ep___notes_featured from './endpoints/notes/featured.js';
|
||||
@@ -612,6 +613,7 @@ const eps = [
|
||||
['notes/conversation', ep___notes_conversation],
|
||||
['notes/create', ep___notes_create],
|
||||
['notes/delete', ep___notes_delete],
|
||||
['notes/update', ep___notes_update],
|
||||
['notes/favorites/create', ep___notes_favorites_create],
|
||||
['notes/favorites/delete', ep___notes_favorites_delete],
|
||||
['notes/featured', ep___notes_featured],
|
||||
@@ -737,7 +739,7 @@ export interface IEndpointMeta {
|
||||
*/
|
||||
readonly requireAdmin?: boolean;
|
||||
|
||||
readonly requireRolePolicy?: keyof RolePolicies;
|
||||
readonly requireRolePolicy?: keyof RolePolicies | readonly (keyof RolePolicies)[];
|
||||
|
||||
/**
|
||||
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
||||
|
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AdsRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.adsRepository.insert({
|
||||
const ad = await this.adsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
@@ -53,7 +55,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
ratio: ps.ratio,
|
||||
place: ps.place,
|
||||
memo: ps.memo,
|
||||
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
|
||||
|
||||
this.moderationLogService.log(me, 'createAd', {
|
||||
adId: ad.id,
|
||||
ad: ad,
|
||||
});
|
||||
|
||||
return ad;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AdsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
constructor(
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
||||
@@ -44,6 +47,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ad == null) throw new ApiError(meta.errors.noSuchAd);
|
||||
|
||||
await this.adsRepository.delete(ad.id);
|
||||
|
||||
this.moderationLogService.log(me, 'deleteAd', {
|
||||
adId: ad.id,
|
||||
ad: ad,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ export const paramDef = {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
publishing: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -36,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
|
||||
if (ps.publishing) {
|
||||
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
|
||||
}
|
||||
const ads = await query.limit(ps.limit).getMany();
|
||||
|
||||
return ads;
|
||||
|
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AdsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
constructor(
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const ad = await this.adsRepository.findOneBy({ id: ps.id });
|
||||
@@ -63,6 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
startsAt: new Date(ps.startsAt),
|
||||
dayOfWeek: ps.dayOfWeek,
|
||||
});
|
||||
|
||||
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });
|
||||
|
||||
this.moderationLogService.log(me, 'updateAd', {
|
||||
adId: ad.id,
|
||||
before: ad,
|
||||
after: updatedAd,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -60,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
|
||||
@@ -78,6 +80,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
const tickets = await Promise.all(ticketsPromises);
|
||||
|
||||
this.moderationLogService.log(me, 'createInvitation', {
|
||||
invitations: tickets,
|
||||
});
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||
mutedWords: profile.mutedWords,
|
||||
mutedInstances: profile.mutedInstances,
|
||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||
notificationRecieveConfig: profile.notificationRecieveConfig,
|
||||
isModerator: isModerator,
|
||||
isSilenced: isSilenced,
|
||||
isLimited: isLimited,
|
||||
|
@@ -166,9 +166,7 @@ export const paramDef = {
|
||||
mutedInstances: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
mutingNotificationTypes: { type: 'array', items: {
|
||||
type: 'string', enum: notificationTypes,
|
||||
} },
|
||||
notificationRecieveConfig: { type: 'object' },
|
||||
emailNotificationTypes: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -249,7 +247,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||
}
|
||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||
if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
|
||||
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
|
||||
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
||||
|
@@ -34,10 +34,11 @@ describe('api:notes/create', () => {
|
||||
.toBe(VALID);
|
||||
});
|
||||
|
||||
test('null post', () => {
|
||||
expect(v({ text: null }))
|
||||
.toBe(INVALID);
|
||||
});
|
||||
// TODO
|
||||
//test('null post', () => {
|
||||
// expect(v({ text: null }))
|
||||
// .toBe(INVALID);
|
||||
//});
|
||||
|
||||
test('0 characters post', () => {
|
||||
expect(v({ text: '' }))
|
||||
|
@@ -119,7 +119,7 @@ export const paramDef = {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
nullable: false,
|
||||
nullable: true,
|
||||
},
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
|
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
@@ -40,6 +41,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
@@ -88,6 +90,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
@@ -52,6 +52,7 @@ export const paramDef = {
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -137,6 +138,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
@@ -42,6 +42,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
fileType: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -110,6 +111,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
@@ -42,6 +42,7 @@ export const paramDef = {
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -126,6 +127,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
89
packages/backend/src/server/api/endpoints/notes/update.ts
Normal file
89
packages/backend/src/server/api/endpoints/notes/update.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: ['canUpdateContent', 'canEditNote'],
|
||||
|
||||
kind: 'write:notes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 10,
|
||||
minInterval: ms('1sec'),
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
text: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||
nullable: false,
|
||||
},
|
||||
cw: { type: 'string', nullable: true, maxLength: 100 },
|
||||
},
|
||||
required: ['noteId', 'text', 'cw'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (note.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchNote);
|
||||
}
|
||||
|
||||
await this.notesRepository.update({ id: note.id }, {
|
||||
updatedAt: new Date(),
|
||||
cw: ps.cw,
|
||||
text: ps.text,
|
||||
});
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
||||
cw: ps.cw,
|
||||
text: ps.text,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -49,6 +49,8 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
@@ -130,6 +132,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}));
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
@@ -147,7 +147,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userListService.push(currentUser, userList, me);
|
||||
await this.userListService.addMember(currentUser, userList, me);
|
||||
} catch (err) {
|
||||
if (err instanceof UserListService.TooManyUsersError) {
|
||||
throw new ApiError(meta.errors.tooManyUsers);
|
||||
|
@@ -4,12 +4,11 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
|
||||
import type { UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -54,12 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userListService: UserListService,
|
||||
private getterService: GetterService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
@@ -78,10 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Pull the user
|
||||
await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id });
|
||||
|
||||
this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user, me));
|
||||
await this.userListService.removeMember(user, userList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -128,7 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userListService.push(user, userList, me);
|
||||
await this.userListService.addMember(user, userList, me);
|
||||
} catch (err) {
|
||||
if (err instanceof UserListService.TooManyUsersError) {
|
||||
throw new ApiError(meta.errors.tooManyUsers);
|
||||
|
@@ -41,7 +41,8 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
includeReplies: { type: 'boolean', default: true },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
withRenotes: { type: 'boolean', default: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
@@ -114,10 +115,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
}
|
||||
|
||||
if (!ps.includeReplies) {
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :userId', { userId: user.id });
|
||||
|
@@ -12,10 +12,10 @@ import type { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
import type { StreamEventEmitter, StreamMessages } from './types.js';
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
@@ -122,7 +122,7 @@ export default class Connection {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
|
||||
private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) {
|
||||
this.sendMessageToWs(data.type, data.body);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export default class Connection {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
|
||||
private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
|
||||
this.sendMessageToWs('noteUpdated', {
|
||||
id: data.body.id,
|
||||
type: data.type,
|
||||
|
@@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import Channel from '../channel.js';
|
||||
import type { StreamMessages } from '../types.js';
|
||||
|
||||
class AntennaChannel extends Channel {
|
||||
public readonly chName = 'antenna';
|
||||
@@ -35,7 +35,7 @@ class AntennaChannel extends Channel {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onEvent(data: StreamMessages['antenna']['payload']) {
|
||||
private async onEvent(data: GlobalEvents['antenna']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
|
@@ -19,6 +19,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -38,8 +39,9 @@ class GlobalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.gtlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
this.withFiles = params.withFiles as boolean;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@@ -73,6 +75,8 @@ class GlobalTimelineChannel extends Channel {
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||
|
||||
|
@@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel {
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -31,8 +32,9 @@ class HomeTimelineChannel extends Channel {
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
this.withFiles = params.withFiles as boolean;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
@@ -82,6 +84,8 @@ class HomeTimelineChannel extends Channel {
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@@ -19,6 +19,7 @@ class HybridTimelineChannel extends Channel {
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -38,8 +39,9 @@ class HybridTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
this.withFiles = params.withFiles as boolean;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withReplies = params.withFiles ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@@ -94,6 +96,8 @@ class HybridTimelineChannel extends Channel {
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@@ -18,6 +18,7 @@ class LocalTimelineChannel extends Channel {
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
private withRenotes: boolean;
|
||||
private withFiles: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -37,8 +38,9 @@ class LocalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
this.withFiles = params.withFiles as boolean;
|
||||
this.withReplies = params.withReplies ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
@@ -73,6 +75,8 @@ class LocalTimelineChannel extends Channel {
|
||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
@@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import Channel from '../channel.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
public readonly chName = 'roleTimeline';
|
||||
@@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
|
||||
private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
|
||||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
|
||||
|
@@ -1,255 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { MiAntenna } from '@/models/Antenna.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import type { MiUserList } from '@/models/UserList.js';
|
||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import type { MiSignin } from '@/models/Signin.js';
|
||||
import type { MiPage } from '@/models/Page.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { MiWebhook } from '@/models/Webhook.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { MiRole, MiRoleAssignment } from '@/models/_.js';
|
||||
import type Emitter from 'strict-event-emitter-types';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
//#region Stream type-body definitions
|
||||
export interface InternalStreamTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
||||
remoteUserUpdated: { id: MiUser['id']; };
|
||||
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
policiesUpdated: MiRole['policies'];
|
||||
roleCreated: MiRole;
|
||||
roleDeleted: MiRole;
|
||||
roleUpdated: MiRole;
|
||||
userRoleAssigned: MiRoleAssignment;
|
||||
userRoleUnassigned: MiRoleAssignment;
|
||||
webhookCreated: MiWebhook;
|
||||
webhookDeleted: MiWebhook;
|
||||
webhookUpdated: MiWebhook;
|
||||
antennaCreated: MiAntenna;
|
||||
antennaDeleted: MiAntenna;
|
||||
antennaUpdated: MiAntenna;
|
||||
metaUpdated: MiMeta;
|
||||
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
|
||||
updateUserProfile: MiUserProfile;
|
||||
mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
|
||||
}
|
||||
|
||||
export interface BroadcastTypes {
|
||||
emojiAdded: {
|
||||
emoji: Packed<'EmojiDetailed'>;
|
||||
};
|
||||
emojiUpdated: {
|
||||
emojis: Packed<'EmojiDetailed'>[];
|
||||
};
|
||||
emojiDeleted: {
|
||||
emojis: {
|
||||
id?: string;
|
||||
name: string;
|
||||
[other: string]: any;
|
||||
}[];
|
||||
};
|
||||
announcementCreated: {
|
||||
announcement: Packed<'Announcement'>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MainStreamTypes {
|
||||
notification: Packed<'Notification'>;
|
||||
mention: Packed<'Note'>;
|
||||
reply: Packed<'Note'>;
|
||||
renote: Packed<'Note'>;
|
||||
follow: Packed<'UserDetailedNotMe'>;
|
||||
followed: Packed<'User'>;
|
||||
unfollow: Packed<'User'>;
|
||||
meUpdated: Packed<'User'>;
|
||||
pageEvent: {
|
||||
pageId: MiPage['id'];
|
||||
event: string;
|
||||
var: any;
|
||||
userId: MiUser['id'];
|
||||
user: Packed<'User'>;
|
||||
};
|
||||
urlUploadFinished: {
|
||||
marker?: string | null;
|
||||
file: Packed<'DriveFile'>;
|
||||
};
|
||||
readAllNotifications: undefined;
|
||||
unreadNotification: Packed<'Notification'>;
|
||||
unreadMention: MiNote['id'];
|
||||
readAllUnreadMentions: undefined;
|
||||
unreadSpecifiedNote: MiNote['id'];
|
||||
readAllUnreadSpecifiedNotes: undefined;
|
||||
readAllAntennas: undefined;
|
||||
unreadAntenna: MiAntenna;
|
||||
readAllAnnouncements: undefined;
|
||||
myTokenRegenerated: undefined;
|
||||
signin: MiSignin;
|
||||
registryUpdated: {
|
||||
scope?: string[];
|
||||
key: string;
|
||||
value: any | null;
|
||||
};
|
||||
driveFileCreated: Packed<'DriveFile'>;
|
||||
readAntenna: MiAntenna;
|
||||
receiveFollowRequest: Packed<'User'>;
|
||||
announcementCreated: {
|
||||
announcement: Packed<'Announcement'>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DriveStreamTypes {
|
||||
fileCreated: Packed<'DriveFile'>;
|
||||
fileDeleted: MiDriveFile['id'];
|
||||
fileUpdated: Packed<'DriveFile'>;
|
||||
folderCreated: Packed<'DriveFolder'>;
|
||||
folderDeleted: MiDriveFolder['id'];
|
||||
folderUpdated: Packed<'DriveFolder'>;
|
||||
}
|
||||
|
||||
export interface NoteStreamTypes {
|
||||
pollVoted: {
|
||||
choice: number;
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
deleted: {
|
||||
deletedAt: Date;
|
||||
};
|
||||
reacted: {
|
||||
reaction: string;
|
||||
emoji?: {
|
||||
name: string;
|
||||
url: string;
|
||||
} | null;
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
unreacted: {
|
||||
reaction: string;
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
type NoteStreamEventTypes = {
|
||||
[key in keyof NoteStreamTypes]: {
|
||||
id: MiNote['id'];
|
||||
body: NoteStreamTypes[key];
|
||||
};
|
||||
};
|
||||
|
||||
export interface UserListStreamTypes {
|
||||
userAdded: Packed<'User'>;
|
||||
userRemoved: Packed<'User'>;
|
||||
}
|
||||
|
||||
export interface AntennaStreamTypes {
|
||||
note: MiNote;
|
||||
}
|
||||
|
||||
export interface RoleTimelineStreamTypes {
|
||||
note: Packed<'Note'>;
|
||||
}
|
||||
|
||||
export interface AdminStreamTypes {
|
||||
newAbuseUserReport: {
|
||||
id: MiAbuseUserReport['id'];
|
||||
targetUserId: MiUser['id'],
|
||||
reporterId: MiUser['id'],
|
||||
comment: string;
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
|
||||
// VS Codeの展開を防止するためにEvents型を定義
|
||||
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
||||
type EventUnionFromDictionary<
|
||||
T extends object,
|
||||
U = Events<T>
|
||||
> = U[keyof U];
|
||||
|
||||
// redis通すとDateのインスタンスはstringに変換されるので
|
||||
export type Serialized<T> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends Date
|
||||
? string
|
||||
: T[K] extends (Date | null)
|
||||
? (string | null)
|
||||
: T[K] extends Record<string, any>
|
||||
? Serialized<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
type SerializedAll<T> = {
|
||||
[K in keyof T]: Serialized<T[K]>;
|
||||
};
|
||||
|
||||
// name/messages(spec) pairs dictionary
|
||||
export type StreamMessages = {
|
||||
internal: {
|
||||
name: 'internal';
|
||||
payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
|
||||
};
|
||||
broadcast: {
|
||||
name: 'broadcast';
|
||||
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
|
||||
};
|
||||
main: {
|
||||
name: `mainStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
|
||||
};
|
||||
drive: {
|
||||
name: `driveStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
|
||||
};
|
||||
note: {
|
||||
name: `noteStream:${MiNote['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
|
||||
};
|
||||
userList: {
|
||||
name: `userListStream:${MiUserList['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
|
||||
};
|
||||
roleTimeline: {
|
||||
name: `roleTimelineStream:${MiRole['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>;
|
||||
};
|
||||
antenna: {
|
||||
name: `antennaStream:${MiAntenna['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
|
||||
};
|
||||
admin: {
|
||||
name: `adminStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
|
||||
};
|
||||
notes: {
|
||||
name: 'notesStream';
|
||||
payload: Serialized<Packed<'Note'>>;
|
||||
};
|
||||
};
|
||||
|
||||
// API event definitions
|
||||
// ストリームごとのEmitterの辞書を用意
|
||||
type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
|
||||
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
||||
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
|
||||
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
|
||||
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
|
||||
|
||||
// provide stream channels union
|
||||
export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
|
@@ -189,7 +189,7 @@ export class ClientServerService {
|
||||
// Authenticate
|
||||
fastify.addHook('onRequest', async (request, reply) => {
|
||||
// %71ueueとかでリクエストされたら困るため
|
||||
const url = decodeURI(request.url);
|
||||
const url = decodeURI(request.routerPath);
|
||||
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
|
||||
const token = request.cookies.token;
|
||||
if (token == null) {
|
||||
@@ -777,8 +777,8 @@ export class ClientServerService {
|
||||
|
||||
fastify.setErrorHandler(async (error, request, reply) => {
|
||||
const errId = randomUUID();
|
||||
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routerPath}: ${error.message}`, {
|
||||
path: request.routerPath,
|
||||
this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, {
|
||||
path: request.routeOptions.url,
|
||||
params: request.params,
|
||||
query: request.query,
|
||||
code: error.name,
|
||||
|
@@ -35,7 +35,7 @@ html
|
||||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.35.0')
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.37.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
Reference in New Issue
Block a user