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:
commit
911f90f95a
17 changed files with 127 additions and 59 deletions
|
|
@ -501,3 +501,30 @@ following apply:
|
|||
|
||||
- It's necessary to use `null` as a data value.
|
||||
`QuantumKVCache` does not allow null values, and thus another option should be chosen.
|
||||
|
||||
### Inter-Process Communication
|
||||
|
||||
Sharkey can utilize multiple processes for a single server.
|
||||
When running in this mode, a mechanism to synchronization changes between each process is necessary.
|
||||
This is accomplished through the use of **Redis IPC**.
|
||||
|
||||
#### IPC Options
|
||||
|
||||
There are three methods to access this IPC system, all of which are available through Dependency Injection:
|
||||
* Using the `pub` and `sub` Redis instances to directly establish channels.
|
||||
This should only be done when necessary, like when implementing very low-level utilities.
|
||||
* The `publishInternalEvent` method of `GlobalEventService`.
|
||||
This method will asynchronously publish an event to redis, which will forward it to all connected processes - **including the sending process**.
|
||||
Due to this and other issues, `publishInternalEvent` should be considered obsolete and avoided in new code.
|
||||
Instead, consider one of the other options.
|
||||
* `InternalEventService`, which is the newest and recommended way to handle IPC.
|
||||
The `emit` method accepts arguments identical to `publishInternalEvent`, which eases migration, while also accepting a configuration object to control event propagation.
|
||||
Additionally, `InternalEventService` ensures that local event listeners are called *before* notifying other processes, avoiding potential data races and other weirdness.
|
||||
|
||||
#### When to use IPC
|
||||
|
||||
IPC should be used whenever cacheable data is modified.
|
||||
By cacheable, we mean any data that could be stored in any of the supported memory caches.
|
||||
Changes to `MiUser`, `MiUserProfile`, or `MiInstance` entities should **always** be considered cacheable, but these are not the only options.
|
||||
A major exception is when the local data is cached in a Quantum cache (`QuantumKVCache`).
|
||||
Quantum caches automatically call `InternalEventService.emit` to synchronize changes, so you only need to `await set()` and the changes will be reflected in other processes' caches too.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue