/* * 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. * * This is the only surface the external daemon can reach. Every action is * validated by the core before anything touches Discord, and every failure is * returned as a structured error instead of throwing. */ export const enum PuppetAction { Ping = "ping", EventsSubscribe = "events.subscribe", StateGet = "state.get", VoiceGet = "voice.get", VoiceOverview = "voice.overview", VoiceJoin = "voice.join", VoiceLeave = "voice.leave", VoiceMove = "voice.move", VoiceUsers = "voice.users", VoiceSpeaking = "voice.speaking", UserStatus = "user.status", UserVolume = "user.volume", UserSetVolume = "user.setVolume", VoiceDevices = "voice.devices", VoiceSetDevices = "voice.setDevices", UserSearch = "user.search", DmList = "dm.list", MessageSend = "message.send", MessageRead = "message.read", MessageFetch = "message.fetch", ReactionAdd = "reaction.add", DmSend = "dm.send" } export const PuppetActions: readonly PuppetAction[] = [ PuppetAction.Ping, PuppetAction.EventsSubscribe, PuppetAction.StateGet, PuppetAction.VoiceGet, PuppetAction.VoiceOverview, PuppetAction.VoiceJoin, PuppetAction.VoiceLeave, PuppetAction.VoiceMove, PuppetAction.VoiceUsers, PuppetAction.VoiceSpeaking, PuppetAction.UserStatus, PuppetAction.UserVolume, PuppetAction.UserSetVolume, PuppetAction.VoiceDevices, PuppetAction.VoiceSetDevices, PuppetAction.UserSearch, PuppetAction.DmList, PuppetAction.MessageSend, PuppetAction.MessageRead, PuppetAction.MessageFetch, PuppetAction.ReactionAdd, PuppetAction.DmSend ]; export const enum PuppetErrorCode { InvalidRequest = "INVALID_REQUEST", PayloadTooLarge = "PAYLOAD_TOO_LARGE", UnknownAction = "UNKNOWN_ACTION", CoreUnavailable = "CORE_UNAVAILABLE", NotLoggedIn = "NOT_LOGGED_IN", InvalidSnowflake = "INVALID_SNOWFLAKE", InvalidParam = "INVALID_PARAM", ContentEmpty = "CONTENT_EMPTY", ContentTooLong = "CONTENT_TOO_LONG", ChannelNotFound = "CHANNEL_NOT_FOUND", NotSendable = "NOT_SENDABLE", SendFailed = "SEND_FAILED", ReactionFailed = "REACTION_FAILED", VoiceFailed = "VOICE_FAILED", DmUnavailable = "DM_UNAVAILABLE", NotSupported = "NOT_SUPPORTED", RateLimited = "RATE_LIMITED", Internal = "INTERNAL" } export interface PuppetError { code: PuppetErrorCode | string; message: string; } export interface PuppetRequest { id?: string | number; action: string; params?: Record; } 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 }; export interface PuppetEvent { type: string; [key: string]: unknown; } /** Hard safety limits applied core-side, regardless of what the daemon asks for. */ export const PUPPET_LIMITS = { maxContentLength: 2000, maxReadLimit: 100, maxFetchLimit: 100, maxUserSearchResults: 25, maxDmList: 50, minActionIntervalMs: 500, maxActionsPerMinute: 30, maxRequestBytes: 64 * 1024, maxChannelsPerGuild: 1000 }; const SNOWFLAKE_RE = /^\d{17,20}$/; export function isSnowflake(value: unknown): value is string { return typeof value === "string" && SNOWFLAKE_RE.test(value); }