Rework pacing, pause poll, add user search, voice overview, and DM list
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2026 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Veskslop core bridge — the only external control surface.
|
||||
*
|
||||
@@ -35,6 +41,9 @@ const KNOWN_ACTIONS = new Set<string>([
|
||||
PuppetAction.EventsSubscribe,
|
||||
PuppetAction.StateGet,
|
||||
PuppetAction.VoiceGet,
|
||||
PuppetAction.VoiceOverview,
|
||||
PuppetAction.UserSearch,
|
||||
PuppetAction.DmList,
|
||||
PuppetAction.MessageSend,
|
||||
PuppetAction.MessageRead,
|
||||
PuppetAction.MessageFetch,
|
||||
|
||||
@@ -11,9 +11,7 @@ import { VesktopNative } from "./VesktopNative";
|
||||
|
||||
// Veskslop: seed the Discord auth token before the client boots, unless a session exists.
|
||||
try {
|
||||
const seed = ipcRenderer.sendSync(IpcEvents.VESKSLOP_GET_TOKEN) as
|
||||
| { token?: string; force?: boolean }
|
||||
| undefined;
|
||||
const seed = ipcRenderer.sendSync(IpcEvents.VESKSLOP_GET_TOKEN) as { token?: string; force?: boolean } | undefined;
|
||||
|
||||
if (seed?.token && (seed.force || !localStorage.getItem("token"))) {
|
||||
localStorage.setItem("token", JSON.stringify(seed.token));
|
||||
|
||||
+217
-34
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2026 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Veskslop core actions — the validated capability surface.
|
||||
*
|
||||
@@ -11,6 +17,7 @@ import { openPrivateChannel, sendMessage } from "@vencord/types/utils";
|
||||
import { findByPropsLazy } from "@vencord/types/webpack";
|
||||
import {
|
||||
ChannelStore,
|
||||
GuildMemberStore,
|
||||
GuildStore,
|
||||
MessageStore,
|
||||
UserStore,
|
||||
@@ -68,8 +75,11 @@ function validateContent(content: unknown): string {
|
||||
PuppetErrorCode.ContentTooLong,
|
||||
`content exceeds ${PUPPET_LIMITS.maxContentLength} characters`
|
||||
);
|
||||
// eslint-disable-next-line no-control-regex
|
||||
assert(!/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(trimmed), PuppetErrorCode.InvalidParam, "content has control characters");
|
||||
assert(
|
||||
!/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(trimmed),
|
||||
PuppetErrorCode.InvalidParam,
|
||||
"content has control characters"
|
||||
);
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
@@ -109,15 +119,37 @@ function getSendableChannel(channelId: string) {
|
||||
return channel;
|
||||
}
|
||||
|
||||
/** Shape a Discord message for the daemon. Attachments carry CDN links only. */
|
||||
function mapMessage(message: any) {
|
||||
return {
|
||||
id: message.id,
|
||||
author: {
|
||||
id: message.author.id,
|
||||
username: message.author.username,
|
||||
avatar: message.author.avatar ?? null
|
||||
},
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
attachments: (message.attachments ?? []).map((attachment: any) => ({
|
||||
filename: attachment.filename,
|
||||
url: attachment.url ?? null,
|
||||
contentType: attachment.contentType ?? null,
|
||||
width: attachment.width ?? null,
|
||||
height: attachment.height ?? null
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function buildState() {
|
||||
const user = UserStore.getCurrentUser();
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
user: { id: user.id, username: user.username },
|
||||
user: { id: user.id, username: user.username, avatar: user.avatar ?? null },
|
||||
guilds: GuildStore.getGuildsArray().map(guild => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.icon ?? null,
|
||||
channels: Object.values(ChannelStore.getMutableBasicGuildChannelsForGuild(guild.id))
|
||||
.slice(0, PUPPET_LIMITS.maxChannelsPerGuild)
|
||||
.map(channel => ({ id: channel.id, name: channel.name, type: channel.type }))
|
||||
@@ -125,6 +157,85 @@ function buildState() {
|
||||
};
|
||||
}
|
||||
|
||||
function voiceMember(state: any) {
|
||||
const user = UserStore.getUser(state.userId);
|
||||
return {
|
||||
id: state.userId,
|
||||
username: user?.username ?? state.userId,
|
||||
globalName: user?.globalName ?? null,
|
||||
avatar: user?.avatar ?? null,
|
||||
mute: !!state.mute,
|
||||
deaf: !!state.deaf,
|
||||
selfMute: !!state.selfMute,
|
||||
selfDeaf: !!state.selfDeaf
|
||||
};
|
||||
}
|
||||
|
||||
function toUserResult(user: any, guildId?: string) {
|
||||
const nick = guildId ? GuildMemberStore.getNick(guildId, user.id) : null;
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
globalName: user.globalName ?? null,
|
||||
avatar: user.avatar ?? null,
|
||||
nick: nick ?? null,
|
||||
bot: !!user.bot
|
||||
};
|
||||
}
|
||||
|
||||
function userMatches(user: any, needle: string, guildId?: string): boolean {
|
||||
const nick = guildId ? GuildMemberStore.getNick(guildId, user.id) : null;
|
||||
return [user?.username, user?.globalName, nick].some((value: string | null | undefined) =>
|
||||
(value ?? "").toLowerCase().includes(needle)
|
||||
);
|
||||
}
|
||||
|
||||
function userRank(user: any, needle: string, guildId?: string): number {
|
||||
const username = (user?.username ?? "").toLowerCase();
|
||||
if (username === needle) return 0;
|
||||
if (username.startsWith(needle)) return 1;
|
||||
|
||||
if ((user?.globalName ?? "").toLowerCase().startsWith(needle)) return 2;
|
||||
|
||||
const nick = (guildId ? GuildMemberStore.getNick(guildId, user.id) : null) ?? "";
|
||||
if (nick.toLowerCase().startsWith(needle)) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
/** Search the client cache: guild members when a guild is given, otherwise all known users. */
|
||||
function searchUsers(query: string, guildId: string | undefined, limit: number) {
|
||||
const needle = query.replace(/^@/, "").trim().toLowerCase();
|
||||
if (!needle) return [];
|
||||
|
||||
if (isSnowflake(needle)) {
|
||||
const user = UserStore.getUser(needle);
|
||||
return user ? [toUserResult(user, guildId)] : [];
|
||||
}
|
||||
|
||||
const pool: any[] = guildId
|
||||
? GuildMemberStore.getMembers(guildId)
|
||||
.map(member => UserStore.getUser(member.userId))
|
||||
.filter(Boolean)
|
||||
: Object.values(UserStore.getUsers());
|
||||
|
||||
const seen = new Set<string>();
|
||||
const matches: any[] = [];
|
||||
|
||||
for (const user of pool) {
|
||||
if (!user?.id || seen.has(user.id) || !userMatches(user, needle, guildId)) continue;
|
||||
seen.add(user.id);
|
||||
matches.push(user);
|
||||
}
|
||||
|
||||
matches.sort(
|
||||
(a, b) =>
|
||||
userRank(a, needle, guildId) - userRank(b, needle, guildId) ||
|
||||
String(a.username).localeCompare(String(b.username))
|
||||
);
|
||||
|
||||
return matches.slice(0, limit).map(user => toUserResult(user, guildId));
|
||||
}
|
||||
|
||||
async function send(channelId: string, content: string): Promise<unknown> {
|
||||
try {
|
||||
return await sendMessage(channelId, { content }, false);
|
||||
@@ -141,7 +252,9 @@ async function resolveMessageId(raw: unknown, channelId: string, content: string
|
||||
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
const messages: any[] = MessageStore.getMessages(channelId)?._array ?? [];
|
||||
const match = [...messages].reverse().find(message => message.author?.id === selfId && message.content === content);
|
||||
const match = [...messages]
|
||||
.reverse()
|
||||
.find(message => message.author?.id === selfId && message.content === content);
|
||||
if (match) return match.id;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
@@ -164,10 +277,7 @@ async function resolveDmChannel(userId: string): Promise<string> {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
export async function handlePuppetAction(
|
||||
action: string,
|
||||
params: Record<string, unknown> = {}
|
||||
): Promise<PuppetResult> {
|
||||
export async function handlePuppetAction(action: string, params: Record<string, unknown> = {}): Promise<PuppetResult> {
|
||||
try {
|
||||
switch (action) {
|
||||
case PuppetAction.Ping:
|
||||
@@ -191,7 +301,12 @@ export async function handlePuppetAction(
|
||||
const targetId = (userId as string | undefined) ?? UserStore.getCurrentUser()?.id;
|
||||
assert(targetId, PuppetErrorCode.NotLoggedIn, "no user is logged in");
|
||||
|
||||
const voiceState = VoiceStateStore.getVoiceState((guildId as string | null) ?? null, targetId);
|
||||
const voiceState =
|
||||
guildId !== undefined
|
||||
? VoiceStateStore.getVoiceState(guildId as string, targetId)
|
||||
: (VoiceStateStore.getVoiceStateForUser(targetId) ??
|
||||
VoiceStateStore.getVoiceState(null, targetId));
|
||||
|
||||
if (!voiceState?.channelId) {
|
||||
return ok({ guildId: guildId ?? null, channelId: null, channelName: null, members: [] });
|
||||
}
|
||||
@@ -203,18 +318,85 @@ export async function handlePuppetAction(
|
||||
guildId: channel?.guild_id ?? guildId ?? null,
|
||||
channelId: voiceState.channelId,
|
||||
channelName: channel?.name ?? null,
|
||||
members: Object.values(states).map(state => {
|
||||
const user = UserStore.getUser(state.userId);
|
||||
return {
|
||||
id: state.userId,
|
||||
username: user?.username ?? state.userId,
|
||||
mute: state.mute,
|
||||
deaf: state.deaf
|
||||
};
|
||||
})
|
||||
members: Object.values(states).map(voiceMember)
|
||||
});
|
||||
}
|
||||
|
||||
case PuppetAction.VoiceOverview: {
|
||||
const { guildId } = params;
|
||||
assert(isSnowflake(guildId), PuppetErrorCode.InvalidSnowflake, "guildId must be a snowflake");
|
||||
|
||||
const byChannel = new Map<string, any[]>();
|
||||
for (const state of Object.values(VoiceStateStore.getVoiceStates(guildId as string))) {
|
||||
if (!state?.channelId) continue;
|
||||
const members = byChannel.get(state.channelId) ?? [];
|
||||
members.push(state);
|
||||
byChannel.set(state.channelId, members);
|
||||
}
|
||||
|
||||
return ok({
|
||||
guildId,
|
||||
channels: [...byChannel.entries()].map(([channelId, states]) => ({
|
||||
channelId,
|
||||
channelName: ChannelStore.getChannel(channelId)?.name ?? null,
|
||||
members: states.map(voiceMember)
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
case PuppetAction.UserSearch: {
|
||||
const { query, guildId, limit = 10 } = params;
|
||||
assert(
|
||||
typeof query === "string" && query.trim().length > 0 && query.length <= 100,
|
||||
PuppetErrorCode.InvalidParam,
|
||||
"query must be a string of 1 to 100 characters"
|
||||
);
|
||||
if (guildId !== undefined) {
|
||||
assert(isSnowflake(guildId), PuppetErrorCode.InvalidSnowflake, "guildId must be a snowflake");
|
||||
}
|
||||
assert(
|
||||
Number.isInteger(limit) &&
|
||||
Number(limit) >= 1 &&
|
||||
Number(limit) <= PUPPET_LIMITS.maxUserSearchResults,
|
||||
PuppetErrorCode.InvalidParam,
|
||||
`limit must be an integer between 1 and ${PUPPET_LIMITS.maxUserSearchResults}`
|
||||
);
|
||||
|
||||
return ok({
|
||||
query,
|
||||
guildId: guildId ?? null,
|
||||
users: searchUsers(query, guildId as string | undefined, Number(limit))
|
||||
});
|
||||
}
|
||||
|
||||
case PuppetAction.DmList: {
|
||||
const { limit = 25 } = params;
|
||||
assert(
|
||||
Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= PUPPET_LIMITS.maxDmList,
|
||||
PuppetErrorCode.InvalidParam,
|
||||
`limit must be an integer between 1 and ${PUPPET_LIMITS.maxDmList}`
|
||||
);
|
||||
|
||||
const channels = ChannelStore.getSortedPrivateChannels()
|
||||
.slice(0, Number(limit))
|
||||
.map(channel => ({
|
||||
id: channel.id,
|
||||
type: channel.type,
|
||||
name: (channel as any).name ?? null,
|
||||
icon: (channel as any).icon ?? null,
|
||||
lastMessageId: (channel as any).lastMessageId ?? null,
|
||||
recipients: Object.values((channel as any).recipients ?? {}).map((user: any) => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
globalName: user.globalName ?? null,
|
||||
avatar: user.avatar ?? null,
|
||||
bot: !!user.bot
|
||||
}))
|
||||
}));
|
||||
|
||||
return ok({ channels });
|
||||
}
|
||||
|
||||
case PuppetAction.MessageSend: {
|
||||
const { channelId } = params;
|
||||
assert(isSnowflake(channelId), PuppetErrorCode.InvalidSnowflake, "channelId must be a snowflake");
|
||||
@@ -266,17 +448,16 @@ export async function handlePuppetAction(
|
||||
`limit must be an integer between 1 and ${PUPPET_LIMITS.maxReadLimit}`
|
||||
);
|
||||
|
||||
assert(ChannelStore.getChannel(channelId), PuppetErrorCode.ChannelNotFound, `channel ${channelId} not found`);
|
||||
assert(
|
||||
ChannelStore.getChannel(channelId),
|
||||
PuppetErrorCode.ChannelNotFound,
|
||||
`channel ${channelId} not found`
|
||||
);
|
||||
const messages = MessageStore.getMessages(channelId)?._array ?? [];
|
||||
|
||||
return ok({
|
||||
cached: messages.length,
|
||||
messages: messages.slice(-Number(limit)).map(message => ({
|
||||
id: message.id,
|
||||
author: { id: message.author.id, username: message.author.username },
|
||||
content: message.content,
|
||||
timestamp: message.timestamp
|
||||
}))
|
||||
messages: messages.slice(-Number(limit)).map(mapMessage)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,9 +475,17 @@ export async function handlePuppetAction(
|
||||
if (params.after !== undefined) {
|
||||
assert(isSnowflake(params.after), PuppetErrorCode.InvalidSnowflake, "after must be a snowflake");
|
||||
}
|
||||
assert(!(params.before && params.after), PuppetErrorCode.InvalidParam, "before and after are mutually exclusive");
|
||||
assert(
|
||||
!(params.before && params.after),
|
||||
PuppetErrorCode.InvalidParam,
|
||||
"before and after are mutually exclusive"
|
||||
);
|
||||
|
||||
assert(ChannelStore.getChannel(channelId), PuppetErrorCode.ChannelNotFound, `channel ${channelId} not found`);
|
||||
assert(
|
||||
ChannelStore.getChannel(channelId),
|
||||
PuppetErrorCode.ChannelNotFound,
|
||||
`channel ${channelId} not found`
|
||||
);
|
||||
|
||||
const fetchMessages = (MessageActions as any)?.fetchMessages;
|
||||
assert(
|
||||
@@ -325,13 +514,7 @@ export async function handlePuppetAction(
|
||||
return ok({
|
||||
channelId,
|
||||
fetched: filtered.length,
|
||||
messages: filtered.slice(-Number(limit)).map(message => ({
|
||||
id: message.id,
|
||||
author: { id: message.author.id, username: message.author.username },
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
attachments: (message.attachments ?? []).map(attachment => ({ filename: attachment.filename }))
|
||||
}))
|
||||
messages: filtered.slice(-Number(limit)).map(mapMessage)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2026 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Veskslop core events — normalize client events and push them to main.
|
||||
*
|
||||
@@ -6,8 +12,8 @@
|
||||
* is guarded.
|
||||
*/
|
||||
|
||||
import { ChannelStore } from "@vencord/types/webpack/common";
|
||||
import { filters, waitFor } from "@vencord/types/webpack";
|
||||
import { ChannelStore } from "@vencord/types/webpack/common";
|
||||
import type { PuppetEvent } from "shared/puppetProtocol";
|
||||
|
||||
function normalizeMessage(event: any): PuppetEvent | undefined {
|
||||
@@ -23,18 +29,19 @@ function normalizeMessage(event: any): PuppetEvent | undefined {
|
||||
author: {
|
||||
id: message.author.id,
|
||||
username: message.author.username,
|
||||
avatar: message.author.avatar ?? null,
|
||||
bot: !!message.author.bot
|
||||
},
|
||||
content: message.content ?? "",
|
||||
mentions: [
|
||||
...(message.mentions ?? []).map((user: any) => user.id),
|
||||
...(message.mentionIds ?? [])
|
||||
],
|
||||
mentions: [...(message.mentions ?? []).map((user: any) => user.id), ...(message.mentionIds ?? [])],
|
||||
timestamp: message.timestamp ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReaction(event: any, type: "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_REMOVE"): PuppetEvent | undefined {
|
||||
function normalizeReaction(
|
||||
event: any,
|
||||
type: "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_REMOVE"
|
||||
): PuppetEvent | undefined {
|
||||
if (!event?.messageId || !event?.userId) return undefined;
|
||||
|
||||
return {
|
||||
@@ -50,6 +57,24 @@ function normalizeReaction(event: any, type: "MESSAGE_REACTION_ADD" | "MESSAGE_R
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeVoiceState(payload: any): PuppetEvent | undefined {
|
||||
const state = payload?.voiceState ?? payload;
|
||||
const userId = state?.userId ?? state?.user_id;
|
||||
if (!userId) return undefined;
|
||||
|
||||
return {
|
||||
type: "VOICE_STATE_UPDATE",
|
||||
userId,
|
||||
channelId: state.channelId ?? state.channel_id ?? null,
|
||||
guildId: state.guildId ?? state.guild_id ?? null,
|
||||
sessionId: state.sessionId ?? state.session_id ?? null,
|
||||
mute: !!state.mute,
|
||||
deaf: !!state.deaf,
|
||||
selfMute: !!state.selfMute,
|
||||
selfDeaf: !!state.selfDeaf
|
||||
};
|
||||
}
|
||||
|
||||
let installed = false;
|
||||
|
||||
export function initPuppetEvents() {
|
||||
@@ -84,6 +109,26 @@ export function initPuppetEvents() {
|
||||
}
|
||||
});
|
||||
|
||||
dispatcher.subscribe("VOICE_STATE_UPDATE", (event: any) => {
|
||||
try {
|
||||
const normalized = normalizeVoiceState(event);
|
||||
if (normalized) VesktopNative.puppet.emit(normalized);
|
||||
} catch (error) {
|
||||
console.error("[puppet] voice state normalization failed:", error);
|
||||
}
|
||||
});
|
||||
|
||||
dispatcher.subscribe("VOICE_STATE_UPDATES", (event: any) => {
|
||||
try {
|
||||
for (const state of event?.voiceStates ?? []) {
|
||||
const normalized = normalizeVoiceState(state);
|
||||
if (normalized) VesktopNative.puppet.emit(normalized);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[puppet] voice state normalization failed:", error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[veskslop] puppet events active");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2026 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Veskslop core entry — starts the event push and exports the action handler.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
|
||||
* Copyright (c) 2026 Vendicated and Vesktop contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/*
|
||||
* Vesktop ⇄ Veskslop daemon protocol.
|
||||
*
|
||||
@@ -11,6 +17,9 @@ export const enum PuppetAction {
|
||||
EventsSubscribe = "events.subscribe",
|
||||
StateGet = "state.get",
|
||||
VoiceGet = "voice.get",
|
||||
VoiceOverview = "voice.overview",
|
||||
UserSearch = "user.search",
|
||||
DmList = "dm.list",
|
||||
MessageSend = "message.send",
|
||||
MessageRead = "message.read",
|
||||
MessageFetch = "message.fetch",
|
||||
@@ -23,6 +32,9 @@ export const PuppetActions: readonly PuppetAction[] = [
|
||||
PuppetAction.EventsSubscribe,
|
||||
PuppetAction.StateGet,
|
||||
PuppetAction.VoiceGet,
|
||||
PuppetAction.VoiceOverview,
|
||||
PuppetAction.UserSearch,
|
||||
PuppetAction.DmList,
|
||||
PuppetAction.MessageSend,
|
||||
PuppetAction.MessageRead,
|
||||
PuppetAction.MessageFetch,
|
||||
@@ -61,13 +73,10 @@ export interface PuppetRequest {
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type PuppetResult =
|
||||
| { ok: true; result?: unknown }
|
||||
| { ok: false; error: PuppetError };
|
||||
export type PuppetResult = { ok: true; result?: unknown } | { ok: false; error: PuppetError };
|
||||
|
||||
export type PuppetResponse =
|
||||
| { id?: string | number; ok: true; result?: unknown }
|
||||
| { id?: string | number; ok: false; error: PuppetError };
|
||||
{ id?: string | number; ok: true; result?: unknown } | { id?: string | number; ok: false; error: PuppetError };
|
||||
|
||||
export interface PuppetEvent {
|
||||
type: string;
|
||||
@@ -79,6 +88,8 @@ export const PUPPET_LIMITS = {
|
||||
maxContentLength: 2000,
|
||||
maxReadLimit: 100,
|
||||
maxFetchLimit: 100,
|
||||
maxUserSearchResults: 25,
|
||||
maxDmList: 50,
|
||||
minActionIntervalMs: 500,
|
||||
maxActionsPerMinute: 30,
|
||||
maxRequestBytes: 64 * 1024,
|
||||
|
||||
Reference in New Issue
Block a user