Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # package.json # packages/frontend/src/components/MkEmojiPicker.vue # packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts # packages/frontend/src/pages/about.emojis.vue # packages/frontend/src/pages/custom-emojis-manager.vue
This commit is contained in:
commit
7e155917e7
62 changed files with 1629 additions and 728 deletions
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -26,7 +26,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
|
|
@ -53,9 +52,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
|
@ -150,13 +146,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
fromRedisConverter: (value) => JSON.parse(value),
|
||||
});
|
||||
|
||||
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
// NOTE: チャンネルのフォロー状況キャッシュはChannelFollowingServiceで行っている
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
|
@ -221,7 +211,6 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.userBlockedCache.dispose();
|
||||
this.renoteMutingsCache.dispose();
|
||||
this.userFollowingsCache.dispose();
|
||||
this.userFollowingChannelsCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
104
packages/backend/src/core/ChannelFollowingService.ts
Normal file
104
packages/backend/src/core/ChannelFollowingService.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ChannelFollowingsRepository } from '@/models/_.js';
|
||||
import { MiChannel } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelFollowingService implements OnModuleInit {
|
||||
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.channelFollowingsRepository.find({
|
||||
where: { followerId: key },
|
||||
select: ['followeeId'],
|
||||
}).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(
|
||||
requestUser: MiLocalUser,
|
||||
targetChannel: MiChannel,
|
||||
): Promise<void> {
|
||||
await this.channelFollowingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
followerId: requestUser.id,
|
||||
followeeId: targetChannel.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('followChannel', {
|
||||
userId: requestUser.id,
|
||||
channelId: targetChannel.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unfollow(
|
||||
requestUser: MiLocalUser,
|
||||
targetChannel: MiChannel,
|
||||
): Promise<void> {
|
||||
await this.channelFollowingsRepository.delete({
|
||||
followerId: requestUser.id,
|
||||
followeeId: targetChannel.id,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('unfollowChannel', {
|
||||
userId: requestUser.id,
|
||||
channelId: targetChannel.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'followChannel': {
|
||||
this.userFollowingChannelsCache.refresh(body.userId);
|
||||
break;
|
||||
}
|
||||
case 'unfollowChannel': {
|
||||
this.userFollowingChannelsCache.delete(body.userId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.userFollowingChannelsCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ import { SearchService } from './SearchService.js';
|
|||
import { ClipService } from './ClipService.js';
|
||||
import { FeaturedService } from './FeaturedService.js';
|
||||
import { FunoutTimelineService } from './FunoutTimelineService.js';
|
||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
|
|
@ -193,6 +194,7 @@ const $SearchService: Provider = { provide: 'SearchService', useExisting: Search
|
|||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
|
||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
|
|
@ -327,6 +329,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ClipService,
|
||||
FeaturedService,
|
||||
FunoutTimelineService,
|
||||
ChannelFollowingService,
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
|
|
@ -454,6 +457,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ClipService,
|
||||
$FeaturedService,
|
||||
$FunoutTimelineService,
|
||||
$ChannelFollowingService,
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
|
|
@ -582,6 +586,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ClipService,
|
||||
FeaturedService,
|
||||
FunoutTimelineService,
|
||||
ChannelFollowingService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
|
|
@ -708,6 +713,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ClipService,
|
||||
$FeaturedService,
|
||||
$FunoutTimelineService,
|
||||
$ChannelFollowingService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class FeaturedService {
|
|||
`${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES');
|
||||
redisPipeline.zrange(
|
||||
`${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES');
|
||||
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => r[1] as string[]) : [[], []]);
|
||||
const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => (r[1] ?? []) as string[]) : [[], []]);
|
||||
|
||||
const ranking = new Map<string, number>();
|
||||
for (let i = 0; i < currentRankingResult.length; i += 2) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/Connection.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
|
|
@ -39,6 +40,7 @@ export class StreamingApiServerService {
|
|||
private channelsService: ChannelsService,
|
||||
private notificationService: NotificationService,
|
||||
private usersService: UserService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +95,7 @@ export class StreamingApiServerService {
|
|||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
this.channelFollowingService,
|
||||
user, app,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { ChannelsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -41,11 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
|
|
@ -56,11 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await this.channelFollowingsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
followerId: me.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
await this.channelFollowingService.follow(me, channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository } from '@/models/_.js';
|
||||
import type { ChannelsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -40,9 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.channelsRepository)
|
||||
private channelsRepository: ChannelsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const channel = await this.channelsRepository.findOneBy({
|
||||
|
|
@ -53,10 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
|
||||
await this.channelFollowingsRepository.delete({
|
||||
followerId: me.id,
|
||||
followeeId: channel.id,
|
||||
});
|
||||
await this.channelFollowingService.unfollow(me, channel);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
|
@ -63,16 +64,18 @@ export const paramDef = {
|
|||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private avatarDecorationService: AvatarDecorationService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const decorations = await this.avatarDecorationService.getAll(true);
|
||||
const allRoles = await this.roleService.getRoles();
|
||||
|
||||
return decorations.map(decoration => ({
|
||||
id: decoration.id,
|
||||
name: decoration.name,
|
||||
description: decoration.description,
|
||||
url: decoration.url,
|
||||
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration,
|
||||
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
tokenId: { type: 'string', format: 'misskey:id' },
|
||||
token: { type: 'string' },
|
||||
},
|
||||
required: ['tokenId'],
|
||||
anyOf: [
|
||||
{ required: ['tokenId'] },
|
||||
{ required: ['token'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -29,13 +33,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private accessTokensRepository: AccessTokensRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
||||
if (ps.tokenId) {
|
||||
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
||||
|
||||
if (tokenExist) {
|
||||
await this.accessTokensRepository.delete({
|
||||
id: ps.tokenId,
|
||||
userId: me.id,
|
||||
});
|
||||
if (tokenExist) {
|
||||
await this.accessTokensRepository.delete({
|
||||
id: ps.tokenId,
|
||||
userId: me.id,
|
||||
});
|
||||
}
|
||||
} else if (ps.token) {
|
||||
const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } });
|
||||
|
||||
if (tokenExist) {
|
||||
await this.accessTokensRepository.delete({
|
||||
token: ps.token,
|
||||
userId: me.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository, FollowingsRepository, MiNote } from '@/models/_.js';
|
||||
import type { NotesRepository, FollowingsRepository, MiNote, ChannelFollowingsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
|
|
@ -69,6 +69,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleService: RoleService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
|
|
@ -131,6 +134,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
|
|
@ -141,9 +146,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -159,15 +164,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
|
|
@ -206,6 +211,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withReplies: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
const followingChannels = await this.channelFollowingsRepository.find({
|
||||
where: {
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => {
|
||||
|
|
@ -224,6 +234,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (followingChannels.length > 0) {
|
||||
const followingChannelIds = followingChannels.map(x => x.followeeId);
|
||||
|
||||
query.andWhere('note.channelId IN (:...followingChannelIds) OR note.channelId IS NULL', { followingChannelIds });
|
||||
} else {
|
||||
query.andWhere('note.channelId IS NULL');
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
|
|
@ -120,9 +122,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -139,17 +141,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
|
@ -89,6 +89,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
|
|
@ -99,9 +101,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -120,15 +122,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
|
|
@ -138,6 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -149,12 +152,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; }, me: MiLocalUser) {
|
||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; }, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
|
||||
//#region Construct query
|
||||
|
|
@ -222,6 +226,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { MiNote, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
|
@ -13,7 +13,9 @@ 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 { QueryService } from '@/core/QueryService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'lists'],
|
||||
|
|
@ -70,11 +72,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
private cacheService: CacheService,
|
||||
private idService: IdService,
|
||||
private funoutTimelineService: FunoutTimelineService,
|
||||
private queryService: QueryService,
|
||||
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
|
@ -102,44 +109,129 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
if (redisTimeline.length > 0) {
|
||||
this.activeUsersChart.read(me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど投稿者自身への返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど自分宛ての返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけどwithRepliesがtrueの場合
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('userListMemberships.withReplies = true');
|
||||
}));
|
||||
}));
|
||||
|
||||
let timeline = await query.getMany();
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
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 != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
|
@ -42,6 +43,7 @@ export default class Connection {
|
|||
private noteReadService: NoteReadService,
|
||||
private notificationService: NotificationService,
|
||||
private cacheService: CacheService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
|
||||
user: MiUser | null | undefined,
|
||||
token: MiAccessToken | null | undefined,
|
||||
|
|
@ -56,7 +58,7 @@ export default class Connection {
|
|||
const [userProfile, following, followingChannels, userIdsWhoMeMuting, userIdsWhoBlockingMe, userIdsWhoMeMutingRenotes] = await Promise.all([
|
||||
this.cacheService.userProfileCache.fetch(this.user.id),
|
||||
this.cacheService.userFollowingsCache.fetch(this.user.id),
|
||||
this.cacheService.userFollowingChannelsCache.fetch(this.user.id),
|
||||
this.channelFollowingService.userFollowingChannelsCache.fetch(this.user.id),
|
||||
this.cacheService.userMutingsCache.fetch(this.user.id),
|
||||
this.cacheService.userBlockedCache.fetch(this.user.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(this.user.id),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue