merge: Expand Mandatory CW feature and fixup block/mute/silence features (resolves #809, #910, #912, #943, #1064, #1142, and #1186) (!1148)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1148 Closes #809, #910, #912, #943, #1064, #1142, and #1186 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
741e612508
125 changed files with 3195 additions and 1338 deletions
|
|
@ -34,6 +34,8 @@ export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decor
|
|||
export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js';
|
||||
export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js';
|
||||
export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js';
|
||||
export * as 'admin/cw-instance' from './endpoints/admin/cw-instance.js';
|
||||
export * as 'admin/cw-note' from './endpoints/admin/cw-note.js';
|
||||
export * as 'admin/cw-user' from './endpoints/admin/cw-user.js';
|
||||
export * as 'admin/decline-user' from './endpoints/admin/decline-user.js';
|
||||
export * as 'admin/delete-account' from './endpoints/admin/delete-account.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:cw-instance',
|
||||
|
||||
res: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: { type: 'string' },
|
||||
cw: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['host', 'cw'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private readonly moderationLogService: ModerationLogService,
|
||||
private readonly federatedInstanceService: FederatedInstanceService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const instance = await this.federatedInstanceService.fetchOrRegister(ps.host);
|
||||
|
||||
// Collapse empty strings to null
|
||||
const newCW = ps.cw?.trim() || null;
|
||||
const oldCW = instance.mandatoryCW;
|
||||
|
||||
// Skip if there's nothing to do
|
||||
if (oldCW === newCW) return;
|
||||
|
||||
// This synchronizes caches automatically
|
||||
await this.federatedInstanceService.update(instance.id, { mandatoryCW: newCW });
|
||||
|
||||
await this.moderationLogService.log(me, 'setMandatoryCWForInstance', {
|
||||
newCW,
|
||||
oldCW,
|
||||
host: ps.host,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
112
packages/backend/src/server/api/endpoints/admin/cw-note.ts
Normal file
112
packages/backend/src/server/api/endpoints/admin/cw-note.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { ChannelsRepository, DriveFilesRepository, MiNote, NotesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { NoteEditService, Option } from '@/core/NoteEditService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:cw-note',
|
||||
|
||||
res: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', format: 'misskey:id' },
|
||||
cw: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['noteId', 'cw'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private readonly notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private readonly driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.channelsRepository)
|
||||
private readonly channelsRepository: ChannelsRepository,
|
||||
|
||||
private readonly noteEditService: NoteEditService,
|
||||
private readonly moderationLogService: ModerationLogService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.notesRepository.findOneOrFail({
|
||||
where: { id: ps.noteId },
|
||||
relations: { reply: true, renote: true, channel: true },
|
||||
});
|
||||
const user = await this.cacheService.findUserById(note.userId);
|
||||
|
||||
// Collapse empty strings to null
|
||||
const newCW = ps.cw?.trim() || null;
|
||||
const oldCW = note.mandatoryCW;
|
||||
|
||||
// Skip if there's nothing to do
|
||||
if (oldCW === newCW) return;
|
||||
|
||||
// TODO remove this after merging hazelnoot/fix-note-edit-logic.
|
||||
// Until then, we have to ensure that everything is populated just like it would be from notes/edit.ts.
|
||||
// Otherwise forcing a CW will erase everything else in the note.
|
||||
// After merging remove all the "createUpdate" stuff and just pass "{ mandatoryCW: newCW }" into noteEditService.edit().
|
||||
const update = await this.createUpdate(note, newCW);
|
||||
await this.noteEditService.edit(user, note.id, update);
|
||||
|
||||
await this.moderationLogService.log(me, 'setMandatoryCWForNote', {
|
||||
newCW,
|
||||
oldCW,
|
||||
noteId: note.id,
|
||||
noteUserId: user.id,
|
||||
noteUserUsername: user.username,
|
||||
noteUserHost: user.host,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Note: user must be fetched with reply, renote, and channel relations populated
|
||||
private async createUpdate(note: MiNote, newCW: string | null) {
|
||||
// This is based on the call to NoteEditService.edit from notes/edit endpoint.
|
||||
// noinspection ES6MissingAwait
|
||||
return await awaitAll<Option>({
|
||||
// Preserve these from original note
|
||||
files: note.fileIds.length > 0
|
||||
? this.driveFilesRepository.findBy({ id: In(note.fileIds) }) : null,
|
||||
poll: undefined,
|
||||
text: undefined,
|
||||
cw: undefined,
|
||||
reply: note.reply
|
||||
?? (note.replyId ? this.notesRepository.findOneByOrFail({ id: note.replyId }) : null),
|
||||
renote: note.renote
|
||||
?? (note.renoteId ? this.notesRepository.findOneByOrFail({ id: note.renoteId }) : null),
|
||||
localOnly: note.localOnly,
|
||||
reactionAcceptance: note.reactionAcceptance,
|
||||
visibility: note.visibility,
|
||||
visibleUsers: note.visibleUserIds.length > 0
|
||||
? this.cacheService.getUsers(note.visibleUserIds).then(us => Array.from(us.values())) : null,
|
||||
channel: note.channel ?? (note.channelId ? this.channelsRepository.findOneByOrFail({ id: note.channelId }) : null),
|
||||
apMentions: undefined,
|
||||
apHashtags: undefined,
|
||||
apEmojis: undefined,
|
||||
|
||||
// But override the mandatory CW!
|
||||
mandatoryCW: newCW,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:cw-user',
|
||||
|
||||
res: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
@ -41,21 +43,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
|
||||
// Skip if there's nothing to do
|
||||
if (user.mandatoryCW === ps.cw) return;
|
||||
// Collapse empty strings to null
|
||||
const newCW = ps.cw?.trim() || null;
|
||||
const oldCW = user.mandatoryCW;
|
||||
|
||||
await this.usersRepository.update(ps.userId, {
|
||||
// Collapse empty strings to null
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
mandatoryCW: ps.cw || null,
|
||||
});
|
||||
// Skip if there's nothing to do
|
||||
if (oldCW === newCW) return;
|
||||
|
||||
await this.usersRepository.update(ps.userId, { mandatoryCW: newCW });
|
||||
|
||||
// Synchronize caches and other processes
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
|
||||
const evt = user.host == null ? 'localUserUpdated' : 'remoteUserUpdated';
|
||||
this.globalEventService.publishInternalEvent(evt, { id: ps.userId });
|
||||
|
||||
await this.moderationLogService.log(me, 'setMandatoryCW', {
|
||||
newCW: ps.cw,
|
||||
oldCW: user.mandatoryCW,
|
||||
newCW,
|
||||
oldCW,
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export const paramDef = {
|
|||
properties: {
|
||||
host: { type: 'string' },
|
||||
isSuspended: { type: 'boolean' },
|
||||
isNSFW: { type: 'boolean' },
|
||||
rejectReports: { type: 'boolean' },
|
||||
moderationNote: { type: 'string' },
|
||||
rejectQuotes: { type: 'boolean' },
|
||||
|
|
@ -58,7 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
await this.federatedInstanceService.update(instance.id, {
|
||||
suspensionState,
|
||||
isNSFW: ps.isNSFW,
|
||||
rejectReports: ps.rejectReports,
|
||||
rejectQuotes: ps.rejectQuotes,
|
||||
moderationNote: ps.moderationNote,
|
||||
|
|
@ -78,14 +76,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
}
|
||||
|
||||
if (ps.isNSFW != null && instance.isNSFW !== ps.isNSFW) {
|
||||
const message = ps.rejectReports ? 'setRemoteInstanceNSFW' : 'unsetRemoteInstanceNSFW';
|
||||
this.moderationLogService.log(me, message, {
|
||||
id: instance.id,
|
||||
host: instance.host,
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.rejectReports != null && instance.rejectReports !== ps.rejectReports) {
|
||||
const message = ps.rejectReports ? 'rejectRemoteInstanceReports' : 'acceptRemoteInstanceReports';
|
||||
this.moderationLogService.log(me, message, {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -107,8 +107,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
|
||||
}
|
||||
|
||||
const threadMutings = me ? await this.cacheService.threadMutingsCache.fetch(me.id) : null;
|
||||
|
||||
return await this.fanoutTimelineEndpointService.timeline({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -122,13 +120,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
dbFallback: async (untilId, sinceId, limit) => {
|
||||
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me);
|
||||
},
|
||||
noteFilter: note => {
|
||||
if (threadMutings?.has(note.threadId ?? note.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
|
||||
// Fetch file
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
id: ps.fileId,
|
||||
userId: await this.roleService.isModerator(me) ? undefined : me.id,
|
||||
userId: isModerator ? undefined : me.id,
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
|
|
@ -90,16 +92,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.limit(ps.limit);
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (!isModerator) {
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
const notes = await query.getMany();
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me, {
|
||||
detail: true,
|
||||
skipHide: isModerator,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
|
@ -23,6 +25,19 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
gtlDisabled: {
|
||||
message: 'Global timeline has been disabled.',
|
||||
code: 'GTL_DISABLED',
|
||||
id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b',
|
||||
},
|
||||
ltlDisabled: {
|
||||
message: 'Local timeline has been disabled.',
|
||||
code: 'LTL_DISABLED',
|
||||
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
|
||||
},
|
||||
},
|
||||
|
||||
// 120 calls per minute
|
||||
// 200 ms between calls
|
||||
limit: {
|
||||
|
|
@ -55,8 +70,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private readonly roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
if (!ps.local && !policies.gtlAvailable) {
|
||||
throw new ApiError(meta.errors.gtlDisabled);
|
||||
}
|
||||
if (ps.local && !policies.ltlAvailable) {
|
||||
throw new ApiError(meta.errors.ltlDisabled);
|
||||
}
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
|
|
@ -68,7 +92,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit);
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) {
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
|
|
@ -77,10 +100,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
if (ps.local) {
|
||||
query.andWhere('note.userHost IS NULL');
|
||||
} else {
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
}
|
||||
|
||||
if (ps.reply !== undefined) {
|
||||
query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
|
||||
if (ps.reply) {
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
} else if (ps.reply === false) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.renote !== undefined) {
|
||||
|
|
|
|||
|
|
@ -93,9 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoin('(select "host" from "instance" where "isBubbled" = true)', 'bubbleInstance', '"bubbleInstance"."host" = "note"."userHost"')
|
||||
.andWhere('"bubbleInstance" IS NOT NULL');
|
||||
this.queryService
|
||||
.leftJoinInstance(query, 'note.userInstance', 'userInstance', '"userInstance"."host" = "bubbleInstance"."host"');
|
||||
.leftJoin(query, 'note.userInstance', 'userInstance');
|
||||
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
// 10 calls per 5 seconds
|
||||
// Up to 25 calls, then 4 / second
|
||||
limit: {
|
||||
duration: 1000 * 5,
|
||||
max: 10,
|
||||
type: 'bucket',
|
||||
size: 25,
|
||||
dripRate: 250,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { NoteCreateService } from '@/core/NoteCreateService.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -261,6 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.text && ps.text.length > this.config.maxNoteLength) {
|
||||
|
|
@ -303,7 +305,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (isRenote(renote) && !isQuote(renote)) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(renote, me.id)) {
|
||||
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(renote, me)).accessible) {
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +353,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
|
||||
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { NoteEditService } from '@/core/NoteEditService.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -311,6 +312,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private noteEditService: NoteEditService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.text && ps.text.length > this.config.maxNoteLength) {
|
||||
|
|
@ -408,7 +410,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
|
||||
} else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||
|
|
|
|||
|
|
@ -102,14 +102,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return [];
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]) : [new Set<string>(), new Set<string>()];
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
|
|
@ -120,15 +112,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('note.channel', 'channel')
|
||||
.andWhere('user.isExplorable = TRUE');
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
}
|
||||
|
||||
const notes = (await query.getMany()).filter(note => {
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
const notes = await query.getMany();
|
||||
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
|
|
|
|||
|
|
@ -150,11 +150,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
// Hide blocked users / instances
|
||||
query.andWhere('"user"."isSuspended" = false');
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me, ps.list !== 'followers');
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
|
||||
// Respect blocks, mutes, and privacy
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, ps.list !== 'followers');
|
||||
|
||||
// Support pagination
|
||||
this.queryService
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -145,14 +145,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
];
|
||||
}
|
||||
|
||||
const [
|
||||
followings,
|
||||
mutedThreads,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.threadMutingsCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -161,20 +153,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
me,
|
||||
redisTimelines: timelineConfig,
|
||||
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
|
||||
alwaysIncludeMyNotes: true,
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
excludeBots: !ps.withBots,
|
||||
noteFilter: note => {
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!followings.has(note.reply.userId) && note.reply.userId !== me.id) return false;
|
||||
}
|
||||
|
||||
if (mutedThreads.has(note.threadId ?? note.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -204,14 +184,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withRenotes: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
// 1. by a user I follow, 2. a public local post, 3. my own post
|
||||
// by a user I follow OR a public local post OR my own post
|
||||
.andWhere(new Brackets(qb => this.queryService
|
||||
.orFollowingUser(qb, ':meId', 'note.userId')
|
||||
.orWhere(new Brackets(qbb => qbb
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.userHost IS NULL')))
|
||||
.orWhere(':meId = note.userId')))
|
||||
// 1. in a channel I follow, 2. not in a channel
|
||||
// in a channel I follow OR not in a channel
|
||||
.andWhere(new Brackets(qb => this.queryService
|
||||
.orFollowingChannel(qb, ':meId', 'note.channelId')
|
||||
.orWhere('note.channelId IS NULL')))
|
||||
|
|
@ -224,11 +204,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit);
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query
|
||||
// 1. Not a reply, 2. a self-reply
|
||||
.andWhere(new Brackets(qb => qb
|
||||
.orWhere('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = note.userId')));
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
|
|
|
|||
|
|
@ -117,8 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
|
||||
const mutedThreads = me ? await this.cacheService.threadMutingsCache.fetch(me.id) : null;
|
||||
|
||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -131,7 +129,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']
|
||||
: me ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`]
|
||||
: ['localTimeline'],
|
||||
alwaysIncludeMyNotes: true,
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
excludeBots: !ps.withBots,
|
||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||
|
|
@ -143,13 +140,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withBots: ps.withBots,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me),
|
||||
noteFilter: note => {
|
||||
if (mutedThreads?.has(note.threadId ?? note.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
if (me) {
|
||||
|
|
@ -184,11 +174,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit);
|
||||
|
||||
if (!ps.withReplies) {
|
||||
query
|
||||
// 1. Not a reply, 2. a self-reply
|
||||
.andWhere(new Brackets(qb => qb
|
||||
.orWhere('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = note.userId')));
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(qb, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(qb);
|
||||
this.queryService.generateSuspendedUserQueryForNote(qb);
|
||||
this.queryService.generateSilencedUserQueryForNotes(qb, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(qb, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(qb, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(qb, me);
|
||||
|
|
|
|||
|
|
@ -137,10 +137,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
//#region block/mute/vis
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
if (me) {
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
}
|
||||
|
||||
const renotes = await query.limit(ps.limit).getMany();
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
if (me) this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
|
||||
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
|
||||
|
||||
|
|
@ -123,19 +123,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw e;
|
||||
}
|
||||
|
||||
if (ps.reply != null) {
|
||||
if (ps.reply === false) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
} else {
|
||||
if (ps.reply) {
|
||||
query.andWhere('note.replyId IS NOT NULL');
|
||||
} else {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
if (ps.renote != null) {
|
||||
if (ps.renote === false) {
|
||||
this.queryService.andIsNotRenote(query, 'note');
|
||||
} else {
|
||||
if (ps.renote) {
|
||||
this.queryService.andIsRenote(query, 'note');
|
||||
} else {
|
||||
this.queryService.andIsNotRenote(query, 'note');
|
||||
}
|
||||
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,14 +97,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
|
||||
const [
|
||||
followings,
|
||||
threadMutings,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.threadMutingsCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
const timeline = this.fanoutTimelineEndpointService.timeline({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -113,18 +105,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
me,
|
||||
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
|
||||
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
|
||||
alwaysIncludeMyNotes: true,
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
noteFilter: note => {
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!followings.has(note.reply.userId) && note.reply.userId !== me.id) return false;
|
||||
}
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
|
||||
if (threadMutings.has(note.threadId ?? note.id)) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -146,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; withFiles: boolean; withRenotes: boolean; withBots: boolean; }, me: MiLocalUser) {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
// 1. in a channel I follow, 2. my own post, 3. by a user I follow
|
||||
// in a channel I follow OR my own post OR by a user I follow
|
||||
.andWhere(new Brackets(qb => this.queryService
|
||||
.orFollowingChannel(qb, ':meId', 'note.channelId')
|
||||
.orWhere(':meId = note.userId')
|
||||
|
|
@ -154,10 +135,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.andFollowingUser(qb2, ':meId', 'note.userId')
|
||||
.andWhere('note.channelId IS NULL'))),
|
||||
))
|
||||
// 1. Not a reply, 2. a self-reply
|
||||
.andWhere(new Brackets(qb => qb
|
||||
.orWhere('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere('note.replyUserId = note.userId')))
|
||||
.setParameters({ meId: me.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
|
|
@ -166,6 +143,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.limit(ps.limit);
|
||||
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { hasText } from '@/models/Note.js';
|
||||
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||
import { NoteVisibilityService } from '@/core/NoteVisibilityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -84,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleService: RoleService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly loggerService: ApiLoggerService,
|
||||
private readonly noteVisibilityService: NoteVisibilityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
||||
|
|
@ -91,7 +93,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw err;
|
||||
});
|
||||
|
||||
if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null, { me }))) {
|
||||
const { accessible } = await this.noteVisibilityService.checkNoteVisibilityAsync(note, me);
|
||||
if (!accessible) {
|
||||
throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,8 +125,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
me,
|
||||
useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
|
||||
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
|
||||
alwaysIncludeMyNotes: true,
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
ignoreAuthorFromUserSilence: true,
|
||||
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb(list, {
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -156,16 +156,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => qb
|
||||
// 返信ではない
|
||||
.orWhere('note.replyId IS NULL')
|
||||
// 返信だけど投稿者自身への返信
|
||||
.orWhere('note.replyUserId = note.userId')
|
||||
// 返信だけど自分宛ての返信
|
||||
.orWhere('note.replyUserId = :meId')
|
||||
// 返信だけどwithRepliesがtrueの場合
|
||||
.orWhere('userListMemberships.withReplies = true'),
|
||||
))
|
||||
.setParameters({ meId: me.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
|
|
@ -174,10 +164,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.limit(ps.limit);
|
||||
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me, 'userListMemberships.withReplies');
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me, true);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, true);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me);
|
||||
|
|
|
|||
|
|
@ -78,12 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return [];
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
]) : [new Set<string>()];
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
|
|
@ -95,13 +89,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me, true);
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
if (me) {
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, true);
|
||||
}
|
||||
|
||||
const notes = (await query.getMany()).filter(note => {
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe, false)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting, true)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
const notes = await query.getMany();
|
||||
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
|
|
|
|||
|
|
@ -134,8 +134,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (ps.withReplies) redisTimelines.push(`userTimelineWithReplies:${ps.userId}`);
|
||||
if (ps.withChannelNotes) redisTimelines.push(`userTimelineWithChannel:${ps.userId}`);
|
||||
|
||||
const isFollowing = me && (await this.cacheService.userFollowingsCache.fetch(me.id)).has(ps.userId);
|
||||
|
||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
|
@ -147,14 +145,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
ignoreAuthorFromMute: true,
|
||||
ignoreAuthorFromInstanceBlock: true,
|
||||
ignoreAuthorFromUserSuspension: true,
|
||||
ignoreAuthorFromUserSilence: true,
|
||||
excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies
|
||||
excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
excludeBots: !ps.withBots,
|
||||
noteFilter: note => {
|
||||
if (note.channel?.isSensitive && !isSelf) return false;
|
||||
if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
|
||||
if (note.visibility === 'followers' && !isFollowing && !isSelf) return false;
|
||||
|
||||
// These are handled by DB fallback, but we duplicate them here in case a timeline was already populated with notes
|
||||
if (!ps.withRepliesToSelf && note.reply?.userId === note.userId) return false;
|
||||
|
|
@ -218,12 +215,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.andWhere('note.channelId IS NULL');
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query, true);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query, true);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me, true);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, true);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedNoteThreadQuery(query, me);
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
|
|
@ -241,13 +239,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) {
|
||||
query.andWhere('reply.id IS NULL');
|
||||
} else if (!ps.withRepliesToOthers) {
|
||||
query.andWhere('(reply.id IS NULL OR reply."userId" = note."userId")');
|
||||
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
|
||||
} else if (!ps.withRepliesToSelf) {
|
||||
query.andWhere('(reply.id IS NULL OR reply."userId" != note."userId")');
|
||||
}
|
||||
|
||||
if (!ps.withNonPublic) {
|
||||
query.andWhere('note.visibility = \'public\'');
|
||||
} else {
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
}
|
||||
|
||||
if (!ps.withBots) {
|
||||
|
|
|
|||
|
|
@ -100,8 +100,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
}
|
||||
|
||||
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
|
||||
|
||||
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('reaction.userId = :userId', { userId: ps.userId })
|
||||
|
|
@ -115,21 +113,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBlockedHostQueryForNote(query);
|
||||
this.queryService.generateSuspendedUserQueryForNote(query);
|
||||
this.queryService.generateSilencedUserQueryForNotes(query, me, true);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserQueryForNotes(query, me, true);
|
||||
this.queryService.generateBlockedUserQueryForNotes(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
}
|
||||
|
||||
const reactions = (await query
|
||||
.limit(ps.limit)
|
||||
.getMany()).filter(reaction => {
|
||||
if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user
|
||||
if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
const reactions = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.j
|
|||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { appendContentWarning } from '@/misc/append-content-warning.js';
|
||||
import { isRenote } from '@/misc/is-renote.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
|
||||
// Missing from Megalodon apparently
|
||||
// https://docs.joinmastodon.org/entities/StatusEdit/
|
||||
|
|
@ -68,6 +69,7 @@ export class MastodonConverters {
|
|||
private readonly idService: IdService,
|
||||
private readonly driveFileEntityService: DriveFileEntityService,
|
||||
private readonly mastodonDataService: MastodonDataService,
|
||||
private readonly federatedInstanceService: FederatedInstanceService,
|
||||
) {}
|
||||
|
||||
private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
|
||||
|
|
@ -210,6 +212,7 @@ export class MastodonConverters {
|
|||
}
|
||||
|
||||
const noteUser = await this.getUser(note.userId);
|
||||
const noteInstance = noteUser.instance ?? (noteUser.host ? await this.federatedInstanceService.fetch(noteUser.host) : null);
|
||||
const account = await this.convertAccount(noteUser);
|
||||
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
|
||||
const history: StatusEdit[] = [];
|
||||
|
|
@ -224,7 +227,16 @@ export class MastodonConverters {
|
|||
// TODO avoid re-packing files for each edit
|
||||
const files = await this.driveFileEntityService.packManyByIds(edit.fileIds);
|
||||
|
||||
const cw = appendContentWarning(edit.cw, noteUser.mandatoryCW) ?? '';
|
||||
let cw = edit.cw ?? '';
|
||||
if (note.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, note.mandatoryCW);
|
||||
}
|
||||
if (noteUser.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, noteUser.mandatoryCW);
|
||||
}
|
||||
if (noteInstance?.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, noteInstance.mandatoryCW);
|
||||
}
|
||||
|
||||
const isQuote = renote && (edit.cw || edit.newText || edit.fileIds.length > 0 || note.replyId);
|
||||
const quoteUri = isQuote
|
||||
|
|
@ -299,7 +311,13 @@ export class MastodonConverters {
|
|||
? quoteUri.then(quote => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quote) ?? escapeMFM(text))
|
||||
: '';
|
||||
|
||||
const cw = appendContentWarning(note.cw, noteUser.mandatoryCW) ?? '';
|
||||
let cw = note.cw ?? '';
|
||||
if (note.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, note.mandatoryCW);
|
||||
}
|
||||
if (noteUser.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, noteUser.mandatoryCW);
|
||||
}
|
||||
|
||||
const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { JsonObject, JsonValue } from '@/misc/json-value.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import type Connection from '@/server/api/stream/Connection.js';
|
||||
import { NoteVisibilityFilters } from '@/core/NoteVisibilityService.js';
|
||||
|
||||
/**
|
||||
* Stream channel
|
||||
|
|
@ -26,6 +27,10 @@ export default abstract class Channel {
|
|||
public static readonly requireCredential: boolean;
|
||||
public static readonly kind?: string | null;
|
||||
|
||||
protected get noteVisibilityService() {
|
||||
return this.noteEntityService.noteVisibilityService;
|
||||
}
|
||||
|
||||
protected get user() {
|
||||
return this.connection.user;
|
||||
}
|
||||
|
|
@ -105,8 +110,14 @@ export default abstract class Channel {
|
|||
return this.connection.myRecentFavorites;
|
||||
}
|
||||
|
||||
protected async checkNoteVisibility(note: Packed<'Note'>, filters?: NoteVisibilityFilters) {
|
||||
// Don't use any of the local cached data, because this does everything through CacheService which is just as fast with updated data.
|
||||
return await this.noteVisibilityService.checkNoteVisibilityAsync(note, this.user, { filters });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a note is visible to the current user *excluding* blocks and mutes.
|
||||
* @deprecated use isNoteHidden instead
|
||||
*/
|
||||
protected isNoteVisibleToMe(note: Packed<'Note'>): boolean {
|
||||
if (note.visibility === 'public') return true;
|
||||
|
|
@ -120,8 +131,9 @@ export default abstract class Channel {
|
|||
return note.visibleUserIds.includes(this.user.id);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* ミュートとブロックされてるを処理する
|
||||
* @deprecated use isNoteHidden instead
|
||||
*/
|
||||
protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
|
||||
// Ignore notes that require sign-in
|
||||
|
|
@ -196,12 +208,11 @@ export default abstract class Channel {
|
|||
// If we didn't clone the notes here, different connections would asynchronously write
|
||||
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
|
||||
const clonedNote = deepClone(note);
|
||||
const notes = crawl(clonedNote);
|
||||
|
||||
// Hide notes before everything else, since this modifies fields that the other functions will check.
|
||||
await this.noteEntityService.hideNotes(notes, this.user.id);
|
||||
const notes = crawl(clonedNote);
|
||||
|
||||
const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings] = await Promise.all([
|
||||
const [myReactions, myRenotes, myFavorites, myThreadMutings, myNoteMutings, myFollowings] = await Promise.all([
|
||||
this.noteEntityService.populateMyReactions(notes, this.user.id, {
|
||||
myReactions: this.myRecentReactions,
|
||||
}),
|
||||
|
|
@ -213,13 +224,25 @@ export default abstract class Channel {
|
|||
}),
|
||||
this.noteEntityService.populateMyTheadMutings(notes, this.user.id),
|
||||
this.noteEntityService.populateMyNoteMutings(notes, this.user.id),
|
||||
this.cacheService.userFollowingsCache.fetch(this.user.id),
|
||||
]);
|
||||
|
||||
note.myReaction = myReactions.get(note.id) ?? null;
|
||||
note.isRenoted = myRenotes.has(note.id);
|
||||
note.isFavorited = myFavorites.has(note.id);
|
||||
note.isMutingThread = myThreadMutings.has(note.id);
|
||||
note.isMutingNote = myNoteMutings.has(note.id);
|
||||
for (const n of notes) {
|
||||
// Sync visibility in case there's something like "makeNotesFollowersOnlyBefore" enabled
|
||||
this.noteVisibilityService.syncVisibility(n);
|
||||
|
||||
n.myReaction = myReactions.get(n.id) ?? null;
|
||||
n.isRenoted = myRenotes.has(n.id);
|
||||
n.isFavorited = myFavorites.has(n.id);
|
||||
n.isMutingThread = myThreadMutings.has(n.id);
|
||||
n.isMutingNote = myNoteMutings.has(n.id);
|
||||
n.user.bypassSilence = n.userId === this.user.id || myFollowings.has(n.userId);
|
||||
}
|
||||
|
||||
// Hide notes *after* we sync visibility
|
||||
await this.noteEntityService.hideNotes(notes, this.user.id, {
|
||||
userFollowings: myFollowings,
|
||||
});
|
||||
|
||||
return clonedNote;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ class AntennaChannel extends Channel {
|
|||
if (data.type === 'note') {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
|
||||
this.send('note', note);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import Channel, { MiChannelService } from '../channel.js';
|
||||
|
||||
class BubbleTimelineChannel extends Channel {
|
||||
|
|
@ -47,8 +47,6 @@ class BubbleTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
|
|
@ -56,27 +54,9 @@ class BubbleTimelineChannel extends Channel {
|
|||
if (note.channelId != null) return;
|
||||
if (!this.utilityService.isBubbledHost(note.user.host)) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ChannelChannel extends Channel {
|
||||
|
|
@ -45,9 +46,9 @@ class ChannelChannel extends Channel {
|
|||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -48,36 +48,15 @@ class GlobalTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ class HashtagChannel extends Channel {
|
|||
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
|
||||
if (!matched) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -50,28 +50,9 @@ class HomeTimelineChannel extends Channel {
|
|||
if (!isMe && !this.following.has(note.userId)) return;
|
||||
}
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -67,28 +67,10 @@ class HybridTimelineChannel extends Channel {
|
|||
(note.channelId != null && this.followingChannels.has(note.channelId))
|
||||
)) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies && !this.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
if (!this.withReplies && note.replyId != null) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -50,8 +50,6 @@ class LocalTimelineChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||
if (!this.withBots && note.user.isBot) return;
|
||||
|
||||
|
|
@ -59,28 +57,10 @@ class LocalTimelineChannel extends Channel {
|
|||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
if (!this.withReplies && note.replyId != null) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -35,24 +35,19 @@ class MainChannel extends Channel {
|
|||
if (isUserFromMutedInstance(data.body, this.userMutedInstances)) return;
|
||||
if (data.body.userId && this.userIdsWhoMeMuting.has(data.body.userId)) return;
|
||||
|
||||
if (data.body.note && data.body.note.isHidden) {
|
||||
if (this.isNoteMutedOrBlocked(data.body.note)) return;
|
||||
if (!this.isNoteVisibleToMe(data.body.id)) return;
|
||||
const note = await this.noteEntityService.pack(data.body.note.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
data.body.note = note;
|
||||
if (data.body.note) {
|
||||
const { accessible, silence } = await this.checkNoteVisibility(data.body.note, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
|
||||
data.body.note = await this.rePackNote(data.body.note);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
if (this.isNoteMutedOrBlocked(data.body)) return;
|
||||
if (data.body.isHidden) {
|
||||
const note = await this.noteEntityService.pack(data.body.id, this.user, {
|
||||
detail: true,
|
||||
});
|
||||
data.body = note;
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(data.body, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
|
||||
data.body = await this.rePackNote(data.body);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote, isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
|
|
@ -49,26 +49,8 @@ class RoleTimelineChannel extends Channel {
|
|||
}
|
||||
if (note.visibility !== 'public') return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user?.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note);
|
||||
if (!accessible || silence) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
|
@ -82,8 +82,6 @@ class UserListChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
private async onNote(note: Packed<'Note'>) {
|
||||
const isMe = this.user?.id === note.userId;
|
||||
|
||||
// チャンネル投稿は無視する
|
||||
if (note.channelId) return;
|
||||
|
||||
|
|
@ -91,28 +89,9 @@ class UserListChannel extends Channel {
|
|||
|
||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||
|
||||
if (this.isNoteMutedOrBlocked(note)) return;
|
||||
if (!this.isNoteVisibleToMe(note)) return;
|
||||
|
||||
if (note.reply) {
|
||||
const reply = note.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
if (!this.following.get(note.userId)?.withReplies) {
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 純粋なリノート(引用リノートでないリノート)の場合
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
|
||||
if (!this.withRenotes) return;
|
||||
if (note.renote.reply) {
|
||||
const reply = note.renote.reply;
|
||||
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
|
||||
if (!this.isNoteVisibleToMe(reply)) return;
|
||||
}
|
||||
}
|
||||
const { accessible, silence } = await this.checkNoteVisibility(note, { includeReplies: true });
|
||||
if (!accessible || silence) return;
|
||||
if (!this.withRenotes && isPackedPureRenote(note)) return;
|
||||
|
||||
const clonedNote = await this.rePackNote(note);
|
||||
this.send('note', clonedNote);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue