Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # package.json # packages/backend/src/server/api/stream/channels/hybrid-timeline.ts # packages/frontend/package.json
This commit is contained in:
commit
cc5037cb85
28 changed files with 886 additions and 740 deletions
16
packages/backend/migration/1698041201306-enable-ftt.js
Normal file
16
packages/backend/migration/1698041201306-enable-ftt.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class EnableFtt1698041201306 {
|
||||
name = 'EnableFtt1698041201306'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimeline" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimeline"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"start": "node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
"build": "swc src -d built -D",
|
||||
"watch:swc": "swc src -d built -D -w",
|
||||
|
|
@ -76,9 +77,9 @@
|
|||
"@nestjs/testing": "10.2.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "8.3.2",
|
||||
"@sinonjs/fake-timers": "11.2.1",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.93",
|
||||
"@swc/core": "1.3.94",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
"nanoid": "5.0.2",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.6",
|
||||
"nodemailer": "6.9.7",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
|
|
@ -142,7 +143,7 @@
|
|||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.20.3",
|
||||
"re2": "1.20.4",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
|
|
@ -155,7 +156,7 @@
|
|||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.21.12",
|
||||
"systeminformation": "5.21.13",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.8",
|
||||
|
|
@ -216,7 +217,7 @@
|
|||
"@typescript-eslint/parser": "6.8.0",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.51.0",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"execa": "8.0.1",
|
||||
"jest": "29.7.0",
|
||||
|
|
|
|||
|
|
@ -77,7 +77,13 @@ export interface MainEventTypes {
|
|||
unreadAntenna: MiAntenna;
|
||||
readAllAnnouncements: undefined;
|
||||
myTokenRegenerated: undefined;
|
||||
signin: MiSignin;
|
||||
signin: {
|
||||
id: MiSignin['id'];
|
||||
createdAt: string;
|
||||
ip: string;
|
||||
headers: Record<string, any>;
|
||||
success: boolean;
|
||||
};
|
||||
registryUpdated: {
|
||||
scope?: string[];
|
||||
key: string;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { SearchService } from '@/core/SearchService.js';
|
|||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
|
@ -216,6 +217,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private activeUsersChart: ActiveUsersChart,
|
||||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) { }
|
||||
|
||||
@bindThis
|
||||
|
|
@ -292,6 +294,18 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
|
||||
if (data.renote.userHost === null) {
|
||||
if (data.renote.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返信対象がpublicではないならhomeにする
|
||||
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
|
||||
data.visibility = 'home';
|
||||
|
|
@ -825,6 +839,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
|
||||
const meta = await this.metaService.fetch();
|
||||
if (!meta.enableFanoutTimeline) return;
|
||||
|
||||
const r = this.redisForTimelines.pipeline();
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class QueryService {
|
|||
) {
|
||||
}
|
||||
|
||||
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
|
||||
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
|
||||
if (sinceId && untilId) {
|
||||
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
|
||||
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import { Injectable } from '@nestjs/common';
|
|||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiSignin } from '@/models/Signin.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class SigninEntityService {
|
||||
constructor(
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -18,7 +20,13 @@ export class SigninEntityService {
|
|||
public async pack(
|
||||
src: MiSignin,
|
||||
) {
|
||||
return src;
|
||||
return {
|
||||
id: src.id,
|
||||
createdAt: this.idService.parse(src.id).date.toISOString(),
|
||||
ip: src.ip,
|
||||
headers: src.headers,
|
||||
success: src.success,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -504,6 +504,11 @@ export class MiMeta {
|
|||
})
|
||||
public preservedUsernames: string[];
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public enableFanoutTimeline: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 300,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -106,11 +106,11 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
silencedHosts: {
|
||||
type: "array",
|
||||
type: 'array',
|
||||
optional: true,
|
||||
nullable: false,
|
||||
items: {
|
||||
type: "string",
|
||||
type: 'string',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
|
|
@ -291,6 +291,10 @@ export const meta = {
|
|||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableFanoutTimeline: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
perLocalUserUserTimelineCacheMax: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
|
|
@ -423,6 +427,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
enableIdenticonGeneration: instance.enableIdenticonGeneration,
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
manifestJsonOverride: instance.manifestJsonOverride,
|
||||
enableFanoutTimeline: instance.enableFanoutTimeline,
|
||||
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export const paramDef = {
|
|||
serverRules: { type: 'array', items: { type: 'string' } },
|
||||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||
manifestJsonOverride: { type: 'string' },
|
||||
enableFanoutTimeline: { type: 'boolean' },
|
||||
perLocalUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perUserHomeTimelineCacheMax: { type: 'integer' },
|
||||
|
|
@ -491,6 +492,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.manifestJsonOverride = ps.manifestJsonOverride;
|
||||
}
|
||||
|
||||
if (ps.enableFanoutTimeline !== undefined) {
|
||||
set.enableFanoutTimeline = ps.enableFanoutTimeline;
|
||||
}
|
||||
|
||||
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
|
||||
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -75,6 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private funoutTimelineService: FunoutTimelineService,
|
||||
private queryService: QueryService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
|
@ -85,163 +88,200 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.stlDisabled);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
if (ps.withFiles) {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.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([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
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');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere(new Brackets(qb => {
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
} else {
|
||||
qb.where('note.userId = :meId', { meId: me.id });
|
||||
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
}
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.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([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
if (!shouldFallbackToDb) {
|
||||
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');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(ps: {
|
||||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
includeMyRenotes: boolean,
|
||||
includeRenotedMyNotes: boolean,
|
||||
includeLocalRenotes: boolean,
|
||||
withFiles: boolean,
|
||||
withReplies: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => {
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
} else {
|
||||
qb.where('note.userId = :meId', { meId: me.id });
|
||||
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
|
||||
}
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -69,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private cacheService: CacheService,
|
||||
private funoutTimelineService: FunoutTimelineService,
|
||||
private queryService: QueryService,
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
|
@ -79,112 +82,140 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.ltlDisabled);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
let noteIds: string[];
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||
|
||||
if (ps.withFiles) {
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
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');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
let noteIds: string[];
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
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');
|
||||
|
||||
let timeline = await query.getMany();
|
||||
|
||||
timeline = timeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
}, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(ps: {
|
||||
sinceId: string | null,
|
||||
untilId: string | null,
|
||||
limit: number,
|
||||
withFiles: boolean,
|
||||
withReplies: boolean,
|
||||
}, me: MiLocalUser | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId)
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
|
@ -63,144 +65,171 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private funoutTimelineService: FunoutTimelineService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private queryService: QueryService,
|
||||
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 [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
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');
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let timeline = await query.getMany();
|
||||
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');
|
||||
|
||||
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;
|
||||
let timeline = await query.getMany();
|
||||
|
||||
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 (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
}
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
// TODO: フィルタした結果件数が足りなかった場合の対応
|
||||
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
timeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
|
||||
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||
} else {
|
||||
query.andWhere('note.userId = :meId', { meId: me.id });
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
}, me);
|
||||
}
|
||||
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
} else {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
}, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; }, me: MiLocalUser) {
|
||||
const followees = await this.userFollowingService.getFollowees(me.id);
|
||||
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.channelId IS NULL')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
if (followees.length > 0) {
|
||||
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
|
||||
|
||||
query.andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
|
||||
} else {
|
||||
query.andWhere('note.userId = :meId', { meId: me.id });
|
||||
}
|
||||
|
||||
query.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');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
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)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,11 +59,15 @@ class HomeTimelineChannel extends Channel {
|
|||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.following[note.userId]?.withReplies) {
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
if (this.following[note.userId]?.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
|
||||
} else {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
|
|
|||
|
|
@ -72,11 +72,16 @@ class HybridTimelineChannel extends Channel {
|
|||
|
||||
// Ignore notes from instances the user has muted
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return;
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
|
||||
} else {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
|
|
|||
|
|
@ -90,11 +90,15 @@ class UserListChannel extends Channel {
|
|||
if (!note.visibleUserIds!.includes(this.user!.id)) return;
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
if (this.membershipsMap[note.userId]?.withReplies) {
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
|
||||
} else {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
|
|
|
|||
|
|
@ -159,6 +159,10 @@ describe('Streaming', () => {
|
|||
});
|
||||
*/
|
||||
|
||||
test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => {
|
||||
// TODO
|
||||
});
|
||||
|
||||
test('フォローしていないユーザーの投稿は流れない', async () => {
|
||||
const fired = await waitFire(
|
||||
kyoko, 'homeTimeline', // kyoko:home
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ describe('ActivityPub', () => {
|
|||
const metaInitial = {
|
||||
cacheRemoteFiles: true,
|
||||
cacheRemoteSensitiveFiles: true,
|
||||
enableFanoutTimeline: true,
|
||||
perUserHomeTimelineCacheMax: 100,
|
||||
perLocalUserUserTimelineCacheMax: 100,
|
||||
perRemoteUserUserTimelineCacheMax: 100,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue