Merge branch 'develop' into develop

This commit is contained in:
Marie 2023-11-26 17:04:46 +01:00 committed by GitHub
commit baf162c337
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 1499 additions and 627 deletions

View file

@ -0,0 +1,8 @@
import { loadConfig } from './built/config.js'
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');

View file

@ -0,0 +1,11 @@
export class HardMute1700383825690 {
name = 'HardMute1700383825690'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
}
}

View file

@ -23,7 +23,8 @@
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage"
"test-and-coverage": "pnpm jest-and-coverage",
"generate-api-json": "node ./generate_api_json.js"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
@ -165,7 +166,7 @@
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
"typescript": "5.2.2",
"typescript": "5.3.2",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.6",

View file

@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService,
private globalEventService: GlobalEventService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
) {
this.antennasFetched = false;
this.antennas = [];
@ -60,11 +60,21 @@ export class AntennaService implements OnApplicationShutdown {
lastUsedAt: new Date(body.lastUsedAt),
});
break;
case 'antennaUpdated':
this.antennas[this.antennas.findIndex(a => a.id === body.id)] = {
...body,
lastUsedAt: new Date(body.lastUsedAt),
};
case 'antennaUpdated': {
const idx = this.antennas.findIndex(a => a.id === body.id);
if (idx >= 0) {
this.antennas[idx] = {
...body,
lastUsedAt: new Date(body.lastUsedAt),
};
} else {
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
this.antennas.push({
...body,
lastUsedAt: new Date(body.lastUsedAt),
});
}
}
break;
case 'antennaDeleted':
this.antennas = this.antennas.filter(a => a.id !== body.id);
@ -84,7 +94,7 @@ export class AntennaService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline();
for (const antenna of matchedAntennas) {
this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
}

View file

@ -62,7 +62,7 @@ import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js';
import { FanoutTimelineService } from './FanoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js';
import { RegistryApiService } from './RegistryApiService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
@ -194,7 +194,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
@ -330,7 +330,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SearchService,
ClipService,
FeaturedService,
FunoutTimelineService,
FanoutTimelineService,
ChannelFollowingService,
RegistryApiService,
ChartLoggerService,
@ -459,7 +459,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SearchService,
$ClipService,
$FeaturedService,
$FunoutTimelineService,
$FanoutTimelineService,
$ChannelFollowingService,
$RegistryApiService,
$ChartLoggerService,
@ -589,7 +589,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SearchService,
ClipService,
FeaturedService,
FunoutTimelineService,
FanoutTimelineService,
ChannelFollowingService,
RegistryApiService,
FederationChart,
@ -717,7 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SearchService,
$ClipService,
$FeaturedService,
$FunoutTimelineService,
$FanoutTimelineService,
$ChannelFollowingService,
$RegistryApiService,
$FederationChart,

View file

@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class FunoutTimelineService {
export class FanoutTimelineService {
constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,

View file

@ -5,11 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { MiNote, MiUser } from '@/models/_.js';
import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
const HASHTAG_RANKING_WINDOW = 1000 * 60 * 60; // 1時間ごと
@ -79,6 +80,11 @@ export class FeaturedService {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score);
}
@bindThis
public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
@ -99,6 +105,11 @@ export class FeaturedService {
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, threshold);
}
@bindThis
public getGalleryPostsRanking(threshold: number): Promise<MiGalleryPost['id'][]> {
return this.getRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, threshold);
}
@bindThis
public getInChannelNotesRanking(channelId: MiNote['channelId'], threshold: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, threshold);

View file

@ -250,6 +250,12 @@ export class MfmService {
}
}
function fnDefault(node: mfm.MfmFn) {
const el = doc.createElement('i');
appendChildren(node.children, el);
return el;
}
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
bold: (node) => {
const el = doc.createElement('b');
@ -276,17 +282,68 @@ export class MfmService {
},
fn: (node) => {
if (node.props.name === 'unixtime') {
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
const date = new Date(parseInt(text, 10) * 1000);
const el = doc.createElement('time');
el.setAttribute('datetime', date.toISOString());
el.textContent = date.toISOString();
return el;
} else {
const el = doc.createElement('i');
appendChildren(node.children, el);
return el;
switch (node.props.name) {
case 'unixtime': {
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
try {
const date = new Date(parseInt(text, 10) * 1000);
const el = doc.createElement('time');
el.setAttribute('datetime', date.toISOString());
el.textContent = date.toISOString();
return el;
} catch (err) {
return fnDefault(node);
}
}
case 'ruby': {
if (node.children.length === 1) {
const child = node.children[0];
const text = child.type === 'text' ? child.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備りゅうび」となるようにする
const rpStartEl = doc.createElement('rp');
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
} else {
const rt = node.children.at(-1);
if (!rt) {
return fnDefault(node);
}
const text = rt.type === 'text' ? rt.props.text : '';
const rubyEl = doc.createElement('ruby');
const rtEl = doc.createElement('rt');
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備りゅうび」となるようにする
const rpStartEl = doc.createElement('rp');
rpStartEl.appendChild(doc.createTextNode('('));
const rpEndEl = doc.createElement('rp');
rpEndEl.appendChild(doc.createTextNode(')'));
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
rtEl.appendChild(doc.createTextNode(text.trim()));
rubyEl.appendChild(rpStartEl);
rubyEl.appendChild(rtEl);
rubyEl.appendChild(rpEndEl);
return rubyEl;
}
}
default: {
return fnDefault(node);
}
}
},

View file

@ -54,7 +54,7 @@ import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
@ -194,7 +194,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private idService: IdService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private noteReadService: NoteReadService,
private notificationService: NotificationService,
private relayService: RelayService,
@ -843,9 +843,9 @@ export class NoteCreateService implements OnApplicationShutdown {
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
@ -855,9 +855,9 @@ export class NoteCreateService implements OnApplicationShutdown {
});
for (const channelFollowing of channelFollowings) {
this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
@ -895,9 +895,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!following.withReplies) continue;
}
this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
@ -913,36 +913,36 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue;
}
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
}
} else {
this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
this.funoutTimelineService.push('localTimeline', note.id, 1000, r);
this.fanoutTimelineService.push('localTimeline', note.id, 1000, r);
if (note.fileIds.length > 0) {
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
}

View file

@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
@ -87,6 +87,9 @@ export class RoleService implements OnApplicationShutdown {
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@ -105,7 +108,7 @@ export class RoleService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private idService: IdService,
private moderationLogService: ModerationLogService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
) {
//this.onMessage = this.onMessage.bind(this);
@ -476,10 +479,10 @@ export class RoleService implements OnApplicationShutdown {
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
const roles = await this.getUserRoles(note.userId);
const redisPipeline = this.redisClient.pipeline();
const redisPipeline = this.redisForTimelines.pipeline();
for (const role of roles) {
this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
}

View file

@ -29,7 +29,7 @@ import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@ -84,7 +84,7 @@ export class UserFollowingService implements OnModuleInit {
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@ -305,7 +305,7 @@ export class UserFollowingService implements OnModuleInit {
}
});
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
}
// Publish followed event
@ -374,7 +374,7 @@ export class UserFollowingService implements OnModuleInit {
}
});
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
this.fanoutTimelineService.purge(`homeTimeline:${follower.id}`);
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {

View file

@ -198,12 +198,14 @@ export class NotificationEntityService implements OnModuleInit {
});
} else if (notification.type === 'renote:grouped') {
const users = await Promise.all(notification.userIds.map(userId => {
const user = hint?.packedUsers != null
? hint.packedUsers.get(userId)
: this.userEntityService.pack(userId!, { id: meId }, {
detail: false,
});
return user;
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
if (packedUser) {
return packedUser;
}
return this.userEntityService.pack(userId, { id: meId }, {
detail: false,
});
}));
return await awaitAll({
id: notification.id,

View file

@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
unreadNotificationsCount: notificationsInfo?.unreadCount,
mutedWords: profile!.mutedWords,
hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig,

View file

@ -215,7 +215,12 @@ export class MiUserProfile {
@Column('jsonb', {
default: [],
})
public mutedWords: string[][];
public mutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],
})
public hardMutedWords: (string[] | string)[];
@Column('jsonb', {
default: [],

View file

@ -42,11 +42,15 @@ export const packedAnnouncementSchema = {
type: 'string',
optional: false, nullable: false,
},
forYou: {
needConfirmationToRead: {
type: 'boolean',
optional: false, nullable: false,
},
needConfirmationToRead: {
silence: {
type: 'boolean',
optional: false, nullable: false,
},
forYou: {
type: 'boolean',
optional: false, nullable: false,
},

View file

@ -19,7 +19,7 @@ export const packedChannelSchema = {
},
lastNotedAt: {
type: 'string',
optional: false, nullable: true,
nullable: true, optional: false,
format: 'date-time',
},
name: {
@ -28,38 +28,18 @@ export const packedChannelSchema = {
},
description: {
type: 'string',
nullable: true, optional: false,
},
bannerUrl: {
type: 'string',
format: 'url',
nullable: true, optional: false,
},
isArchived: {
type: 'boolean',
optional: false, nullable: false,
},
notesCount: {
type: 'number',
nullable: false, optional: false,
},
usersCount: {
type: 'number',
nullable: false, optional: false,
},
isFollowing: {
type: 'boolean',
optional: true, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
optional: false, nullable: true,
},
userId: {
type: 'string',
nullable: true, optional: false,
format: 'id',
},
bannerUrl: {
type: 'string',
format: 'url',
nullable: true, optional: false,
},
pinnedNoteIds: {
type: 'array',
nullable: false, optional: false,
@ -72,6 +52,18 @@ export const packedChannelSchema = {
type: 'string',
optional: false, nullable: false,
},
isArchived: {
type: 'boolean',
optional: false, nullable: false,
},
usersCount: {
type: 'number',
nullable: false, optional: false,
},
notesCount: {
type: 'number',
nullable: false, optional: false,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
@ -80,5 +72,22 @@ export const packedChannelSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isFollowing: {
type: 'boolean',
optional: true, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
pinnedNotes: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
},
} as const;

View file

@ -44,13 +44,13 @@ export const packedClipSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
favoritedCount: {
type: 'number',
optional: false, nullable: false,
},
isFavorited: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;

View file

@ -74,7 +74,7 @@ export const packedDriveFileSchema = {
},
url: {
type: 'string',
optional: false, nullable: true,
optional: false, nullable: false,
format: 'url',
},
thumbnailUrl: {

View file

@ -21,6 +21,12 @@ export const packedDriveFolderSchema = {
type: 'string',
optional: false, nullable: false,
},
parentId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
foldersCount: {
type: 'number',
optional: true, nullable: false,
@ -29,12 +35,6 @@ export const packedDriveFolderSchema = {
type: 'number',
optional: true, nullable: false,
},
parentId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
parent: {
type: 'object',
optional: true, nullable: true,

View file

@ -79,6 +79,10 @@ export const packedFederationInstanceSchema = {
type: 'string',
optional: false, nullable: true,
},
isSilenced: {
type: 'boolean',
optional: false, nullable: false,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
@ -93,11 +97,6 @@ export const packedFederationInstanceSchema = {
type: 'string',
optional: false, nullable: true,
},
isSilenced: {
type: "boolean",
optional: false,
nullable: false,
},
infoUpdatedAt: {
type: 'string',
optional: false, nullable: true,

View file

@ -22,6 +22,16 @@ export const packedFlashSchema = {
optional: false, nullable: false,
format: 'date-time',
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
title: {
type: 'string',
optional: false, nullable: false,
@ -34,16 +44,6 @@ export const packedFlashSchema = {
type: 'string',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
likedCount: {
type: 'number',
optional: false, nullable: true,

View file

@ -22,16 +22,16 @@ export const packedFollowingSchema = {
optional: false, nullable: false,
format: 'id',
},
followee: {
type: 'object',
optional: true, nullable: false,
ref: 'UserDetailed',
},
followerId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
followee: {
type: 'object',
optional: true, nullable: false,
ref: 'UserDetailed',
},
follower: {
type: 'object',
optional: true, nullable: false,

View file

@ -22,14 +22,6 @@ export const packedGalleryPostSchema = {
optional: false, nullable: false,
format: 'date-time',
},
title: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: false,
@ -40,6 +32,14 @@ export const packedGalleryPostSchema = {
ref: 'UserLite',
optional: false, nullable: false,
},
title: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: true,
},
fileIds: {
type: 'array',
optional: true, nullable: false,
@ -70,5 +70,13 @@ export const packedGalleryPostSchema = {
type: 'boolean',
optional: false, nullable: false,
},
likedCount: {
type: 'number',
optional: false, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;

View file

@ -127,22 +127,26 @@ export const packedNoteSchema = {
channel: {
type: 'object',
optional: true, nullable: true,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: true,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
color: {
type: 'string',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
allowRenoteToExternal: {
type: 'boolean',
optional: false, nullable: false,
},
},
},

View file

@ -42,13 +42,9 @@ export const packedNotificationSchema = {
type: 'string',
optional: true, nullable: true,
},
choice: {
type: 'number',
optional: true, nullable: true,
},
invitation: {
type: 'object',
optional: true, nullable: true,
achievement: {
type: 'string',
optional: true, nullable: false,
},
body: {
type: 'string',
@ -81,14 +77,14 @@ export const packedNotificationSchema = {
required: ['user', 'reaction'],
},
},
},
users: {
type: 'array',
optional: true, nullable: true,
items: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
users: {
type: 'array',
optional: true, nullable: true,
items: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
},
},
} as const;

View file

@ -22,6 +22,32 @@ export const packedPageSchema = {
optional: false, nullable: false,
format: 'date-time',
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
content: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
},
},
variables: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
},
},
title: {
type: 'string',
optional: false, nullable: false,
@ -34,23 +60,47 @@ export const packedPageSchema = {
type: 'string',
optional: false, nullable: true,
},
content: {
type: 'array',
hideTitleWhenPinned: {
type: 'boolean',
optional: false, nullable: false,
},
variables: {
type: 'array',
alignCenter: {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
font: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
script: {
type: 'string',
optional: false, nullable: false,
},
eyeCatchingImageId: {
type: 'string',
optional: false, nullable: true,
},
eyeCatchingImage: {
type: 'object',
optional: false, nullable: true,
ref: 'DriveFile',
},
attachedFiles: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'DriveFile',
},
},
likedCount: {
type: 'number',
optional: false, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
} as const;

View file

@ -49,11 +49,6 @@ export const packedUserLiteSchema = {
nullable: false, optional: false,
format: 'id',
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
angle: {
type: 'number',
nullable: false, optional: true,
@ -62,19 +57,14 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
url: {
type: 'string',
format: 'url',
nullable: false, optional: false,
},
},
},
},
isAdmin: {
type: 'boolean',
nullable: false, optional: true,
default: false,
},
isModerator: {
type: 'boolean',
nullable: false, optional: true,
default: false,
},
isBot: {
type: 'boolean',
nullable: false, optional: true,
@ -83,12 +73,67 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
instance: {
type: 'object',
nullable: false, optional: true,
properties: {
name: {
type: 'string',
nullable: true, optional: false,
},
softwareName: {
type: 'string',
nullable: true, optional: false,
},
softwareVersion: {
type: 'string',
nullable: true, optional: false,
},
iconUrl: {
type: 'string',
nullable: true, optional: false,
},
faviconUrl: {
type: 'string',
nullable: true, optional: false,
},
themeColor: {
type: 'string',
nullable: true, optional: false,
},
},
},
emojis: {
type: 'object',
nullable: false, optional: false,
},
onlineStatus: {
type: 'string',
format: 'url',
nullable: true, optional: false,
nullable: false, optional: false,
enum: ['unknown', 'online', 'active', 'offline'],
},
badgeRoles: {
type: 'array',
nullable: false, optional: true,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
iconUrl: {
type: 'string',
nullable: true, optional: false,
},
displayOrder: {
type: 'number',
nullable: false, optional: false,
},
},
},
},
},
} as const;
@ -105,21 +150,18 @@ export const packedUserDetailedNotMeOnlySchema = {
format: 'uri',
nullable: true, optional: false,
},
movedToUri: {
movedTo: {
type: 'string',
format: 'uri',
nullable: true,
optional: false,
nullable: true, optional: false,
},
alsoKnownAs: {
type: 'array',
nullable: true,
optional: false,
nullable: true, optional: false,
items: {
type: 'string',
format: 'id',
nullable: false,
optional: false,
nullable: false, optional: false,
},
},
createdAt: {
@ -249,6 +291,11 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
ffVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
twoFactorEnabled: {
type: 'boolean',
nullable: false, optional: false,
@ -264,6 +311,57 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
default: false,
},
roles: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
id: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
name: {
type: 'string',
nullable: false, optional: false,
},
color: {
type: 'string',
nullable: true, optional: false,
},
iconUrl: {
type: 'string',
nullable: true, optional: false,
},
description: {
type: 'string',
nullable: false, optional: false,
},
isModerator: {
type: 'boolean',
nullable: false, optional: false,
},
isAdministrator: {
type: 'boolean',
nullable: false, optional: false,
},
displayOrder: {
type: 'number',
nullable: false, optional: false,
},
},
},
},
memo: {
type: 'string',
nullable: true, optional: false,
},
moderationNote: {
type: 'string',
nullable: false, optional: true,
},
//#region relations
isFollowing: {
type: 'boolean',
@ -297,10 +395,6 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: true,
},
memo: {
type: 'string',
nullable: false, optional: true,
},
notify: {
type: 'string',
nullable: false, optional: true,
@ -326,29 +420,37 @@ export const packedMeDetailedOnlySchema = {
nullable: true, optional: false,
format: 'id',
},
injectFeaturedNote: {
isModerator: {
type: 'boolean',
nullable: true, optional: false,
},
isAdmin: {
type: 'boolean',
nullable: true, optional: false,
},
injectFeaturedNote: {
type: 'boolean',
nullable: false, optional: false,
},
receiveAnnouncementEmail: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
alwaysMarkNsfw: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
autoSensitive: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
carefulBot: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
autoAcceptFollowed: {
type: 'boolean',
nullable: true, optional: false,
nullable: false, optional: false,
},
noCrawle: {
type: 'boolean',
@ -387,10 +489,23 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
unreadAnnouncements: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
ref: 'Announcement',
},
},
hasUnreadAntenna: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadChannel: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadNotification: {
type: 'boolean',
nullable: false, optional: false,
@ -415,6 +530,18 @@ export const packedMeDetailedOnlySchema = {
},
},
},
hardMutedWords: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
},
mutedInstances: {
type: 'array',
nullable: true, optional: false,
@ -429,12 +556,132 @@ export const packedMeDetailedOnlySchema = {
},
emailNotificationTypes: {
type: 'array',
nullable: true, optional: false,
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
achievements: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'object',
nullable: false, optional: false,
properties: {
name: {
type: 'string',
nullable: false, optional: false,
},
unlockedAt: {
type: 'number',
nullable: false, optional: false,
},
},
},
},
loggedInDays: {
type: 'number',
nullable: false, optional: false,
},
policies: {
type: 'object',
nullable: false, optional: false,
properties: {
gtlAvailable: {
type: 'boolean',
nullable: false, optional: false,
},
ltlAvailable: {
type: 'boolean',
nullable: false, optional: false,
},
canPublicNote: {
type: 'boolean',
nullable: false, optional: false,
},
canInvite: {
type: 'boolean',
nullable: false, optional: false,
},
inviteLimit: {
type: 'number',
nullable: false, optional: false,
},
inviteLimitCycle: {
type: 'number',
nullable: false, optional: false,
},
inviteExpirationTime: {
type: 'number',
nullable: false, optional: false,
},
canManageCustomEmojis: {
type: 'boolean',
nullable: false, optional: false,
},
canManageAvatarDecorations: {
type: 'boolean',
nullable: false, optional: false,
},
canSearchNotes: {
type: 'boolean',
nullable: false, optional: false,
},
canUseTranslator: {
type: 'boolean',
nullable: false, optional: false,
},
canHideAds: {
type: 'boolean',
nullable: false, optional: false,
},
driveCapacityMb: {
type: 'number',
nullable: false, optional: false,
},
alwaysMarkNsfw: {
type: 'boolean',
nullable: false, optional: false,
},
pinLimit: {
type: 'number',
nullable: false, optional: false,
},
antennaLimit: {
type: 'number',
nullable: false, optional: false,
},
wordMuteLimit: {
type: 'number',
nullable: false, optional: false,
},
webhookLimit: {
type: 'number',
nullable: false, optional: false,
},
clipLimit: {
type: 'number',
nullable: false, optional: false,
},
noteEachClipsLimit: {
type: 'number',
nullable: false, optional: false,
},
userListLimit: {
type: 'number',
nullable: false, optional: false,
},
userEachUserListsLimit: {
type: 'number',
nullable: false, optional: false,
},
rateLimitFactor: {
type: 'number',
nullable: false, optional: false,
},
},
},
//#region secrets
email: {
type: 'string',
@ -511,5 +758,13 @@ export const packedUserSchema = {
type: 'object',
ref: 'UserDetailed',
},
{
type: 'object',
ref: 'UserDetailedNotMe',
},
{
type: 'object',
ref: 'MeDetailed',
},
],
} as const;

View file

@ -126,7 +126,7 @@ export class SignupApiService {
code: invitationCode,
});
if (ticket == null) {
if (ticket == null || ticket.usedById != null) {
reply.code(400);
return;
}

View file

@ -22,7 +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 },
publishing: { type: 'boolean', default: null, nullable: true },
},
required: [],
} as const;
@ -37,8 +37,10 @@ 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) {
if (ps.publishing === true) {
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
} else if (ps.publishing === false) {
query.andWhere('ad.expiresAt <= :now', { now: new Date() }).orWhere('ad.startsAt > :now', { now: new Date() });
}
const ads = await query.limit(ps.limit).getMany();

View file

@ -267,6 +267,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
enableVerifymailApi: {
type: 'boolean',
optional: false, nullable: false,
},
verifymailAuthKey: {
type: 'string',
optional: false, nullable: true,
},
enableChartsForRemoteUser: {
type: 'boolean',
optional: false, nullable: false,
@ -421,6 +429,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
enableVerifymailApi: instance.enableVerifymailApi,
verifymailAuthKey: instance.verifymailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,

View file

@ -12,7 +12,8 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -70,7 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private noteReadService: NoteReadService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -85,12 +87,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAntenna);
}
this.antennasRepository.update(antenna.id, {
isActive: true,
lastUsedAt: new Date(),
});
// falseだった場合はアンテナの配信先が増えたことを通知したい
const needPublishEvent = !antenna.isActive;
let noteIds = await this.funoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
antenna.isActive = true;
antenna.lastUsedAt = new Date();
this.antennasRepository.update(antenna.id, antenna);
if (needPublishEvent) {
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
}
let noteIds = await this.fanoutTimelineService.get(`antennaTimeline:${antenna.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) {
return [];

View file

@ -12,9 +12,10 @@ 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 { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -69,15 +70,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isRangeSpecified = untilId != null && sinceId != null;
const serverSettings = await this.metaService.fetch();
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
@ -88,14 +92,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me);
if (isRangeSpecified || sinceId == null) {
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [
userIdsWhoMeMuting,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
]) : [new Set<string>()];
let noteIds = await this.funoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
let noteIds = await this.fanoutTimelineService.get(`channelTimeline:${channel.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length > 0) {

View file

@ -16,12 +16,9 @@ export const meta = {
requireCredential: false,
res: {
oneOf: [{
type: 'object',
ref: 'FederationInstance',
}, {
type: 'null',
}],
type: 'object',
optional: false, nullable: true,
ref: 'FederationInstance',
},
} as const;

View file

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository } from '@/models/_.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js';
import { FeaturedService } from '@/core/FeaturedService.js';
export const meta = {
tags: ['gallery'],
@ -27,25 +28,49 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {},
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
private galleryPostsRankingCache: string[] = [];
private galleryPostsRankingCacheLastFetchedAt = 0;
constructor(
@Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository,
private galleryPostEntityService: GalleryPostEntityService,
private featuredService: FeaturedService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.galleryPostsRepository.createQueryBuilder('post')
.andWhere('post.createdAt > :date', { date: new Date(Date.now() - (1000 * 60 * 60 * 24 * 3)) })
.andWhere('post.likedCount > 0')
.orderBy('post.likedCount', 'DESC');
let postIds: string[];
if (this.galleryPostsRankingCacheLastFetchedAt !== 0 && (Date.now() - this.galleryPostsRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
postIds = this.galleryPostsRankingCache;
} else {
postIds = await this.featuredService.getGalleryPostsRanking(100);
this.galleryPostsRankingCache = postIds;
this.galleryPostsRankingCacheLastFetchedAt = Date.now();
}
const posts = await query.limit(10).getMany();
postIds.sort((a, b) => a > b ? -1 : 1);
if (ps.untilId) {
postIds = postIds.filter(id => id < ps.untilId!);
}
postIds = postIds.slice(0, ps.limit);
if (postIds.length === 0) {
return [];
}
const query = this.galleryPostsRepository.createQueryBuilder('post')
.where('post.id IN (:...postIds)', { postIds: postIds });
const posts = await query.getMany();
return await this.galleryPostEntityService.packMany(posts, me);
});

View file

@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryLikesRepository, GalleryPostsRepository } from '@/models/_.js';
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository,
private featuredService: FeaturedService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
@ -88,6 +90,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id,
});
// ランキング更新
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
await this.featuredService.updateGalleryPostsRanking(post.id, 1);
}
this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1);
});
}

View file

@ -6,6 +6,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, GalleryLikesRepository } from '@/models/_.js';
import { FeaturedService, GALLERY_POSTS_RANKING_WINDOW } from '@/core/FeaturedService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@ -49,6 +51,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.galleryLikesRepository)
private galleryLikesRepository: GalleryLikesRepository,
private featuredService: FeaturedService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
@ -68,6 +73,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Delete like
await this.galleryLikesRepository.delete(exist.id);
// ランキング更新
if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) {
await this.featuredService.updateGalleryPostsRanking(post.id, -1);
}
this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1);
});
}

View file

@ -123,6 +123,11 @@ export const meta = {
},
} as const;
const muteWords = { type: 'array', items: { oneOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'string' }
] } } as const;
export const paramDef = {
type: 'object',
properties: {
@ -171,7 +176,8 @@ export const paramDef = {
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array' },
mutedWords: muteWords,
hardMutedWords: muteWords,
mutedInstances: { type: 'array', items: {
type: 'string',
} },
@ -234,16 +240,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
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.mutedWords !== undefined) {
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
// TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length;
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
const length = JSON.stringify(mutedWords).length;
if (length > limit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}
}
// validate regular expression syntax
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
const regexp = x.match(/^\/(.+)\/(.*)$/);
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
for (const mutedWord of mutedWords) {
if (typeof mutedWord !== "string") continue;
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
});
}
}
if (ps.mutedWords !== undefined) {
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
validateMuteWordRegex(ps.mutedWords);
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.hardMutedWords !== undefined) {
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
validateMuteWordRegex(ps.hardMutedWords);
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;

View file

@ -262,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote.channelId && renote.channelId !== ps.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneById(renote.channelId);
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルが無い
throw new ApiError(meta.errors.noSuchChannel);

View file

@ -64,16 +64,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
if (noteIds.length === 0) {
return [];
}
noteIds.sort((a, b) => a > b ? -1 : 1);
if (ps.untilId) {
noteIds = noteIds.filter(id => id < ps.untilId!);
}
noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) {
return [];
}
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')

View file

@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { CacheService } from '@/core/CacheService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MetaService } from '@/core/MetaService.js';
@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
private metaService: MetaService,
@ -120,20 +120,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let shouldFallbackToDb = false;
if (ps.withFiles) {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimelineWithFiles:${me.id}`,
'localTimelineWithFiles',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
} else if (ps.withReplies) {
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
} else {
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
const [htlNoteIds, ltlNoteIds] = await this.fanoutTimelineService.getMulti([
`homeTimeline:${me.id}`,
'localTimeline',
], untilId, sinceId);

View file

@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService,
private metaService: MetaService,
) {
@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let noteIds: string[];
if (ps.withFiles) {
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
noteIds = await this.fanoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
} else {
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
const [nonReplyNoteIds, replyNoteIds] = await this.fanoutTimelineService.getMulti([
'localTimeline',
'localTimelineWithReplies',
], untilId, sinceId);

View file

@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
@ -65,7 +65,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
private cacheService: CacheService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private userFollowingService: UserFollowingService,
private queryService: QueryService,
private metaService: MetaService,
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.cacheService.userBlockedCache.fetch(me.id),
]);
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
let redisTimeline: MiNote[] = [];

View file

@ -13,7 +13,7 @@ import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private activeUsersChart: ActiveUsersChart,
private cacheService: CacheService,
private idService: IdService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private queryService: QueryService,
private metaService: MetaService,
) {
@ -123,7 +123,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.cacheService.userBlockedCache.fetch(me.id),
]);
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
let noteIds = await this.fanoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
let redisTimeline: MiNote[] = [];

View file

@ -11,7 +11,7 @@ import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { IdService } from '@/core/IdService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return [];
}
let noteIds = await this.funoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
let noteIds = await this.fanoutTimelineService.get(`roleTimeline:${role.id}`, untilId, sinceId);
noteIds = noteIds.slice(0, ps.limit);
if (noteIds.length === 0) {

View file

@ -14,7 +14,8 @@ import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { QueryService } from '@/core/QueryService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -70,7 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
private cacheService: CacheService,
private idService: IdService,
private funoutTimelineService: FunoutTimelineService,
private fanoutTimelineService: FanoutTimelineService,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@ -78,7 +80,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const isRangeSpecified = untilId != null && sinceId != null;
const isSelf = me && (me.id === ps.userId);
if (isRangeSpecified || sinceId == null) {
const serverSettings = await this.metaService.fetch();
if (serverSettings.enableFanoutTimeline && (isRangeSpecified || sinceId == null)) {
const [
userIdsWhoMeMuting,
] = me ? await Promise.all([
@ -86,9 +90,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]) : [new Set<string>()];
const [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
this.funoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
ps.withReplies ? this.funoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
ps.withChannelNotes ? this.funoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
this.fanoutTimelineService.get(ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, untilId, sinceId),
ps.withReplies ? this.fanoutTimelineService.get(`userTimelineWithReplies:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
ps.withChannelNotes ? this.fanoutTimelineService.get(`userTimelineWithChannel:${ps.userId}`, untilId, sinceId) : Promise.resolve([]),
]);
let noteIds = Array.from(new Set([

View file

@ -4,7 +4,7 @@
*/
import type { Config } from '@/config.js';
import endpoints from '../endpoints.js';
import endpoints, { IEndpoint } from '../endpoints.js';
import { errors as basicErrors } from './errors.js';
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) {
schemas: schemas,
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'body',
name: 'i',
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
},
},
};
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
// 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
const errors = {} as any;
if (endpoint.meta.errors) {
@ -79,6 +80,13 @@ export function genOpenapiSpec(config: Config) {
schema.required = [...schema.required ?? [], 'file'];
}
if (schema.required && schema.required.length <= 0) {
// 空配列は許可されない
schema.required = undefined;
}
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = {
operationId: endpoint.name,
summary: endpoint.name,
@ -92,17 +100,19 @@ export function genOpenapiSpec(config: Config) {
} : {}),
...(endpoint.meta.requireCredential ? {
security: [{
ApiKeyAuth: [],
bearerAuth: [],
}],
} : {}),
requestBody: {
required: true,
content: {
[requestType]: {
schema,
...(hasBody ? {
requestBody: {
required: true,
content: {
[requestType]: {
schema,
},
},
},
},
} : {}),
responses: {
...(endpoint.meta.res ? {
'200': {
@ -118,6 +128,11 @@ export function genOpenapiSpec(config: Config) {
description: 'OK (without any results)',
},
}),
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
'204': {
description: 'OK (without any results)',
},
} : {}),
'400': {
description: 'Client error',
content: {
@ -190,6 +205,7 @@ export function genOpenapiSpec(config: Config) {
};
spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}),
post: info,
};
}

View file

@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema) {
const res: any = schema;
// optional, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema;
if (schema.type === 'object' && schema.properties) {
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない
res.required = required;
}
for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);

View file

@ -58,7 +58,7 @@ export class FeedService {
const feed = new Feed({
id: author.link,
title: `${author.name} (@${user.username}@${this.config.host})`,
updated: this.idService.parse(notes[0].id).date,
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
generator: 'Misskey',
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link,

View file

@ -93,7 +93,7 @@ describe('Webリソース', () => {
});
aliceChannel = await channel(alice, {});
bob = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
afterAll(async () => {
@ -152,6 +152,11 @@ describe('Webリソース', () => {
type,
}));
test('がGETできる。(ノートが存在しない場合でも。)', async () => await ok({
path: path(bob.username),
type,
}));
test('は存在しないユーザーはGETできない。', async () => await notOk({
path: path('nonexisting'),
status: 404,

View file

@ -168,6 +168,7 @@ describe('ユーザー', () => {
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
unreadAnnouncements: user.unreadAnnouncements,
mutedWords: user.mutedWords,
hardMutedWords: user.hardMutedWords,
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
notificationRecieveConfig: user.notificationRecieveConfig,