merge: Fix regressions from recent block/mute fixes (!1239)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1239

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2025-11-15 13:38:52 -05:00
commit 911f90f95a
17 changed files with 127 additions and 59 deletions

View file

@ -116,7 +116,10 @@ export class FanoutTimelineEndpointService {
const parentFilter = filter;
filter = (note, populated) => {
const { accessible, silence } = this.noteVisibilityService.checkNoteVisibility(populated, me, { data, filters: { includeSilencedAuthor: ps.ignoreAuthorFromUserSilence } });
const { accessible, silence } = this.noteVisibilityService.checkNoteVisibility(populated, me, { data, filters: {
includeSilencedAuthor: ps.ignoreAuthorFromUserSilence,
includeReplies: true, // Include replies because we check them elsewhere
} });
if (!accessible || silence) return false;
return parentFilter(note, populated);

View file

@ -3,8 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MiUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js';
export function isReply(note: any, viewerId?: MiUser['id'] | undefined | null): boolean {
return note.replyId && note.replyUserId !== note.userId && note.replyUserId !== viewerId;
// Should really be named "isReplyToOther"
export function isReply(note: MiNote, viewerId?: MiUser['id'] | undefined | null): boolean {
return note.replyId != null && note.replyUserId !== note.userId && note.replyUserId !== viewerId;
}

View file

@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import webpush from 'web-push';
const { generateVAPIDKeys } = webpush;
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -15,6 +14,21 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:meta',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
public: {
type: 'string',
optional: false, nullable: false,
},
private: {
type: 'string',
optional: false, nullable: false,
},
},
},
} as const;
export const paramDef = {
@ -28,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const keys = await generateVAPIDKeys();
super(meta, paramDef, async () => {
const keys = webpush.generateVAPIDKeys();
// TODO add moderation log

View file

@ -489,6 +489,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
about: {
type: 'string',
optional: false, nullable: true,
},
disableRegistration: {
type: 'boolean',
optional: false, nullable: false,

View file

@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { InternalEventService } from '@/global/InternalEventService.js';
export const meta = {
tags: ['admin'],
@ -36,7 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private readonly roleService: RoleService,
private readonly globalEventService: GlobalEventService,
private readonly internalEventService: InternalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
@ -47,11 +47,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user.isSilenced) return;
await this.usersRepository.update(user.id, {
await this.usersRepository.update({ id: user.id }, {
isSilenced: true,
});
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
await this.internalEventService.emit(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});

View file

@ -9,7 +9,7 @@ import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { InternalEventService } from '@/global/InternalEventService.js';
export const meta = {
tags: ['admin'],
@ -34,18 +34,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private readonly usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private readonly globalEventService: GlobalEventService,
private readonly internalEventService: InternalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
if (!user.isSilenced) return;
await this.usersRepository.update(user.id, {
await this.usersRepository.update({ id: user.id }, {
isSilenced: false,
});
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
await this.internalEventService.emit(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});

View file

@ -8,6 +8,8 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { InternalEventService } from '@/global/InternalEventService.js';
import { CacheService } from '@/core/CacheService.js';
export const meta = {
tags: ['admin'],
@ -34,23 +36,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private readonly internalEventService: InternalEventService,
private readonly cacheService: CacheService,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
const currentProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
const [user, currentProfile] = await Promise.all([
this.cacheService.findUserById(ps.userId),
this.cacheService.userProfileCache.fetch(ps.userId),
]);
await this.userProfilesRepository.update({ userId: user.id }, {
moderationNote: ps.text,
});
await this.internalEventService.emit('updateUserProfile', { userId: user.id });
this.moderationLogService.log(me, 'updateUserNote', {
await this.moderationLogService.log(me, 'updateUserNote', {
userId: user.id,
userUsername: user.username,
userHost: user.host,

View file

@ -28,10 +28,11 @@ export const meta = {
},
},
// 2 calls per second
// 20 calls, then 4 per second
limit: {
duration: 1000,
max: 2,
type: 'bucket',
size: 20,
dripRate: 250,
},
} as const;

View file

@ -51,10 +51,11 @@ export const meta = {
},
},
// 5 calls per second
// Up to 20 calls, then 4/second
limit: {
duration: 1000,
max: 5,
type: 'bucket',
size: 20,
dripRate: 250,
},
} as const;
@ -63,7 +64,6 @@ export const paramDef = {
properties: {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean', default: false },
withRepliesToSelf: { type: 'boolean', default: true },
withQuotes: { type: 'boolean', default: true },
withRenotes: { type: 'boolean', default: true },
withBots: { type: 'boolean', default: true },
@ -122,8 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withQuotes: ps.withQuotes,
withBots: ps.withBots,
withNonPublic: ps.withNonPublic,
withRepliesToOthers: ps.withReplies,
withRepliesToSelf: ps.withRepliesToSelf,
withReplies: ps.withReplies,
}, me);
return await this.noteEntityService.packMany(timeline, me);
@ -146,15 +145,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
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
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;
// 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;
if (!ps.withQuotes && isRenote(note) && isQuote(note)) return false;
if (!ps.withNonPublic && note.visibility !== 'public') return false;
@ -171,8 +167,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withQuotes: ps.withQuotes,
withBots: ps.withBots,
withNonPublic: ps.withNonPublic,
withRepliesToOthers: ps.withReplies,
withRepliesToSelf: ps.withRepliesToSelf,
withReplies: ps.withReplies,
}, me),
});
@ -191,8 +186,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withQuotes: boolean,
withBots: boolean,
withNonPublic: boolean,
withRepliesToOthers: boolean,
withRepliesToSelf: boolean,
withReplies: boolean,
}, me: MiLocalUser | null) {
const isSelf = me && (me.id === ps.userId);
@ -236,12 +230,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.andIsNotQuote(query, 'note');
}
if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) {
query.andWhere('reply.id IS NULL');
} else if (!ps.withRepliesToOthers) {
if (!ps.withReplies) {
this.queryService.generateExcludedRepliesQueryForNotes(query, me);
} else if (!ps.withRepliesToSelf) {
query.andWhere('(reply.id IS NULL OR reply."userId" != note."userId")');
}
if (!ps.withNonPublic) {

View file

@ -472,7 +472,7 @@ const proxyAccountForm = useForm({
description: state.description,
});
}
if (state.enabled !== proxyAccount.enabled) {
if (state.enabled !== meta.enableProxyAccount) {
await os.apiWithDialog('admin/update-meta', {
enableProxyAccount: state.enabled,
});

View file

@ -262,6 +262,9 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
.body {
margin: 0 12px;
// https://stackoverflow.com/questions/36230944/prevent-flex-items-from-overflowing-a-container
min-width: 0;
}
.header {

View file

@ -33,10 +33,11 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
import { store } from '@/store.js';
import { deepMerge } from '@/utility/merge.js';
import * as os from '@/os.js';
import { useMuteOverrides } from '@/utility/check-word-mute.js';
import { store } from '@/store.js';
import { $i } from '@/i.js';
import * as os from '@/os.js';
const router = useRouter();
@ -65,10 +66,22 @@ function saveTlFilter(key: keyof typeof store.s.tl.filter, newValue: boolean) {
}
}
const muteOverrides = useMuteOverrides();
watch(() => props.listId, async () => {
list.value = await misskeyApi('users/lists/show', {
const _list = await misskeyApi('users/lists/show', {
listId: props.listId,
});
list.value = _list;
// Disable mandatory CW for all list members
muteOverrides.user = {}; // Reset prior
for (const userId of _list.userIds) {
muteOverrides.user[userId] = {
userMandatoryCW: null,
instanceMandatoryCW: null,
};
}
}, { immediate: true });
function queueUpdated(q) {
@ -76,6 +89,7 @@ function queueUpdated(q) {
}
function top() {
if (!rootEl.value) return;
scrollInContainer(rootEl.value, { top: 0 });
}

View file

@ -250,6 +250,9 @@ type AdminFederationUpdateInstanceRequest = operations['admin___federation___upd
// @public (undocumented)
type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminGenVapidKeysResponse = operations['admin___gen-vapid-keys']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
@ -1598,6 +1601,7 @@ declare namespace entities {
AdminFederationRemoveAllFollowingRequest,
AdminFederationUpdateInstanceRequest,
AdminForwardAbuseUserReportRequest,
AdminGenVapidKeysResponse,
AdminGetIndexStatsResponse,
AdminGetTableStatsResponse,
AdminGetUserIpsRequest,

View file

@ -71,6 +71,7 @@ import type {
AdminFederationRemoveAllFollowingRequest,
AdminFederationUpdateInstanceRequest,
AdminForwardAbuseUserReportRequest,
AdminGenVapidKeysResponse,
AdminGetIndexStatsResponse,
AdminGetTableStatsResponse,
AdminGetUserIpsRequest,
@ -726,7 +727,7 @@ export type Endpoints = {
'admin/federation/remove-all-following': { req: AdminFederationRemoveAllFollowingRequest; res: EmptyResponse };
'admin/federation/update-instance': { req: AdminFederationUpdateInstanceRequest; res: EmptyResponse };
'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
'admin/gen-vapid-keys': { req: EmptyRequest; res: EmptyResponse };
'admin/gen-vapid-keys': { req: EmptyRequest; res: AdminGenVapidKeysResponse };
'admin/get-index-stats': { req: EmptyRequest; res: AdminGetIndexStatsResponse };
'admin/get-table-stats': { req: EmptyRequest; res: AdminGetTableStatsResponse };
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };

View file

@ -74,6 +74,7 @@ export type AdminFederationRefreshRemoteInstanceMetadataRequest = operations['ad
export type AdminFederationRemoveAllFollowingRequest = operations['admin___federation___remove-all-following']['requestBody']['content']['application/json'];
export type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminGenVapidKeysResponse = operations['admin___gen-vapid-keys']['responses']['200']['content']['application/json'];
export type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
export type AdminGetTableStatsResponse = operations['admin___get-table-stats']['responses']['200']['content']['application/json'];
export type AdminGetUserIpsRequest = operations['admin___get-user-ips']['requestBody']['content']['application/json'];

View file

@ -15800,12 +15800,17 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description OK (without any results) */
204: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
content: {
'application/json': {
public: string;
private: string;
};
};
};
/** @description Client error */
400: {
@ -16346,6 +16351,7 @@ export interface operations {
defaultLightTheme: string | null;
defaultLike: string;
description: string | null;
about: string | null;
disableRegistration: boolean;
impressumUrl: string | null;
donationUrl: string | null;
@ -47895,8 +47901,6 @@ export interface operations {
/** @default false */
withReplies?: boolean;
/** @default true */
withRepliesToSelf?: boolean;
/** @default true */
withQuotes?: boolean;
/** @default true */
withRenotes?: boolean;