Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop

This commit is contained in:
syuilo 2023-04-13 08:43:15 +09:00
commit 62b6c4d09b
7 changed files with 82 additions and 38 deletions

View file

@ -26,10 +26,11 @@
- -
### Server ### Server
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io
- Misskey Webでのサーバーサイドエラー画面を改善 - Misskey Webでのサーバーサイドエラー画面を改善
- Misskey Webでのサーバーサイドエラーのログが残るように - Misskey Webでのサーバーサイドエラーのログが残るように
- ノート作成時のアンテナ追加パフォーマンスを改善 - ノート作成時のアンテナ追加パフォーマンスを改善
- フォローインポートなどでの大量のフォロー等操作をキューイングするように #10544 @nmkj-io - アンテナとロールTLのuntil/sinceプロパティが動くように
## 13.11.2 ## 13.11.2

View file

@ -13,6 +13,7 @@ import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { query } from '@/misc/prelude/url.js'; import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/server/api/stream/types.js';
@Injectable() @Injectable()
export class CustomEmojiService { export class CustomEmojiService {
@ -46,9 +47,9 @@ export class CustomEmojiService {
toRedisConverter: (value) => JSON.stringify(Array.from(value.values())), toRedisConverter: (value) => JSON.stringify(Array.from(value.values())),
fromRedisConverter: (value) => { fromRedisConverter: (value) => {
if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す) if (!Array.isArray(JSON.parse(value))) return undefined; // 古いバージョンの壊れたキャッシュが残っていることがある(そのうち消す)
return new Map(JSON.parse(value).map((x) => [x.name, { return new Map(JSON.parse(value).map((x: Serialized<Emoji>) => [x.name, {
...x, ...x,
updatedAt: new Date(x.updatedAt), updatedAt: x.updatedAt && new Date(x.updatedAt),
}])); }]));
}, },
}); });

View file

@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -44,6 +45,9 @@ export class UserFollowingService implements OnModuleInit {
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -411,7 +415,7 @@ export class UserFollowingService implements OnModuleInit {
} }
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
this.queueService.deliver(follower, content, followee.inbox, false); this.queueService.deliver(follower, content, followee.inbox, false);
} }
} }

View file

@ -6,7 +6,7 @@ import { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
import accepts from 'accepts'; import accepts from 'accepts';
import vary from 'vary'; import vary from 'vary';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/index.js'; import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/index.js';
import * as url from '@/misc/prelude/url.js'; import * as url from '@/misc/prelude/url.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@ -54,6 +54,9 @@ export class ActivityPubServerService {
@Inject(DI.followingsRepository) @Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository, private followingsRepository: FollowingsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
@ -205,22 +208,22 @@ export class ActivityPubServerService {
reply.code(400); reply.code(400);
return; return;
} }
const page = request.query.page === 'true'; const page = request.query.page === 'true';
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(), host: IsNull(),
}); });
if (user == null) { if (user == null) {
reply.code(404); reply.code(404);
return; return;
} }
//#region Check ff visibility //#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') { if (profile.ffVisibility === 'private') {
reply.code(403); reply.code(403);
reply.header('Cache-Control', 'public, max-age=30'); reply.header('Cache-Control', 'public, max-age=30');
@ -231,31 +234,31 @@ export class ActivityPubServerService {
return; return;
} }
//#endregion //#endregion
const limit = 10; const limit = 10;
const partOf = `${this.config.url}/users/${userId}/following`; const partOf = `${this.config.url}/users/${userId}/following`;
if (page) { if (page) {
const query = { const query = {
followerId: user.id, followerId: user.id,
} as FindOptionsWhere<Following>; } as FindOptionsWhere<Following>;
// カーソルが指定されている場合 // カーソルが指定されている場合
if (cursor) { if (cursor) {
query.id = LessThan(cursor); query.id = LessThan(cursor);
} }
// Get followings // Get followings
const followings = await this.followingsRepository.find({ const followings = await this.followingsRepository.find({
where: query, where: query,
take: limit + 1, take: limit + 1,
order: { id: -1 }, order: { id: -1 },
}); });
// 「次のページ」があるかどうか // 「次のページ」があるかどうか
const inStock = followings.length === limit + 1; const inStock = followings.length === limit + 1;
if (inStock) followings.pop(); if (inStock) followings.pop();
const renderedFollowees = await Promise.all(followings.map(following => this.apRendererService.renderFollowUser(following.followeeId))); const renderedFollowees = await Promise.all(followings.map(following => this.apRendererService.renderFollowUser(following.followeeId)));
const rendered = this.apRendererService.renderOrderedCollectionPage( const rendered = this.apRendererService.renderOrderedCollectionPage(
`${partOf}?${url.query({ `${partOf}?${url.query({
@ -269,7 +272,7 @@ export class ActivityPubServerService {
cursor: followings[followings.length - 1].id, cursor: followings[followings.length - 1].id,
})}` : undefined, })}` : undefined,
); );
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered)); return (this.apRendererService.addContext(rendered));
} else { } else {
@ -330,33 +333,33 @@ export class ActivityPubServerService {
reply.code(400); reply.code(400);
return; return;
} }
const untilId = request.query.until_id; const untilId = request.query.until_id;
if (untilId != null && typeof untilId !== 'string') { if (untilId != null && typeof untilId !== 'string') {
reply.code(400); reply.code(400);
return; return;
} }
const page = request.query.page === 'true'; const page = request.query.page === 'true';
if (countIf(x => x != null, [sinceId, untilId]) > 1) { if (countIf(x => x != null, [sinceId, untilId]) > 1) {
reply.code(400); reply.code(400);
return; return;
} }
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(), host: IsNull(),
}); });
if (user == null) { if (user == null) {
reply.code(404); reply.code(404);
return; return;
} }
const limit = 20; const limit = 20;
const partOf = `${this.config.url}/users/${userId}/outbox`; const partOf = `${this.config.url}/users/${userId}/outbox`;
if (page) { if (page) {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId: user.id }) .andWhere('note.userId = :userId', { userId: user.id })
@ -365,11 +368,11 @@ export class ActivityPubServerService {
.orWhere('note.visibility = \'home\''); .orWhere('note.visibility = \'home\'');
})) }))
.andWhere('note.localOnly = FALSE'); .andWhere('note.localOnly = FALSE');
const notes = await query.take(limit).getMany(); const notes = await query.take(limit).getMany();
if (sinceId) notes.reverse(); if (sinceId) notes.reverse();
const activities = await Promise.all(notes.map(note => this.packActivity(note))); const activities = await Promise.all(notes.map(note => this.packActivity(note)));
const rendered = this.apRendererService.renderOrderedCollectionPage( const rendered = this.apRendererService.renderOrderedCollectionPage(
`${partOf}?${url.query({ `${partOf}?${url.query({
@ -387,7 +390,7 @@ export class ActivityPubServerService {
until_id: notes[notes.length - 1].id, until_id: notes[notes.length - 1].id,
})}` : undefined, })}` : undefined,
); );
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(rendered)); return (this.apRendererService.addContext(rendered));
} else { } else {
@ -457,7 +460,7 @@ export class ActivityPubServerService {
// note // note
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOneBy({
id: request.params.note, id: request.params.note,
visibility: In(['public', 'home']), visibility: In(['public', 'home']),
@ -639,6 +642,41 @@ export class ActivityPubServerService {
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee))); return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
}); });
// follow
fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => {
// This may be used before the follow is completed, so we do not
// check if the following exists and only check if the follow request exists.
const followRequest = await this.followRequestsRepository.findOneBy({
id: request.params.followRequestId,
});
if (followRequest == null) {
reply.code(404);
return;
}
const [follower, followee] = await Promise.all([
this.usersRepository.findOneBy({
id: followRequest.followerId,
host: IsNull(),
}),
this.usersRepository.findOneBy({
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
reply.code(404);
return;
}
reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)));
});
done(); done();
} }
} }

View file

@ -76,18 +76,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchAntenna); throw new ApiError(meta.errors.noSuchAntenna);
} }
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const noteIdsRes = await this.redisClient.xrevrange( const noteIdsRes = await this.redisClient.xrevrange(
`antennaTimeline:${antenna.id}`, `antennaTimeline:${antenna.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-', ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit); 'COUNT', limit);
if (noteIdsRes.length === 0) { if (noteIdsRes.length === 0) {
return []; return [];
} }
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) { if (noteIds.length === 0) {
return []; return [];

View file

@ -71,18 +71,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchRole); throw new ApiError(meta.errors.noSuchRole);
} }
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const noteIdsRes = await this.redisClient.xrevrange( const noteIdsRes = await this.redisClient.xrevrange(
`roleTimeline:${role.id}`, `roleTimeline:${role.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-', ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit); 'COUNT', limit);
if (noteIdsRes.length === 0) { if (noteIdsRes.length === 0) {
return []; return [];
} }
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId); const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) { if (noteIds.length === 0) {
return []; return [];

View file

@ -172,7 +172,7 @@ type EventUnionFromDictionary<
> = U[keyof U]; > = U[keyof U];
// redis通すとDateのインスタンスはstringに変換されるので // redis通すとDateのインスタンスはstringに変換されるので
type Serialized<T> = { export type Serialized<T> = {
[K in keyof T]: [K in keyof T]:
T[K] extends Date T[K] extends Date
? string ? string