├── .gitignore ├── .vscode ├── build.sh ├── config.sh ├── launch.json ├── setup.sh └── tasks.json ├── LICENSE ├── README.md ├── assets └── logo.png ├── backend ├── Dockerfile ├── Makefile └── entrypoint.sh ├── decky_plugin.pyi ├── defaults ├── deckcord_client.js ├── discord_client │ ├── __init__.py │ ├── event_handler.py │ └── store_access.py ├── gst_webrtc.py ├── tab_utils │ ├── __init__.py │ ├── cdp.py │ └── tab.py └── webrtc_client.js ├── main.py ├── package.json ├── plugin.json ├── pnpm-lock.yaml ├── rollup.config.js ├── src ├── components │ ├── DiscordTab.tsx │ ├── UploadScreenshot.tsx │ ├── VoiceChatViews.tsx │ └── buttons │ │ ├── DeafenButton.tsx │ │ ├── DisconnectButton.tsx │ │ ├── GoLiveButton.tsx │ │ ├── MuteButton.tsx │ │ └── PushToTalk.tsx ├── hooks │ └── useDeckcordState.ts ├── index.tsx ├── patches │ └── menuPatch.tsx └── types.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Coverage reports 17 | coverage 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | .pnpm-store 26 | 27 | # Editors 28 | .idea 29 | *.iml 30 | 31 | # OS metadata 32 | .DS_Store 33 | Thumbs.db 34 | 35 | # Ignore built ts files 36 | dist/ 37 | 38 | __pycache__/ 39 | 40 | /.yalc 41 | yalc.lock 42 | 43 | .vscode/settings.json 44 | 45 | # Ignore output folder 46 | 47 | backend/out 48 | 49 | # Make sure to ignore any instance of the loader's decky_plugin.py 50 | decky_plugin.py 51 | 52 | # Ignore decky CLI for building plugins 53 | out 54 | out/* 55 | cli/ 56 | cli/* 57 | cli/decky 58 | .vscode/tasks.json 59 | .vscode/setup.sh 60 | .vscode/defsettings.json 61 | -------------------------------------------------------------------------------- /.vscode/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | CLI_LOCATION="$(pwd)/cli" 3 | echo "Building plugin in $(pwd)" 4 | printf "Please input sudo password to proceed.\n" 5 | 6 | # read -s sudopass 7 | 8 | # printf "\n" 9 | 10 | echo $sudopass | sudo $CLI_LOCATION/decky plugin build $(pwd) 11 | -------------------------------------------------------------------------------- /.vscode/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; 3 | # printf "${SCRIPT_DIR}\n" 4 | # printf "$(dirname $0)\n" 5 | if ! [[ -e "${SCRIPT_DIR}/settings.json" ]]; then 6 | printf '.vscode/settings.json does not exist. Creating it with default settings. Exiting afterwards. Run your task again.\n\n' 7 | cp "${SCRIPT_DIR}/defsettings.json" "${SCRIPT_DIR}/settings.json" 8 | exit 1 9 | else 10 | printf '.vscode/settings.json does exist. Congrats.\n' 11 | printf 'Make sure to change settings.json to match your deck.\n' 12 | fi -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${cwd}/main.py", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | PNPM_INSTALLED="$(which pnpm)" 3 | DOCKER_INSTALLED="$(which docker)" 4 | CLI_INSTALLED="$(pwd)/cli/decky" 5 | 6 | # echo "$PNPM_INSTALLED" 7 | # echo "$DOCKER_INSTALLED" 8 | # echo "$CLI_INSTALLED" 9 | 10 | echo "If you are using alpine linux, do not expect any support." 11 | if [[ "$PNPM_INSTALLED" =~ "which" ]]; then 12 | echo "pnpm is not currently installed, you can install it via your distro's package managment system or via a script that will attempt to do a manual install based on your system. If you wish to proceed with installing via the script then answer "no" (capitals do not matter) and proceed with the rest of the script. Otherwise, just hit enter to proceed and use the script." 13 | read run_pnpm_script 14 | if [[ "$run_pnpm_script" =~ "n" ]]; then 15 | echo "You have chose to install pnpm via npm or your distros package manager. Please make sure to do so before attempting to build your plugin." 16 | else 17 | CURL_INSTALLED="$(which curl)" 18 | WGET_INSTALLED="$(which wget)" 19 | if [[ "$CURL_INSTALLED" =~ "which" ]]; then 20 | printf "curl not found, attempting with wget.\n" 21 | if [[ "$WGET_INSTALLED" =~ "which" ]]; then 22 | printf "wget not found, please install wget or curl.\n" 23 | printf "Could not install pnpm as curl and wget were not found.\n" 24 | else 25 | wget -qO- https://get.pnpm.io/install.sh | sh - 26 | fi 27 | else 28 | curl -fsSL https://get.pnpm.io/install.sh | sh - 29 | fi 30 | fi 31 | fi 32 | 33 | if [[ "$DOCKER_INSTALLED" =~ "which" ]]; then 34 | echo "Docker is not currently installed, in order build plugins with a backend you will need to have Docker installed. Please install Docker via the preferred method for your distribution." 35 | fi 36 | 37 | if ! test -f "$CLI_INSTALLED"; then 38 | echo "The Decky CLI tool (binary file is just called "decky") is used to build your plugin as a zip file which you can then install on your Steam Deck to perform testing. We highly recommend you install it. Hitting enter now will run the script to install Decky CLI and extract it to a folder called cli in the current plugin directory. You can also type 'no' and hit enter to skip this but keep in mind you will not have a usable plugin without building it." 39 | read run_cli_script 40 | if [[ "$run_cli_script" =~ "n" ]]; then 41 | echo "You have chosen to not install the Decky CLI tool to build your plugins. Please install this tool to build and test your plugin before submitting it to the Plugin Database." 42 | else 43 | mkdir $(pwd)/cli 44 | curl -L -o $(pwd)/cli/decky "https://github.com/SteamDeckHomebrew/cli/releases/download/0.0.1-alpha.11/decky" 45 | chmod +x $(pwd)/cli/decky 46 | echo "Decky CLI tool is now installed and you can build plugins into easy zip files using the "Build Zip" Task in vscodium." 47 | fi 48 | fi -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | //PRELIMINARY SETUP TASKS 5 | //Dependency setup task 6 | { 7 | "label": "depsetup", 8 | "type": "shell", 9 | "group": "none", 10 | "detail": "Install depedencies for basic setup", 11 | "linux": { 12 | "command": "${workspaceFolder}/.vscode/setup.sh", 13 | }, 14 | // // placeholder for windows scripts, not currently planned 15 | // "windows": { 16 | // "command": "call -c ${workspaceFolder}\\.vscode\\setup.bat", 17 | // }, 18 | "problemMatcher": [] 19 | }, 20 | //pnpm setup task to grab all needed modules 21 | { 22 | "label": "pnpmsetup", 23 | "type": "shell", 24 | "group": "none", 25 | "detail": "Setup pnpm", 26 | "command": "which pnpm && pnpm i", 27 | "problemMatcher": [] 28 | }, 29 | //Preliminary "All-in-one" setup task 30 | { 31 | "label": "setup", 32 | "detail": "Set up depedencies, pnpm and update Decky Frontend Library.", 33 | "dependsOrder": "sequence", 34 | "dependsOn": [ 35 | "depsetup", 36 | "pnpmsetup", 37 | "updatefrontendlib" 38 | ], 39 | "problemMatcher": [] 40 | }, 41 | //Preliminary Deploy Config Setup 42 | { 43 | "label": "settingscheck", 44 | "type": "shell", 45 | "group": "none", 46 | "detail": "Check that settings.json has been created", 47 | "linux": { 48 | "command": "${workspaceFolder}/.vscode/config.sh", 49 | }, 50 | // // placeholder for windows scripts, not currently planned 51 | // "windows": { 52 | // "command": "call ${workspaceFolder}\\.vscode\\config.bat", 53 | // }, 54 | "problemMatcher": [] 55 | }, 56 | //BUILD TASKS 57 | { 58 | "label": "cli-build", 59 | "group": "build", 60 | "detail": "Build plugin with CLI", 61 | "linux": { 62 | "command": "${workspaceFolder}/.vscode/build.sh", 63 | }, 64 | // // placeholder for windows logic, not currently planned 65 | // "windows": { 66 | // "command": "call ${workspaceFolder}\\.vscode\\build.bat", 67 | // }, 68 | "problemMatcher": [] 69 | }, 70 | //"All-in-one" build task 71 | { 72 | "label": "build", 73 | "group": "build", 74 | "detail": "Build decky-plugin-template", 75 | "dependsOrder": "sequence", 76 | "dependsOn": [ 77 | "settingscheck", 78 | "cli-build", 79 | ], 80 | "problemMatcher": [] 81 | }, 82 | //DEPLOY TASKS 83 | //Copies the zip file of the built plugin to the plugins folder 84 | { 85 | "label": "copyzip", 86 | "detail": "Deploy plugin zip to deck", 87 | "type": "shell", 88 | "group": "none", 89 | "dependsOn": [ 90 | "chmodplugins" 91 | ], 92 | "command": "rsync -azp --chmod=D0755,F0755 --rsh='ssh -p ${config:deckport} ${config:deckkey}' out/ ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/plugins", 93 | "problemMatcher": [] 94 | }, 95 | // 96 | { 97 | "label": "extractzip", 98 | "detail": "", 99 | "type": "shell", 100 | "group": "none", 101 | "command": "echo '${config:deckdir}/homebrew/plugins/${config:pluginname}.zip' && ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'echo ${config:deckpass} | sudo -S mkdir 755 -p \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|\")\" && echo ${config:deckpass} | sudo -S chown ${config:deckuser}:${config:deckuser} \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|\")\" && echo ${config:deckpass} | sudo -S bsdtar -xzpf \"${config:deckdir}/homebrew/plugins/${config:pluginname}.zip\" -C \"$(echo \"${config:deckdir}/homebrew/plugins/${config:pluginname}\" | sed \"s| |-|g\")\" --strip-components=1 --fflags '", 102 | "problemMatcher": [] 103 | }, 104 | //"All-in-one" deploy task 105 | { 106 | "label": "deploy", 107 | "dependsOrder": "sequence", 108 | "group": "none", 109 | "dependsOn": [ 110 | "copyzip", 111 | "extractzip" 112 | ], 113 | "problemMatcher": [] 114 | }, 115 | //"All-in-on" build & deploy task 116 | { 117 | "label": "builddeploy", 118 | "detail": "Builds plugin and deploys to deck", 119 | "dependsOrder": "sequence", 120 | "group": "none", 121 | "dependsOn": [ 122 | "build", 123 | "deploy" 124 | ], 125 | "problemMatcher": [] 126 | }, 127 | //GENERAL TASKS 128 | //Update Decky Frontend Library, aka DFL 129 | { 130 | "label": "updatefrontendlib", 131 | "type": "shell", 132 | "group": "build", 133 | "detail": "Update deck-frontend-lib aka DFL", 134 | "command": "pnpm update decky-frontend-lib --latest", 135 | "problemMatcher": [] 136 | }, 137 | //Used chmod plugins folder to allow copy-over of files 138 | { 139 | "label": "chmodplugins", 140 | "detail": "chmods plugins folder to prevent perms issues", 141 | "type": "shell", 142 | "group": "none", 143 | "command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'echo '${config:deckpass}' | sudo -S chmod -R ug+rw ${config:deckdir}/homebrew/plugins/'", 144 | "problemMatcher": [] 145 | }, 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Hypothetical Plugin Developer 4 | Original Copyright (c) 2022, Steam Deck Homebrew 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deckcord 2 | ## Discord on the Deck, made easy 3 | 4 | ## Features 5 | - Runs web discord as a separate tab in the background. 6 | - Open/Close easily while in-game from the main menu. 7 | - Mute/Deafen/Disconnect and check members in your channel from the QAM. 8 | - One-button post steam screenshots to your recent channels. 9 | - Show your current game as playing status. 10 | - Get notifications for DMs and pings in-game. 11 | - Push-to-talk support, with physical keybind to rear buttons. (R5 Button) 12 | - [Vencord](https://vencord.dev/) gets injected automatically before discord is loaded. It's needed to access a lot of the functionality that allows the plugin to do cool stuff, but also gives ya access to tons of other cool stuff. 13 | - Working screenshare with audio 14 | 15 | ## Credits 16 | - Huge thanks to [@aagaming](https://github.com/AAGaming00) for his enormous contributions towards getting mic working on the SteamClient tab, as well as his general support throughout the development of this plugin. 17 | - Huge thanks to [@Epictek](https://github.com/Epictek) for his help in getting QR Code login working. 18 | - Huge thanks to [@jessebofill](https://github.com/jessebofill) for the menu patching code. -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marios8543/Deckcord/804463e1d8127be5feed6042902f04b8a5983c0c/assets/logo.png -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/steamdeckhomebrew/holo-base:latest 2 | 3 | RUN mkdir /pacman 4 | RUN sed 's/# ParallelDownloads = 10/ParallelDownloads = 1/' /etc/pacman.conf 5 | #For some reason connection gets closed if i try to download too much stuff concurrently 6 | 7 | RUN pacman -Sydd --noconfirm --root /pacman --dbpath /var/lib/pacman \ 8 | gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly \ 9 | gst-plugins-base-libs gst-plugins-bad-libs gst-libav gst-plugin-pipewire \ 10 | faac libnice orc libcdio flac fluidsynth gupnp gupnp-igd gssdp libsrtp nss libvpx openssl 11 | 12 | RUN rm -rf /pacman/usr/lib/p11-kit-trust.so /pacman/usr/lib/libnssckbi.so 13 | 14 | COPY ./entrypoint.sh / 15 | RUN chmod +x /entrypoint.sh 16 | 17 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | # This is the default target, which will be built when 2 | # you invoke make 3 | # Taken from https://github.com/marissa999/decky-recorder 4 | current_dir = $(shell pwd) 5 | container_name = deckcord-backend 6 | .PHONY: all 7 | all: 8 | docker build -t $(container_name) . 9 | docker run --rm -i -v $(current_dir):/backend -v $(current_dir)/out:/backend/out --entrypoint /backend/entrypoint.sh $(container_name) 10 | rebuild: 11 | docker rmi $(container_name) 12 | docker build -t $(container_name) . -------------------------------------------------------------------------------- /backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Taken from https://github.com/marissa999/decky-recorder 3 | set -e 4 | 5 | OUTDIR="/backend/out" 6 | cd /backend 7 | cp -r /pacman/usr/lib/* /backend/out 8 | -------------------------------------------------------------------------------- /decky_plugin.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | This module exposes various constants and helpers useful for decky plugins. 3 | 4 | * Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. 5 | * Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. 6 | * Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. 7 | 8 | Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. 9 | 10 | Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. 11 | 12 | A logging facility `logger` is available which writes to the recommended location. 13 | """ 14 | 15 | __version__ = '0.1.0' 16 | 17 | import logging 18 | 19 | """ 20 | Constants 21 | """ 22 | 23 | HOME: str 24 | """ 25 | The home directory of the effective user running the process. 26 | Environment variable: `HOME`. 27 | If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. 28 | e.g.: `/home/deck` 29 | """ 30 | 31 | USER: str 32 | """ 33 | The effective username running the process. 34 | Environment variable: `USER`. 35 | It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. 36 | e.g.: `deck` 37 | """ 38 | 39 | DECKY_VERSION: str 40 | """ 41 | The version of the decky loader. 42 | Environment variable: `DECKY_VERSION`. 43 | e.g.: `v2.5.0-pre1` 44 | """ 45 | 46 | DECKY_USER: str 47 | """ 48 | The user whose home decky resides in. 49 | Environment variable: `DECKY_USER`. 50 | e.g.: `deck` 51 | """ 52 | 53 | 54 | DECKY_USER_HOME: str 55 | """ 56 | The home of the user where decky resides in. 57 | Environment variable: `DECKY_USER_HOME`. 58 | e.g.: `/home/deck` 59 | """ 60 | 61 | DECKY_HOME: str 62 | """ 63 | The root of the decky folder. 64 | Environment variable: `DECKY_HOME`. 65 | e.g.: `/home/deck/homebrew` 66 | """ 67 | 68 | DECKY_PLUGIN_SETTINGS_DIR: str 69 | """ 70 | The recommended path in which to store configuration files (created automatically). 71 | Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. 72 | e.g.: `/home/deck/homebrew/settings/decky-plugin-template` 73 | """ 74 | 75 | DECKY_PLUGIN_RUNTIME_DIR: str 76 | """ 77 | The recommended path in which to store runtime data (created automatically). 78 | Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. 79 | e.g.: `/home/deck/homebrew/data/decky-plugin-template` 80 | """ 81 | 82 | DECKY_PLUGIN_LOG_DIR: str 83 | """ 84 | The recommended path in which to store persistent logs (created automatically). 85 | Environment variable: `DECKY_PLUGIN_LOG_DIR`. 86 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template` 87 | """ 88 | 89 | DECKY_PLUGIN_DIR: str 90 | """ 91 | The root of the plugin's directory. 92 | Environment variable: `DECKY_PLUGIN_DIR`. 93 | e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` 94 | """ 95 | 96 | DECKY_PLUGIN_NAME: str 97 | """ 98 | The name of the plugin as specified in the 'plugin.json'. 99 | Environment variable: `DECKY_PLUGIN_NAME`. 100 | e.g.: `Example Plugin` 101 | """ 102 | 103 | DECKY_PLUGIN_VERSION: str 104 | """ 105 | The version of the plugin as specified in the 'package.json'. 106 | Environment variable: `DECKY_PLUGIN_VERSION`. 107 | e.g.: `0.0.1` 108 | """ 109 | 110 | DECKY_PLUGIN_AUTHOR: str 111 | """ 112 | The author of the plugin as specified in the 'plugin.json'. 113 | Environment variable: `DECKY_PLUGIN_AUTHOR`. 114 | e.g.: `John Doe` 115 | """ 116 | 117 | DECKY_PLUGIN_LOG: str 118 | """ 119 | The path to the plugin's main logfile. 120 | Environment variable: `DECKY_PLUGIN_LOG`. 121 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` 122 | """ 123 | 124 | """ 125 | Migration helpers 126 | """ 127 | 128 | 129 | def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: 130 | """ 131 | Migrate files and directories to a new location and remove old locations. 132 | Specified files will be migrated to `target_dir`. 133 | Specified directories will have their contents recursively migrated to `target_dir`. 134 | 135 | Returns the mapping of old -> new location. 136 | """ 137 | 138 | 139 | def migrate_settings(*files_or_directories: str) -> dict[str, str]: 140 | """ 141 | Migrate files and directories relating to plugin settings to the recommended location and remove old locations. 142 | Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 143 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 144 | 145 | Returns the mapping of old -> new location. 146 | """ 147 | 148 | 149 | def migrate_runtime(*files_or_directories: str) -> dict[str, str]: 150 | """ 151 | Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations 152 | Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 153 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 154 | 155 | Returns the mapping of old -> new location. 156 | """ 157 | 158 | 159 | def migrate_logs(*files_or_directories: str) -> dict[str, str]: 160 | """ 161 | Migrate files and directories relating to plugin logs to the recommended location and remove old locations. 162 | Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. 163 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. 164 | 165 | Returns the mapping of old -> new location. 166 | """ 167 | 168 | 169 | """ 170 | Logging 171 | """ 172 | 173 | logger: logging.Logger 174 | """The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" 175 | -------------------------------------------------------------------------------- /defaults/deckcord_client.js: -------------------------------------------------------------------------------- 1 | window.Vencord.Plugins.plugins.Deckcord = { 2 | name: "Deckcord", 3 | description: "Plugin required for Deckcord to work", 4 | authors: [], 5 | required: true, 6 | startAt: "DOMContentLoaded", 7 | async start() { 8 | navigator.mediaDevices.getUserMedia = (_) => new Promise(async (resolve, reject) => { 9 | if (window.MIC_STREAM != undefined && window.MIC_PEER_CONNECTION != undefined && window.MIC_PEER_CONNECTION.connectionState == "connected") { 10 | console.log("WebRTC stream available. Returning that."); 11 | return resolve(window.MIC_STREAM); 12 | } 13 | 14 | console.log("Starting WebRTC handshake for mic stream"); 15 | const peerConnection = new RTCPeerConnection(null); 16 | window.MIC_PEER_CONNECTION = peerConnection; 17 | 18 | window.DECKCORD_WS.addEventListener("message", async (e) => { 19 | const data = JSON.parse(e.data); 20 | if (data.type != "$webrtc") return; 21 | 22 | const remoteDescription = new RTCSessionDescription(data.payload); 23 | await peerConnection.setRemoteDescription(remoteDescription); 24 | }); 25 | 26 | peerConnection.addEventListener("icecandidate", event => { 27 | if (event.candidate) { 28 | window.DECKCORD_WS.send(JSON.stringify({ type: "$MIC_WEBRTC", ice: event.candidate })); 29 | } 30 | }); 31 | 32 | peerConnection.onaddstream = (ev) => { 33 | const stream = ev.stream; 34 | console.log("WEBRTC STREAM", stream); 35 | window.MIC_STREAM = stream; 36 | for (const track of stream.getTracks()) { 37 | track.stop = () => { console.log("CALLED STOP ON TRACK") } 38 | track 39 | } 40 | resolve(stream); 41 | } 42 | 43 | peerConnection.ontrack = (ev) => { 44 | ev.track.stop = () => { console.log("CALLED STOP ON TRACK") } 45 | } 46 | 47 | const offer = await peerConnection.createOffer({ offerToReceiveVideo: false, offerToReceiveAudio: true }); 48 | await peerConnection.setLocalDescription(offer); 49 | window.DECKCORD_WS.send(JSON.stringify({ type: "$MIC_WEBRTC", offer: offer })); 50 | }); 51 | 52 | function dataURLtoFile(dataurl, filename) { 53 | var arr = dataurl.split(','), 54 | mime = arr[0].match(/:(.*?);/)[1], 55 | bstr = atob(arr[arr.length - 1]), 56 | n = bstr.length, 57 | u8arr = new Uint8Array(n); 58 | while (n--) { 59 | u8arr[n] = bstr.charCodeAt(n); 60 | } 61 | return new File([u8arr], filename, { type: mime }); 62 | } 63 | 64 | function patchTypingField() { 65 | const t = setInterval(() => { 66 | try { 67 | document.getElementsByClassName("editor__66464")[0].onclick = (e) => fetch("http://127.0.0.1:65123/openkb", { mode: "no-cors" }); 68 | clearInterval(t); 69 | } catch (err) { } 70 | }, 100) 71 | } 72 | 73 | async function getAppId(name) { 74 | const res = await Vencord.Webpack.Common.RestAPI.get({ url: "/applications/detectable" }); 75 | if (res.ok) { 76 | const item = res.body.filter(e => e.name == name); 77 | if (item.length > 0) return item[0].id; 78 | } 79 | return "0"; 80 | } 81 | 82 | let CloudUpload; 83 | CloudUpload = Vencord.Webpack.findLazy(m => m.prototype?.trackUploadFinished);; 84 | function sendAttachmentToChannel(channelId, attachment_b64, filename) { 85 | return new Promise((resolve, reject) => { 86 | const file = dataURLtoFile(`data:text/plain;base64,${attachment_b64}`, filename); 87 | const upload = new CloudUpload({ 88 | file: file, 89 | isClip: false, 90 | isThumbnail: false, 91 | platform: 1, 92 | }, channelId, false, 0); 93 | upload.on("complete", () => { 94 | Vencord.Webpack.Common.RestAPI.post({ 95 | url: `/channels/${channelId}/messages`, 96 | body: { 97 | channel_id: channelId, 98 | content: "", 99 | nonce: Vencord.Webpack.Common.SnowflakeUtils.fromTimestamp(Date.now()), 100 | sticker_ids: [], 101 | type: 0, 102 | attachments: [{ 103 | id: "0", 104 | filename: upload.filename, 105 | uploaded_filename: upload.uploadedFilename 106 | }] 107 | } 108 | }); 109 | resolve(true); 110 | }); 111 | upload.on("error", () => resolve(false)) 112 | upload.upload(); 113 | }) 114 | } 115 | 116 | let MediaEngineStore, FluxDispatcher; 117 | console.log("Deckcord: Waiting for FluxDispatcher..."); 118 | Vencord.Webpack.waitFor(["subscribe", "dispatch", "register"], fdm => { 119 | FluxDispatcher = fdm; 120 | Vencord.Webpack.waitFor(Vencord.Webpack.filters.byStoreName("MediaEngineStore"), m => { 121 | MediaEngineStore = m; 122 | FluxDispatcher.dispatch({ type: "MEDIA_ENGINE_SET_AUDIO_ENABLED", enabled: true, unmute: true }); 123 | }); 124 | 125 | function connect() { 126 | window.DECKCORD_WS = new WebSocket('ws://127.0.0.1:65123/socket'); 127 | window.DECKCORD_WS.addEventListener("message", async function (e) { 128 | const data = JSON.parse(e.data); 129 | if (data.type.startsWith("$")) { 130 | let result; 131 | try { 132 | switch (data.type) { 133 | case "$getuser": 134 | result = Vencord.Webpack.Common.UserStore.getUser(data.id); 135 | break; 136 | case "$getchannel": 137 | result = Vencord.Webpack.Common.ChannelStore.getChannel(data.id); 138 | break; 139 | case "$getguild": 140 | result = Vencord.Webpack.Common.GuildStore.getGuild(data.id); 141 | break; 142 | case "$getmedia": 143 | result = { 144 | mute: MediaEngineStore.isSelfMute(), 145 | deaf: MediaEngineStore.isSelfDeaf(), 146 | live: MediaEngineStore.getGoLiveSource() != undefined 147 | } 148 | break; 149 | case "$get_last_channels": 150 | result = {} 151 | const ChannelStore = Vencord.Webpack.Common.ChannelStore; 152 | const GuildStore = Vencord.Webpack.Common.GuildStore; 153 | const channelIds = Object.values(JSON.parse(Vencord.Util.localStorage.SelectedChannelStore).mostRecentSelectedTextChannelIds); 154 | for (const chId of channelIds) { 155 | const ch = ChannelStore.getChannel(chId); 156 | const guild = GuildStore.getGuild(ch.guild_id); 157 | result[chId] = `${ch.name} (${guild.name})`; 158 | } 159 | break; 160 | case "$get_screen_bounds": 161 | result = { width: screen.width, height: screen.height } 162 | break; 163 | case "$ptt": 164 | try { 165 | MediaEngineStore.getMediaEngine().connections.values().next().value.setForceAudioInput(data.value); 166 | } catch (error) { } 167 | return; 168 | case "$setptt": 169 | FluxDispatcher.dispatch({ 170 | "type": "AUDIO_SET_MODE", 171 | "context": "default", 172 | "mode": data.enabled ? "PUSH_TO_TALK" : "VOICE_ACTIVITY", 173 | "options": MediaEngineStore.getSettings().modeOptions 174 | }); 175 | return; 176 | case "$rpc": 177 | FluxDispatcher.dispatch({ 178 | type: "LOCAL_ACTIVITY_UPDATE", 179 | activity: data.game ? { 180 | application_id: await getAppId(data.game), 181 | name: data.game, 182 | type: 0, 183 | flags: 1, 184 | timestamps: { start: Date.now() } 185 | } : {}, 186 | socketId: "CustomRPC", 187 | }); 188 | return; 189 | case "$screenshot": 190 | result = await sendAttachmentToChannel(data.channel_id, data.attachment_b64, "screenshot.jpg"); 191 | break; 192 | case "$golive": 193 | const vc_channel_id = Vencord.Webpack.findStore("SelectedChannelStore").getVoiceChannelId(); 194 | if (!vc_channel_id) return; 195 | const vc_guild_id = Vencord.Webpack.Common.ChannelStore.getChannel(vc_channel_id).guild_id; 196 | if (data.stop) Vencord.Webpack.wreq(799808).default(null, null, null); 197 | else Vencord.Webpack.wreq(799808).default(vc_guild_id, vc_channel_id, "Activity Panel"); 198 | return; 199 | case "$webrtc": 200 | return 201 | } 202 | } catch (error) { 203 | result = { error: error } 204 | if (data.increment == undefined) return; 205 | } 206 | const payload = { 207 | type: "$deckcord_request", 208 | increment: data.increment, 209 | result: result || {} 210 | }; 211 | console.debug(data, payload); 212 | window.DECKCORD_WS.send(JSON.stringify(payload)); 213 | return; 214 | } 215 | FluxDispatcher.dispatch(data); 216 | }); 217 | 218 | window.DECKCORD_WS.onopen = function (e) { 219 | navigator.mediaDevices.getUserMedia(); 220 | Vencord.Webpack.waitFor("useState", t => 221 | window.DECKCORD_WS.send(JSON.stringify({ 222 | type: "LOADED", 223 | result: true 224 | })) 225 | ); 226 | } 227 | 228 | window.DECKCORD_WS.onclose = function (e) { 229 | FluxDispatcher._interceptors.pop() 230 | setTimeout(function () { 231 | connect(); 232 | }, 100); 233 | }; 234 | 235 | window.DECKCORD_WS.onerror = function (err) { 236 | console.error('Socket encountered error: ', err.message, 'Closing socket'); 237 | window.DECKCORD_WS.close(); 238 | }; 239 | 240 | Vencord.Webpack.onceReady.then(t => 241 | window.DECKCORD_WS.send(JSON.stringify({ 242 | type: "CONNECTION_OPEN", 243 | user: Vencord.Webpack.Common.UserStore.getCurrentUser() 244 | })) 245 | ); 246 | 247 | FluxDispatcher.addInterceptor(e => { 248 | if (e.type == "CHANNEL_SELECT") patchTypingField(); 249 | const shouldPass = [ 250 | "CONNECTION_OPEN", 251 | "LOGOUT", 252 | "CONNECTION_CLOSED", 253 | "VOICE_STATE_UPDATES", 254 | "VOICE_CHANNEL_SELECT", 255 | "AUDIO_TOGGLE_SELF_MUTE", 256 | "AUDIO_TOGGLE_SELF_DEAF", 257 | "RPC_NOTIFICATION_CREATE", 258 | "STREAM_START", 259 | "STREAM_STOP" 260 | ].includes(e.type); 261 | if (shouldPass) { 262 | console.log("Dispatching Deckcord event: ", e); 263 | window.DECKCORD_WS.send(JSON.stringify(e)); 264 | } 265 | }); 266 | console.log("Deckcord: Added event interceptor"); 267 | } 268 | connect(); 269 | }); 270 | 271 | (() => { 272 | const t = setInterval(() => { 273 | try { 274 | if (window.location.pathname == "/login") { 275 | for (const el of document.getElementsByTagName('input')) { 276 | el.onclick = (ev) => fetch("http://127.0.0.1:65123/openkb", { mode: "no-cors" }); 277 | } 278 | } 279 | clearInterval(t); 280 | } 281 | catch (err) { } 282 | }, 100) 283 | })(); 284 | } 285 | }; -------------------------------------------------------------------------------- /defaults/discord_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marios8543/Deckcord/804463e1d8127be5feed6042902f04b8a5983c0c/defaults/discord_client/__init__.py -------------------------------------------------------------------------------- /defaults/discord_client/event_handler.py: -------------------------------------------------------------------------------- 1 | from json import dumps, loads 2 | from asyncio import sleep, get_event_loop, Task, Event, Queue 3 | from aiohttp import WSMsgType 4 | from aiohttp.web import WebSocketResponse 5 | from traceback import print_exception 6 | 7 | from .store_access import StoreAccess, User 8 | from decky_plugin import logger 9 | 10 | class EventHandler: 11 | def __init__(self) -> None: 12 | self.ws: WebSocketResponse 13 | self.api = StoreAccess() 14 | self.state_changed_event = Event() 15 | self.notification_queue = Queue() 16 | self.event_handlers = { 17 | "LOADED": self._loaded, 18 | "CONNECTION_OPEN": self._logged_in, 19 | "LOGOUT": self._logout, 20 | "CONNECTION_CLOSED": self._logout, 21 | "VOICE_STATE_UPDATES": self._voice_state_update, 22 | "VOICE_CHANNEL_SELECT": self._voice_channel_select, 23 | "AUDIO_TOGGLE_SELF_MUTE": self.toggle_mute, 24 | "AUDIO_TOGGLE_SELF_DEAF": self.toggle_deafen, 25 | "RPC_NOTIFICATION_CREATE": self._notification_create, 26 | "STREAM_STOP": self.toggle_mute, 27 | "STREAM_START": self.toggle_mute, 28 | "$MIC_WEBRTC": self._webrtc_mic_forward, 29 | } 30 | 31 | self.loaded = False 32 | self.logged_in = False 33 | self.me = User({"id": "", "username": "", "discriminator": None, "avatar": ""}) 34 | self.voicestates = {} 35 | 36 | self.vc_channel_id = "" 37 | self.vc_channel_name = "" 38 | self.vc_guild_name = "" 39 | 40 | self.webrtc = None 41 | 42 | async def yield_new_state(self): 43 | while True: 44 | await self.state_changed_event.wait() 45 | dc = self.build_state_dict() 46 | yield dc 47 | self.state_changed_event.clear() 48 | 49 | async def yield_notification(self): 50 | while True: 51 | yield await self.notification_queue.get() 52 | 53 | def build_state_dict(self): 54 | 55 | r = { 56 | "loaded": self.loaded, 57 | "logged_in": self.logged_in, 58 | "me": self.me.to_dict(), 59 | "vc": {}, 60 | "webrtc": self.webrtc.copy() if self.webrtc else None 61 | } 62 | if self.vc_channel_id: 63 | r["vc"]["channel_name"] = self.vc_channel_name 64 | r["vc"]["guild_name"] = self.vc_guild_name 65 | r["vc"]["users"] = [] 66 | if self.vc_channel_id in self.voicestates: 67 | for user in self.voicestates[self.vc_channel_id].values(): 68 | r["vc"]["users"].append(user.to_dict()) 69 | 70 | if self.webrtc: 71 | self.webrtc = None 72 | return r 73 | 74 | async def toggle_mute(self, *args, act=False): 75 | if act: 76 | await self.ws.send_json({"type": 'AUDIO_TOGGLE_SELF_MUTE', "context": 'default', "syncRemote": True}) 77 | r = await self.api.get_media() 78 | self.me.is_muted = r["mute"] 79 | self.me.is_deafened = r["deaf"] 80 | self.me.is_live = r["live"] 81 | 82 | async def toggle_deafen(self, *args, act=False): 83 | if act: 84 | await self.ws.send_json({"type": 'AUDIO_TOGGLE_SELF_DEAF', "context": 'default', "syncRemote": True}) 85 | r = await self.api.get_media() 86 | self.me.is_muted = r["mute"] 87 | self.me.is_deafened = r["deaf"] 88 | self.me.is_live = r["live"] 89 | 90 | async def disconnect_vc(self): 91 | await self.ws.send_json({"type":"VOICE_CHANNEL_SELECT","guildId":None,"channelId":None,"currentVoiceChannelId":self.vc_channel_id,"video":False,"stream":False}) 92 | 93 | async def main(self, ws): 94 | logger.info("Received WS Connection. Starting event processing loop") 95 | self.ws = ws 96 | self.api.ws = ws 97 | async for msg in self.ws: 98 | if msg.type == WSMsgType.TEXT: 99 | self._process_event(loads(msg.data)) 100 | elif msg.type == WSMsgType.ERROR: 101 | print('ws connection closed with exception %s' % self.ws.exception()) 102 | 103 | def _process_event(self, data): 104 | if data["type"] == "$ping": 105 | return 106 | if data["type"] == "$deckcord_request" and "increment" in data: 107 | self.api._set_result(data["increment"], data["result"]) 108 | return 109 | if data["type"] in self.event_handlers: 110 | callback = self.event_handlers[data["type"]] 111 | logger.info(f"Handling event: {data['type']}") 112 | #print(dumps(data, indent=2)+"\n\n") 113 | else: 114 | return 115 | def _(future: Task): 116 | self.state_changed_event.set() 117 | e = future.exception() 118 | if e: 119 | print(f"Exception during handling of {data['type']} event. {e}") 120 | print_exception(e) 121 | get_event_loop().create_task(callback(data)).add_done_callback(_) 122 | 123 | async def _loaded(self, data): 124 | self.loaded = True 125 | 126 | async def _logged_in(self, data): 127 | self.logged_in = True 128 | self.me = User(data["user"]) 129 | 130 | s = await self.api.get_media() 131 | self.me.is_muted = s["mute"] 132 | self.me.is_deafened = s["deaf"] 133 | self.me.is_live = s["live"] 134 | 135 | async def _logout(self, data): 136 | self.logged_in = False 137 | 138 | async def _voice_channel_select(self, data): 139 | self.vc_channel_id = data["channelId"] 140 | if not self.vc_channel_id: 141 | self.vc_channel_name = "" 142 | self.vc_guild_name = "" 143 | return 144 | self.vc_channel_name = (await self.api.get_channel(self.vc_channel_id))["name"] 145 | if "guildId" in data and data["guildId"]: 146 | self.vc_guild_name = (await self.api.get_guild(data["guildId"]))["name"] 147 | for user in self.voicestates[self.vc_channel_id].values(): 148 | await user.populate(self.api) 149 | 150 | async def _voice_state_update(self, data): 151 | states = data["voiceStates"] 152 | for state in states: 153 | if "oldChannelId" in state and state["oldChannelId"] in self.voicestates: 154 | self.voicestates[state["oldChannelId"]].pop(state["userId"], None) 155 | if not self.voicestates[state["oldChannelId"]]: 156 | self.voicestates.pop(state["oldChannelId"], None) 157 | if state["userId"] == self.me.id: 158 | user_to_add = self.me 159 | else: 160 | user_to_add = User.from_vc(state) 161 | if state["channelId"] == self.vc_channel_id: 162 | await user_to_add.populate(self.api) 163 | if state["channelId"] in self.voicestates: 164 | self.voicestates[state["channelId"]][state["userId"]] = user_to_add 165 | else: 166 | self.voicestates[state["channelId"]] = {state["userId"]: user_to_add} 167 | 168 | async def _notification_create(self, data): 169 | await self.notification_queue.put(data) 170 | 171 | async def _webrtc_mic_forward(self, data): 172 | self.webrtc = data -------------------------------------------------------------------------------- /defaults/discord_client/store_access.py: -------------------------------------------------------------------------------- 1 | from asyncio import Event 2 | 3 | class User: 4 | def __init__(self, data) -> None: 5 | self.id = data["id"] 6 | self.name = data["username"] 7 | self.discriminator = data["discriminator"] 8 | self.avatar = data["avatar"] 9 | 10 | self.is_muted = False 11 | self.is_deafened = False 12 | self.is_live = False 13 | 14 | @classmethod 15 | def from_vc(self, data): 16 | usr = User({"id": data["userId"], "username": "", "discriminator": None, "avatar": ""}) 17 | usr.is_muted = data["mute"] 18 | usr.is_deafened = data["deaf"] 19 | return usr 20 | 21 | async def populate(self, api): 22 | if self.name: 23 | return 24 | r = await api.get_user(self.id) 25 | self.name = r["username"] 26 | self.discriminator = r["discriminator"] 27 | self.avatar = r["avatar"] 28 | 29 | def to_dict(self): 30 | return { 31 | "id": self.id, 32 | "username": str(self), 33 | "avatar": self.avatar, 34 | "is_muted": self.is_muted, 35 | "is_deafened": self.is_deafened, 36 | "is_live": self.is_live 37 | } 38 | 39 | def __str__(self) -> str: 40 | return f"{self.name}{'#'+self.discriminator if self.discriminator and self.discriminator != '0' else ''}" 41 | 42 | class Response: 43 | def __init__(self) -> None: 44 | self.lock = Event() 45 | self.response = None 46 | 47 | class StoreAccess: 48 | def __init__(self) -> None: 49 | self.request_increment = 0 50 | self.requests = {} 51 | 52 | def _set_result(self, increment, result): 53 | response = self.requests[increment] 54 | response.result = result 55 | response.lock.set() 56 | 57 | async def _store_access_request(self, command, id="", **kwargs): 58 | self.request_increment += 1 59 | response = Response() 60 | self.requests[self.request_increment] = response 61 | await self.ws.send_json({"type": command, "id": id, "increment": self.request_increment, **kwargs}) 62 | await response.lock.wait() 63 | return response.result 64 | 65 | async def get_user(self, id): 66 | return await self._store_access_request("$getuser", id) 67 | 68 | async def get_channel(self, id): 69 | return await self._store_access_request("$getchannel", id) 70 | 71 | async def get_guild(self, id): 72 | return await self._store_access_request("$getguild", id) 73 | 74 | async def get_media(self): 75 | return await self._store_access_request("$getmedia") 76 | 77 | async def get_last_channels(self): 78 | return await self._store_access_request("$get_last_channels") 79 | 80 | async def post_screenshot(self, channel_id, data): 81 | return await self._store_access_request("$screenshot", channel_id=channel_id, attachment_b64=data) 82 | 83 | async def get_screen_bounds(self): 84 | return await self._store_access_request("$get_screen_bounds") -------------------------------------------------------------------------------- /defaults/gst_webrtc.py: -------------------------------------------------------------------------------- 1 | #Most of the code here is adapted from https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-examples/webrtc/sendrecv/gst/webrtc_sendrecv.py 2 | #Code for setting up pipelinesrc and audio pipeline is from https://github.com/marissa999/decky-recorder 3 | 4 | import aiohttp 5 | from aiohttp import web 6 | from logging import getLogger 7 | from gi import require_version 8 | from asyncio import run_coroutine_threadsafe, get_event_loop 9 | from subprocess import getoutput 10 | 11 | log = getLogger("webrtc") 12 | 13 | require_version("Gst", "1.0") 14 | require_version("GstWebRTC", "1.0") 15 | require_version("GstSdp", "1.0") 16 | from gi.repository import Gst, GstWebRTC, GstSdp 17 | 18 | PIPELINE_DESC = """ 19 | webrtcbin name=send latency=0 stun-server=stun://stun.l.google.com:19302 20 | turn-server=turn://gstreamer:IsGreatWhenYouCanGetItToWork@webrtc.nirbheek.in:3478 21 | pipewiresrc do-timestamp=true ! videoconvert ! queue ! 22 | vp8enc deadline=1 keyframe-max-dist=2000 ! rtpvp8pay picture-id-mode=15-bit ! 23 | queue ! application/x-rtp,media=video,encoding-name=VP8,payload={video_pt} ! send. 24 | pulsesrc device="Recording_{monitor}" ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! 25 | queue ! application/x-rtp,media=audio,encoding-name=OPUS,payload={audio_pt} ! send. 26 | """ 27 | 28 | def get_payload_types(sdpmsg, video_encoding, audio_encoding): 29 | video_pt = None 30 | audio_pt = None 31 | for i in range(0, sdpmsg.medias_len()): 32 | media = sdpmsg.get_media(i) 33 | for j in range(0, media.formats_len()): 34 | fmt = media.get_format(j) 35 | if fmt == "webrtc-datachannel": 36 | continue 37 | pt = int(fmt) 38 | caps = media.get_caps_from_media(pt) 39 | s = caps.get_structure(0) 40 | encoding_name = s.get_string("encoding-name") 41 | if video_pt is None and encoding_name == video_encoding: 42 | video_pt = pt 43 | elif audio_pt is None and encoding_name == audio_encoding: 44 | audio_pt = pt 45 | ret = {video_encoding: video_pt, audio_encoding: audio_pt} 46 | print(ret) 47 | return ret 48 | 49 | class WebRTCServer: 50 | def __init__(self, app = web.Application()) -> None: 51 | Gst.init(None) 52 | 53 | self.loop = get_event_loop() 54 | self.app = app 55 | self.app.add_routes([web.get("/webrtc", self.websocket_handler)]) 56 | 57 | self.webrtc = None 58 | self.remote_ws = None 59 | 60 | def start_pipeline(self, create_offer=True, audio_pt=96, video_pt=97): 61 | audio_monitor = getoutput("pactl get-default-sink").splitlines()[0] + ".monitor" 62 | log.info(f"Creating pipeline, create_offer: {create_offer}") 63 | desc = PIPELINE_DESC.format(video_pt=video_pt, audio_pt=audio_pt, monitor=audio_monitor) 64 | self.pipe = Gst.parse_launch(desc) 65 | self.webrtc = self.pipe.get_by_name("send") 66 | self.webrtc.connect("on-negotiation-needed", self.on_negotiation_needed, create_offer) 67 | self.webrtc.connect("on-ice-candidate", self.send_ice_candidate_message) 68 | self.pipe.set_state(Gst.State.PLAYING) 69 | 70 | def close_pipeline(self): 71 | if self.pipe: 72 | self.pipe.set_state(Gst.State.NULL) 73 | self.pipe = None 74 | self.webrtc = None 75 | 76 | def on_negotiation_needed(self, _, create_offer): 77 | if create_offer: 78 | log.info('Call was connected: creating offer') 79 | promise = Gst.Promise.new_with_change_func(self.on_offer_created, None, None) 80 | self.webrtc.emit('create-offer', None, promise) 81 | 82 | def send_ice_candidate_message(self, _, mlineindex, candidate): 83 | icemsg = {'ice': {'candidate': candidate, 'sdpMLineIndex': mlineindex}} 84 | run_coroutine_threadsafe(self.remote_ws.send_json(icemsg), self.loop) 85 | 86 | def on_offer_set(self, promise, _, __): 87 | assert promise.wait() == Gst.PromiseResult.REPLIED 88 | promise = Gst.Promise.new_with_change_func(self.on_answer_created, None, None) 89 | self.webrtc.emit("create-answer", None, promise) 90 | 91 | def on_answer_created(self, promise, _, __): 92 | assert promise.wait() == Gst.PromiseResult.REPLIED 93 | reply = promise.get_reply() 94 | answer = reply.get_value("answer") 95 | promise = Gst.Promise.new() 96 | self.webrtc.emit("set-local-description", answer, promise) 97 | promise.interrupt() 98 | print(answer) 99 | self.send_sdp(answer) 100 | 101 | def send_sdp(self, offer): 102 | text = offer.sdp.as_text() 103 | log.info("Sending answer:\n%s" % text) 104 | msg = {'sdp': {'type': 'answer', 'sdp': text}} 105 | run_coroutine_threadsafe(self.remote_ws.send_json(msg), self.loop) 106 | 107 | async def websocket_handler(self, request): 108 | ws = web.WebSocketResponse() 109 | await ws.prepare(request) 110 | self.remote_ws = ws 111 | 112 | async for msg in ws: 113 | if msg.type == aiohttp.WSMsgType.TEXT: 114 | data = msg.json() 115 | if "offer" in data: 116 | res, sdpmsg = GstSdp.SDPMessage.new_from_text(data["offer"]["sdp"]) 117 | if not self.webrtc: 118 | log.info("Incoming call: received an offer, creating pipeline") 119 | pts = get_payload_types(sdpmsg, video_encoding="VP8", audio_encoding="OPUS") 120 | assert "VP8" in pts 121 | assert "OPUS" in pts 122 | self.start_pipeline(create_offer=False, video_pt=pts["VP8"], audio_pt=pts["OPUS"]) 123 | 124 | assert self.webrtc 125 | offer = GstWebRTC.WebRTCSessionDescription.new(GstWebRTC.WebRTCSDPType.OFFER, sdpmsg) 126 | promise = Gst.Promise.new_with_change_func(self.on_offer_set, None, None) 127 | self.webrtc.emit("set-remote-description", offer, promise) 128 | elif "ice" in data: 129 | assert self.webrtc 130 | candidate = data['ice']['candidate'] 131 | sdpmlineindex = data['ice']['sdpMLineIndex'] 132 | self.webrtc.emit('add-ice-candidate', sdpmlineindex, candidate) 133 | elif "stop" in data: 134 | await ws.close() 135 | break 136 | 137 | self.close_pipeline() 138 | return ws 139 | 140 | def main(): 141 | app = WebRTCServer() 142 | web.run_app(app.app, port=65124, loop=app.loop) 143 | 144 | if __name__ == "__main__": 145 | main() -------------------------------------------------------------------------------- /defaults/tab_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marios8543/Deckcord/804463e1d8127be5feed6042902f04b8a5983c0c/defaults/tab_utils/__init__.py -------------------------------------------------------------------------------- /defaults/tab_utils/cdp.py: -------------------------------------------------------------------------------- 1 | # Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there. 2 | 3 | from asyncio import sleep, run 4 | from typing import List 5 | 6 | from aiohttp import ClientSession 7 | from aiohttp.client_exceptions import ClientConnectorError, ClientOSError 8 | from asyncio.exceptions import TimeoutError 9 | 10 | BASE_ADDRESS = "http://127.0.0.1:8080" 11 | 12 | class Tab: 13 | cmd_id = 0 14 | 15 | def __init__(self, res) -> None: 16 | self.title = res["title"] 17 | self.id = res["id"] 18 | self.url = res["url"] 19 | self.ws_url = res["webSocketDebuggerUrl"] 20 | 21 | self.websocket = None 22 | self.client = None 23 | 24 | async def ensure_open(self): 25 | if self.websocket.closed: 26 | await self.open_websocket() 27 | 28 | async def open_websocket(self): 29 | self.client = ClientSession() 30 | self.websocket = await self.client.ws_connect(self.ws_url) 31 | 32 | async def close_websocket(self): 33 | await self.websocket.close() 34 | await self.client.close() 35 | 36 | async def listen_for_message(self): 37 | async for message in self.websocket: 38 | data = message.json() 39 | yield data 40 | await self.close_websocket() 41 | 42 | async def _send_devtools_cmd(self, dc, receive=True): 43 | if self.websocket: 44 | self.cmd_id += 1 45 | dc["id"] = self.cmd_id 46 | await self.websocket.send_json(dc) 47 | if receive: 48 | async for msg in self.listen_for_message(): 49 | if "id" in msg and msg["id"] == dc["id"]: 50 | return msg 51 | return None 52 | raise RuntimeError("Websocket not opened") 53 | 54 | async def close(self, manage_socket=True): 55 | try: 56 | if manage_socket: 57 | await self.open_websocket() 58 | 59 | res = await self._send_devtools_cmd({ 60 | "method": "Page.close", 61 | }, False) 62 | 63 | finally: 64 | if manage_socket: 65 | await self.close_websocket() 66 | return res 67 | 68 | async def enable(self): 69 | """ 70 | Enables page domain notifications. 71 | """ 72 | await self._send_devtools_cmd({ 73 | "method": "Page.enable", 74 | }, False) 75 | 76 | async def evaluate(self, js, wait=False): 77 | return await self._send_devtools_cmd({ 78 | "method": "Runtime.evaluate", 79 | "params": { 80 | "expression": js 81 | } 82 | }, wait) 83 | 84 | async def set_request_interception(self, patterns = None): 85 | return await self._send_devtools_cmd({ 86 | "method": "Network.setRequestInterception", 87 | "params": { 88 | "patterns": patterns 89 | } 90 | }) 91 | 92 | async def enable_fetch(self, patterns = None): 93 | return await self._send_devtools_cmd({ 94 | "method": "Fetch.enable", 95 | "params": { 96 | "patterns": patterns 97 | } 98 | }, False) 99 | 100 | async def enable_net(self): 101 | return await self._send_devtools_cmd({ 102 | "method": "Network.enable" 103 | }) 104 | 105 | async def disable_net(self): 106 | return await self._send_devtools_cmd({ 107 | "method": "Network.disable" 108 | }) 109 | 110 | async def disable_fetch(self): 111 | return await self._send_devtools_cmd({ 112 | "method": "Fetch.disable", 113 | }) 114 | 115 | async def continue_request(self, request_id, url=None): 116 | return await self._send_devtools_cmd({ 117 | "method": "Fetch.continueRequest", 118 | "params": { 119 | "requestId": request_id, 120 | "url": url 121 | # "interceptResponse": intercept_response 122 | } 123 | }, False) 124 | 125 | async def fulfill_request(self, request_id, response_code=None, response_headers=None, body=None): 126 | return await self._send_devtools_cmd({ 127 | "method": "Fetch.fulfillRequest", 128 | "params": { 129 | "requestId": request_id, 130 | "responseCode": response_code, 131 | "responseHeaders": response_headers, 132 | "body": body 133 | } 134 | }, False) 135 | 136 | 137 | async def get_tabs() -> List[Tab]: 138 | res = {} 139 | 140 | na = False 141 | while True: 142 | try: 143 | async with ClientSession() as web: 144 | res = await web.get(f"{BASE_ADDRESS}/json", timeout=3) 145 | except ClientConnectorError: 146 | if not na: 147 | na = True 148 | await sleep(5) 149 | except ClientOSError: 150 | await sleep(1) 151 | except TimeoutError: 152 | await sleep(1) 153 | else: 154 | break 155 | 156 | if res.status == 200: 157 | r = await res.json() 158 | return [Tab(i) for i in r] 159 | else: 160 | raise Exception(f"/json did not return 200. {await res.text()}") 161 | 162 | async def get_tab_lambda(test) -> Tab: 163 | tabs = await get_tabs() 164 | tab = next((i for i in tabs if test(i)), None) 165 | if not tab: 166 | raise ValueError(f"Tab not found by lambda") 167 | return tab 168 | 169 | async def get_tab(tab_name) -> Tab: 170 | tabs = await get_tabs() 171 | tab = next((i for i in tabs if i.title == tab_name), None) 172 | if not tab: 173 | raise ValueError(f"Tab {tab_name} not found") 174 | return tab -------------------------------------------------------------------------------- /defaults/tab_utils/tab.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from aiohttp import ClientSession 4 | from .cdp import Tab, get_tab, get_tab_lambda 5 | from asyncio import sleep 6 | from ssl import create_default_context 7 | 8 | async def create_discord_tab(): 9 | while True: 10 | try: 11 | tab = await get_tab("SharedJSContext") 12 | break 13 | except: 14 | await sleep(0.1) 15 | await tab.open_websocket() 16 | while True: 17 | await tab.evaluate(""" 18 | if (window.DISCORD_TAB !== undefined) { 19 | window.DISCORD_TAB.m_browserView.SetVisible(false); 20 | window.DISCORD_TAB.Destroy(); 21 | window.DISCORD_TAB = undefined; 22 | } 23 | window.DISCORD_TAB = window.DFL.Router.WindowStore.GamepadUIMainWindowInstance.CreateBrowserView("discord"); 24 | window.DISCORD_TAB.WIDTH = 860; 25 | window.DISCORD_TAB.HEIGHT = 495; 26 | window.DISCORD_TAB.m_browserView.SetBounds(0,0, window.DISCORD_TAB.WIDTH, window.DISCORD_TAB.HEIGHT); 27 | window.DISCORD_TAB.m_browserView.LoadURL("data:text/plain,to_be_discord"); 28 | 29 | DFL.Router.WindowStore.GamepadUIMainWindowInstance.m_VirtualKeyboardManager.IsShowingVirtualKeyboard.m_callbacks.m_vecCallbacks.push((e) => { 30 | if (!e) { 31 | const bounds = window.DISCORD_TAB.m_browserView.GetBounds(); 32 | if (bounds.height != window.DISCORD_TAB.HEIGHT) { 33 | window.DISCORD_TAB.m_browserView.SetBounds(0,0, window.DISCORD_TAB.WIDTH, window.DISCORD_TAB.HEIGHT); 34 | } 35 | } 36 | else { 37 | const bounds = window.DISCORD_TAB.m_browserView.GetBounds(); 38 | if (bounds.height != window.DISCORD_TAB.HEIGHT * 0.6) { 39 | window.DISCORD_TAB.m_browserView.SetBounds(0,0, window.DISCORD_TAB.WIDTH, window.DISCORD_TAB.HEIGHT * 0.6); 40 | } 41 | } 42 | }); 43 | """) 44 | await sleep(3) 45 | try: 46 | discord_tab = await get_tab_lambda(lambda tab: tab.url == "data:text/plain,to_be_discord") 47 | if discord_tab: 48 | await tab.close_websocket() 49 | return discord_tab 50 | except: 51 | pass 52 | 53 | async def fetch_vencord(): 54 | async with ClientSession() as session: 55 | res = await session.get("https://raw.githubusercontent.com/Vencord/builds/main/browser.js", 56 | ssl=create_default_context(cafile="/etc/ssl/cert.pem")) 57 | if res.ok: 58 | return await res.text() 59 | 60 | async def setup_discord_tab(tab: Tab): 61 | await tab.open_websocket() 62 | await tab.enable() 63 | await tab._send_devtools_cmd({ 64 | "method": "Page.addScriptToEvaluateOnNewDocument", 65 | "params": { 66 | "source": 67 | "Object.hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)" + 68 | await fetch_vencord() + 69 | open(Path(__file__).parent.parent.joinpath("deckcord_client.js"), "r").read() + 70 | open(Path(__file__).parent.parent.joinpath("webrtc_client.js"), "r").read(), 71 | "runImmediately": True 72 | } 73 | }) 74 | 75 | async def boot_discord(tab: Tab): 76 | await tab._send_devtools_cmd({ 77 | "method": "Page.navigate", 78 | "params": { 79 | "url": "https://discord.com/app", 80 | "transitionType": "address_bar" 81 | } 82 | }) 83 | 84 | async def setOSK(tab: Tab, state): 85 | if state: 86 | await tab.evaluate("DISCORD_TAB.m_virtualKeyboardHost.m_showKeyboard()") 87 | else: 88 | await tab.evaluate("DISCORD_TAB.m_virtualKeyboardHost.m_hideKeyboard()") -------------------------------------------------------------------------------- /defaults/webrtc_client.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | let waitingForMedia = false; 3 | const getRTCStream = (_) => new Promise((resolve, reject) => { 4 | if (window.DECKCORD_RTC_STREAM) return resolve(window.DECKCORD_RTC_STREAM); 5 | 6 | if (waitingForMedia) return reject(); 7 | waitingForMedia = true; 8 | 9 | const peerConnection = new RTCPeerConnection(null); 10 | const ws = new WebSocket("ws://127.0.0.1:65124/webrtc"); 11 | 12 | window.DECKCORD_PEER_CONNECTION = peerConnection; 13 | 14 | ws.onopen = async (_) => { 15 | const offer = await peerConnection.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }); 16 | await peerConnection.setLocalDescription(offer); 17 | ws.send(JSON.stringify({ "offer": offer })); 18 | 19 | peerConnection.addEventListener("icecandidate", event => { 20 | if (event.candidate) { 21 | ws.send(JSON.stringify({ "ice": event.candidate })); 22 | } 23 | }); 24 | } 25 | ws.onmessage = async (event) => { 26 | const data = JSON.parse(event.data); 27 | if (data.sdp) { 28 | const remoteDescription = new RTCSessionDescription(data.sdp); 29 | console.log(remoteDescription); 30 | await peerConnection.setRemoteDescription(remoteDescription); 31 | } 32 | else if (data.ice) { 33 | console.log(data.ice); 34 | await peerConnection.addIceCandidate(data.ice); 35 | } 36 | } 37 | 38 | peerConnection.onconnectionstatechange = (ev) => { 39 | if (peerConnection.connectionState == "failed") { 40 | waitingForMedia = false; 41 | reject("rtc peer connection failed"); 42 | } 43 | } 44 | 45 | peerConnection.onaddstream = (ev) => { 46 | const stream = ev.stream; 47 | if (stream.getVideoTracks().length == 0) return; 48 | 49 | window.DECKCORD_RTC_STREAM = stream; 50 | for (const track of stream.getTracks()) { 51 | track.stop = () => { 52 | ws.send(JSON.stringify({"stop": ""})); 53 | peerConnection.close(); 54 | window.DECKCORD_RTC_STREAM = undefined; 55 | } 56 | } 57 | waitingForMedia = false; 58 | resolve(stream); 59 | } 60 | }); 61 | 62 | window.navigator.mediaDevices.getDisplayMedia = getRTCStream; 63 | })(); -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from aiohttp.web import ( 2 | Application, 3 | get, 4 | WebSocketResponse, 5 | AppRunner, 6 | TCPSite, 7 | ) 8 | from asyncio import sleep, create_task, create_subprocess_exec 9 | import aiohttp_cors 10 | from json import dumps 11 | from pathlib import Path 12 | from subprocess import PIPE 13 | 14 | import os, sys 15 | 16 | sys.path.append(os.path.dirname(__file__)) 17 | 18 | from tab_utils.tab import ( 19 | create_discord_tab, 20 | setup_discord_tab, 21 | boot_discord, 22 | setOSK, 23 | ) 24 | from tab_utils.cdp import Tab, get_tab 25 | from discord_client.event_handler import EventHandler 26 | 27 | from decky_plugin import logger, DECKY_PLUGIN_DIR 28 | from logging import INFO 29 | 30 | logger.setLevel(INFO) 31 | 32 | 33 | async def stream_watcher(stream, is_err=False): 34 | async for line in stream: 35 | line = line.decode("utf-8") 36 | if not line.strip(): 37 | continue 38 | if is_err: 39 | logger.debug("ERROR: " + line) 40 | else: 41 | logger.debug(line) 42 | 43 | async def initialize(): 44 | tab = await create_discord_tab() 45 | await setup_discord_tab(tab) 46 | await boot_discord(tab) 47 | 48 | create_task(watchdog(tab)) 49 | 50 | async def watchdog(tab: Tab): 51 | while True: 52 | while not tab.websocket.closed: 53 | await sleep(1) 54 | logger.info("Discord tab websocket is no longer open. Trying to reconnect...") 55 | try: 56 | await tab.open_websocket() 57 | logger.info("Reconnected") 58 | except: 59 | break 60 | logger.info("Discord has died. Re-initializing...") 61 | while True: 62 | try: 63 | await initialize() 64 | break 65 | except: 66 | await sleep(1) 67 | 68 | class Plugin: 69 | server = Application() 70 | cors = aiohttp_cors.setup( 71 | server, 72 | defaults={ 73 | "*": aiohttp_cors.ResourceOptions( 74 | expose_headers="*", allow_headers="*", allow_credentials=True 75 | ) 76 | }, 77 | ) 78 | evt_handler = EventHandler() 79 | 80 | async def _main(self): 81 | logger.info("Starting Deckcord backend") 82 | await initialize() 83 | logger.info("Discord initialized") 84 | 85 | Plugin.server.add_routes( 86 | [ 87 | get("/openkb", Plugin._openkb), 88 | get("/socket", Plugin._websocket_handler), 89 | get("/frontend_socket", Plugin._frontend_socket_handler) 90 | ] 91 | ) 92 | for r in list(Plugin.server.router.routes())[:-1]: 93 | Plugin.cors.add(r) 94 | Plugin.runner = AppRunner(Plugin.server, access_log=None) 95 | await Plugin.runner.setup() 96 | logger.info("Starting server.") 97 | await TCPSite(Plugin.runner, "0.0.0.0", 65123).start() 98 | 99 | Plugin.shared_js_tab = await get_tab("SharedJSContext") 100 | await Plugin.shared_js_tab.open_websocket() 101 | create_task(Plugin._notification_dispatcher()) 102 | 103 | Plugin.webrtc_server = await create_subprocess_exec( 104 | "/usr/bin/python", 105 | str(Path(DECKY_PLUGIN_DIR) / "gst_webrtc.py"), 106 | env={ 107 | "LD_LIBRARY_PATH": str(Path(DECKY_PLUGIN_DIR) / "bin"), 108 | "GI_TYPELIB_PATH": str(Path(DECKY_PLUGIN_DIR) / "bin/girepository-1.0"), 109 | "GST_PLUGIN_PATH": str(Path(DECKY_PLUGIN_DIR) / "bin/gstreamer-1.0"), 110 | "GST_VAAPI_ALL_DRIVERS": "1", 111 | "OPENSSL_CONF": "/etc/ssl/openssl.cnf", 112 | "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus", 113 | "XDG_RUNTIME_DIR": "/run/user/1000", 114 | "XDG_DATA_DIRS": "/home/deck/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share", 115 | "LIBVA_DRIVER_NAME": "radeonsi", 116 | }, 117 | stdout=PIPE, 118 | stderr=PIPE, 119 | ) 120 | create_task(stream_watcher(Plugin.webrtc_server.stdout)) 121 | create_task(stream_watcher(Plugin.webrtc_server.stderr, True)) 122 | 123 | while True: 124 | await sleep(3600) 125 | 126 | async def _openkb(request): 127 | await Plugin.shared_js_tab.ensure_open() 128 | await setOSK(Plugin.shared_js_tab, True) 129 | logger.info("Setting discord visibility to true") 130 | return "OK" 131 | 132 | async def _websocket_handler(request): 133 | logger.info("Received websocket connection!") 134 | ws = WebSocketResponse(max_msg_size=0) 135 | await ws.prepare(request) 136 | await Plugin.evt_handler.main(ws) 137 | 138 | 139 | last_ws: WebSocketResponse = None 140 | async def _frontend_socket_handler(request): 141 | if Plugin.last_ws: 142 | await Plugin.last_ws.close() 143 | logger.info("Received frontend websocket connection!") 144 | ws = WebSocketResponse(max_msg_size=0) 145 | Plugin.last_ws = ws 146 | await ws.prepare(request) 147 | async for state in Plugin.evt_handler.yield_new_state(): 148 | await ws.send_json(state) 149 | 150 | async def _notification_dispatcher(): 151 | async for notification in Plugin.evt_handler.yield_notification(): 152 | logger.info("Dispatching notification") 153 | payload = dumps( 154 | {"title": notification["title"], "body": notification["body"]} 155 | ) 156 | await Plugin.shared_js_tab.ensure_open() 157 | await Plugin.shared_js_tab.evaluate(f"window.DECKCORD.dispatchNotification(JSON.parse('{payload}'));") 158 | 159 | async def connect_ws(*args): 160 | await Plugin.shared_js_tab.ensure_open() 161 | await Plugin.shared_js_tab.evaluate(f"window.DECKCORD.connectWs()") 162 | 163 | async def get_state(*args): 164 | return Plugin.evt_handler.build_state_dict() 165 | 166 | async def toggle_mute(*args): 167 | logger.info("Toggling mute") 168 | return await Plugin.evt_handler.toggle_mute(act=True) 169 | 170 | async def toggle_deafen(*args): 171 | logger.info("Toggling deafen") 172 | return await Plugin.evt_handler.toggle_deafen(act=True) 173 | 174 | async def disconnect_vc(*args): 175 | logger.info("Disconnecting vc") 176 | return await Plugin.evt_handler.disconnect_vc() 177 | 178 | async def set_ptt(plugin, value): 179 | await Plugin.evt_handler.ws.send_json({"type": "$ptt", "value": value}) 180 | 181 | async def enable_ptt(plugin, enabled): 182 | await Plugin.evt_handler.ws.send_json({"type": "$setptt", "enabled": enabled}) 183 | 184 | async def set_rpc(plugin, game): 185 | logger.info("Setting RPC") 186 | await Plugin.evt_handler.ws.send_json({"type": "$rpc", "game": game}) 187 | 188 | async def get_last_channels(plugin): 189 | return await plugin.evt_handler.api.get_last_channels() 190 | 191 | async def post_screenshot(plugin, channel_id, data): 192 | logger.info("Posting screenshot to " + channel_id) 193 | r = await Plugin.evt_handler.api.post_screenshot(channel_id, data) 194 | if r: 195 | return True 196 | payload = dumps({"title": "Deckcord", "body": "Error while posting screenshot"}) 197 | await Plugin.shared_js_tab.ensure_open() 198 | await Plugin.shared_js_tab.evaluate( 199 | f"DeckyPluginLoader.toaster.toast(JSON.parse('{payload}'));" 200 | ) 201 | 202 | async def get_screen_bounds(plugin): 203 | return await plugin.evt_handler.api.get_screen_bounds() 204 | 205 | async def go_live(plugin): 206 | await plugin.evt_handler.ws.send_json({"type": "$golive", "stop": False}) 207 | 208 | async def stop_go_live(plugin): 209 | await plugin.evt_handler.ws.send_json({"type": "$golive", "stop": True}) 210 | 211 | async def mic_webrtc_answer(plugin, answer): 212 | await plugin.evt_handler.ws.send_json({"type": "$webrtc", "payload": answer}) 213 | 214 | async def _unload(*args): 215 | if hasattr(Plugin, "webrtc_server"): 216 | Plugin.webrtc_server.kill() 217 | await Plugin.webrtc_server.wait() 218 | if hasattr(Plugin, "runner"): 219 | await Plugin.runner.shutdown() 220 | await Plugin.runner.cleanup() 221 | if hasattr(Plugin, "shared_js_tab"): 222 | await Plugin.shared_js_tab.ensure_open() 223 | await Plugin.shared_js_tab.evaluate( 224 | """ 225 | window.DISCORD_TAB.m_browserView.SetVisible(false); 226 | window.DISCORD_TAB.Destroy(); 227 | window.DISCORD_TAB = undefined; 228 | """ 229 | ) 230 | await Plugin.shared_js_tab.close_websocket() 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deckcord", 3 | "version": "0.3.4", 4 | "description": "Easy access to discord in game mode, and related functionalities", 5 | "scripts": { 6 | "build": "shx rm -rf dist && rollup -c", 7 | "watch": "rollup -c -w", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/marios8543/Deckcord.git" 13 | }, 14 | "keywords": [ 15 | "decky", 16 | "plugin", 17 | "plugin-template", 18 | "steam-deck", 19 | "deck" 20 | ], 21 | "author": "marios8543", 22 | "license": "BSD-3-Clause", 23 | "bugs": { 24 | "url": "https://github.com/marios8543/Deckcord/issues" 25 | }, 26 | "homepage": "https://github.com/marios8543/Deckcord#readme", 27 | "devDependencies": { 28 | "@rollup/plugin-commonjs": "^21.1.0", 29 | "@rollup/plugin-json": "^4.1.0", 30 | "@rollup/plugin-node-resolve": "^13.3.0", 31 | "@rollup/plugin-replace": "^4.0.0", 32 | "@rollup/plugin-typescript": "^8.3.3", 33 | "@types/react": "16.14.0", 34 | "@types/webpack": "^5.28.0", 35 | "rollup": "^2.77.1", 36 | "rollup-plugin-import-assets": "^1.1.1", 37 | "shx": "^0.3.4", 38 | "tslib": "^2.4.0", 39 | "typescript": "^4.7.4" 40 | }, 41 | "dependencies": { 42 | "decky-frontend-lib": "^3.24.1", 43 | "react-icons": "^4.4.0" 44 | }, 45 | "pnpm": { 46 | "peerDependencyRules": { 47 | "ignoreMissing": [ 48 | "react", 49 | "react-dom" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Deckcord", 3 | "author": "marios8543", 4 | "flags": [], 5 | "publish": { 6 | "tags": ["discord", "chat"], 7 | "description": "Easy access to Discord", 8 | "image": "https://opengraph.githubassets.com/1/SteamDeckHomebrew/PluginLoader" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | decky-frontend-lib: 5 | specifier: ^3.24.1 6 | version: 3.24.1 7 | react-icons: 8 | specifier: ^4.4.0 9 | version: 4.4.0 10 | 11 | devDependencies: 12 | '@rollup/plugin-commonjs': 13 | specifier: ^21.1.0 14 | version: 21.1.0(rollup@2.77.1) 15 | '@rollup/plugin-json': 16 | specifier: ^4.1.0 17 | version: 4.1.0(rollup@2.77.1) 18 | '@rollup/plugin-node-resolve': 19 | specifier: ^13.3.0 20 | version: 13.3.0(rollup@2.77.1) 21 | '@rollup/plugin-replace': 22 | specifier: ^4.0.0 23 | version: 4.0.0(rollup@2.77.1) 24 | '@rollup/plugin-typescript': 25 | specifier: ^8.3.3 26 | version: 8.3.3(rollup@2.77.1)(tslib@2.4.0)(typescript@4.7.4) 27 | '@types/react': 28 | specifier: 16.14.0 29 | version: 16.14.0 30 | '@types/webpack': 31 | specifier: ^5.28.0 32 | version: 5.28.0 33 | rollup: 34 | specifier: ^2.77.1 35 | version: 2.77.1 36 | rollup-plugin-import-assets: 37 | specifier: ^1.1.1 38 | version: 1.1.1(rollup@2.77.1) 39 | shx: 40 | specifier: ^0.3.4 41 | version: 0.3.4 42 | tslib: 43 | specifier: ^2.4.0 44 | version: 2.4.0 45 | typescript: 46 | specifier: ^4.7.4 47 | version: 4.7.4 48 | 49 | packages: 50 | 51 | /@jridgewell/gen-mapping@0.3.3: 52 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 53 | engines: {node: '>=6.0.0'} 54 | dependencies: 55 | '@jridgewell/set-array': 1.1.2 56 | '@jridgewell/sourcemap-codec': 1.4.15 57 | '@jridgewell/trace-mapping': 0.3.20 58 | dev: true 59 | 60 | /@jridgewell/resolve-uri@3.1.1: 61 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 62 | engines: {node: '>=6.0.0'} 63 | dev: true 64 | 65 | /@jridgewell/set-array@1.1.2: 66 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 67 | engines: {node: '>=6.0.0'} 68 | dev: true 69 | 70 | /@jridgewell/source-map@0.3.5: 71 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 72 | dependencies: 73 | '@jridgewell/gen-mapping': 0.3.3 74 | '@jridgewell/trace-mapping': 0.3.20 75 | dev: true 76 | 77 | /@jridgewell/sourcemap-codec@1.4.15: 78 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 79 | dev: true 80 | 81 | /@jridgewell/trace-mapping@0.3.20: 82 | resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} 83 | dependencies: 84 | '@jridgewell/resolve-uri': 3.1.1 85 | '@jridgewell/sourcemap-codec': 1.4.15 86 | dev: true 87 | 88 | /@rollup/plugin-commonjs@21.1.0(rollup@2.77.1): 89 | resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==} 90 | engines: {node: '>= 8.0.0'} 91 | peerDependencies: 92 | rollup: ^2.38.3 93 | dependencies: 94 | '@rollup/pluginutils': 3.1.0(rollup@2.77.1) 95 | commondir: 1.0.1 96 | estree-walker: 2.0.2 97 | glob: 7.2.3 98 | is-reference: 1.2.1 99 | magic-string: 0.25.9 100 | resolve: 1.22.8 101 | rollup: 2.77.1 102 | dev: true 103 | 104 | /@rollup/plugin-json@4.1.0(rollup@2.77.1): 105 | resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} 106 | peerDependencies: 107 | rollup: ^1.20.0 || ^2.0.0 108 | dependencies: 109 | '@rollup/pluginutils': 3.1.0(rollup@2.77.1) 110 | rollup: 2.77.1 111 | dev: true 112 | 113 | /@rollup/plugin-node-resolve@13.3.0(rollup@2.77.1): 114 | resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==} 115 | engines: {node: '>= 10.0.0'} 116 | peerDependencies: 117 | rollup: ^2.42.0 118 | dependencies: 119 | '@rollup/pluginutils': 3.1.0(rollup@2.77.1) 120 | '@types/resolve': 1.17.1 121 | deepmerge: 4.3.1 122 | is-builtin-module: 3.2.1 123 | is-module: 1.0.0 124 | resolve: 1.22.8 125 | rollup: 2.77.1 126 | dev: true 127 | 128 | /@rollup/plugin-replace@4.0.0(rollup@2.77.1): 129 | resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==} 130 | peerDependencies: 131 | rollup: ^1.20.0 || ^2.0.0 132 | dependencies: 133 | '@rollup/pluginutils': 3.1.0(rollup@2.77.1) 134 | magic-string: 0.25.9 135 | rollup: 2.77.1 136 | dev: true 137 | 138 | /@rollup/plugin-typescript@8.3.3(rollup@2.77.1)(tslib@2.4.0)(typescript@4.7.4): 139 | resolution: {integrity: sha512-55L9SyiYu3r/JtqdjhwcwaECXP7JeJ9h1Sg1VWRJKIutla2MdZQodTgcCNybXLMCnqpNLEhS2vGENww98L1npg==} 140 | engines: {node: '>=8.0.0'} 141 | peerDependencies: 142 | rollup: ^2.14.0 143 | tslib: '*' 144 | typescript: '>=3.7.0' 145 | peerDependenciesMeta: 146 | tslib: 147 | optional: true 148 | dependencies: 149 | '@rollup/pluginutils': 3.1.0(rollup@2.77.1) 150 | resolve: 1.22.8 151 | rollup: 2.77.1 152 | tslib: 2.4.0 153 | typescript: 4.7.4 154 | dev: true 155 | 156 | /@rollup/pluginutils@3.1.0(rollup@2.77.1): 157 | resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} 158 | engines: {node: '>= 8.0.0'} 159 | peerDependencies: 160 | rollup: ^1.20.0||^2.0.0 161 | dependencies: 162 | '@types/estree': 0.0.39 163 | estree-walker: 1.0.1 164 | picomatch: 2.3.1 165 | rollup: 2.77.1 166 | dev: true 167 | 168 | /@types/eslint-scope@3.7.7: 169 | resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} 170 | dependencies: 171 | '@types/eslint': 8.56.0 172 | '@types/estree': 1.0.5 173 | dev: true 174 | 175 | /@types/eslint@8.56.0: 176 | resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==} 177 | dependencies: 178 | '@types/estree': 1.0.5 179 | '@types/json-schema': 7.0.15 180 | dev: true 181 | 182 | /@types/estree@0.0.39: 183 | resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} 184 | dev: true 185 | 186 | /@types/estree@1.0.5: 187 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 188 | dev: true 189 | 190 | /@types/json-schema@7.0.15: 191 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 192 | dev: true 193 | 194 | /@types/node@20.10.5: 195 | resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} 196 | dependencies: 197 | undici-types: 5.26.5 198 | dev: true 199 | 200 | /@types/prop-types@15.7.11: 201 | resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} 202 | dev: true 203 | 204 | /@types/react@16.14.0: 205 | resolution: {integrity: sha512-jJjHo1uOe+NENRIBvF46tJimUvPnmbQ41Ax0pEm7pRvhPg+wuj8VMOHHiMvaGmZRzRrCtm7KnL5OOE/6kHPK8w==} 206 | dependencies: 207 | '@types/prop-types': 15.7.11 208 | csstype: 3.1.3 209 | dev: true 210 | 211 | /@types/resolve@1.17.1: 212 | resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} 213 | dependencies: 214 | '@types/node': 20.10.5 215 | dev: true 216 | 217 | /@types/webpack@5.28.0: 218 | resolution: {integrity: sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==} 219 | dependencies: 220 | '@types/node': 20.10.5 221 | tapable: 2.2.1 222 | webpack: 5.89.0 223 | transitivePeerDependencies: 224 | - '@swc/core' 225 | - esbuild 226 | - uglify-js 227 | - webpack-cli 228 | dev: true 229 | 230 | /@webassemblyjs/ast@1.11.6: 231 | resolution: {integrity: sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==} 232 | dependencies: 233 | '@webassemblyjs/helper-numbers': 1.11.6 234 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 235 | dev: true 236 | 237 | /@webassemblyjs/floating-point-hex-parser@1.11.6: 238 | resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} 239 | dev: true 240 | 241 | /@webassemblyjs/helper-api-error@1.11.6: 242 | resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} 243 | dev: true 244 | 245 | /@webassemblyjs/helper-buffer@1.11.6: 246 | resolution: {integrity: sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==} 247 | dev: true 248 | 249 | /@webassemblyjs/helper-numbers@1.11.6: 250 | resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} 251 | dependencies: 252 | '@webassemblyjs/floating-point-hex-parser': 1.11.6 253 | '@webassemblyjs/helper-api-error': 1.11.6 254 | '@xtuc/long': 4.2.2 255 | dev: true 256 | 257 | /@webassemblyjs/helper-wasm-bytecode@1.11.6: 258 | resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} 259 | dev: true 260 | 261 | /@webassemblyjs/helper-wasm-section@1.11.6: 262 | resolution: {integrity: sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==} 263 | dependencies: 264 | '@webassemblyjs/ast': 1.11.6 265 | '@webassemblyjs/helper-buffer': 1.11.6 266 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 267 | '@webassemblyjs/wasm-gen': 1.11.6 268 | dev: true 269 | 270 | /@webassemblyjs/ieee754@1.11.6: 271 | resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} 272 | dependencies: 273 | '@xtuc/ieee754': 1.2.0 274 | dev: true 275 | 276 | /@webassemblyjs/leb128@1.11.6: 277 | resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} 278 | dependencies: 279 | '@xtuc/long': 4.2.2 280 | dev: true 281 | 282 | /@webassemblyjs/utf8@1.11.6: 283 | resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} 284 | dev: true 285 | 286 | /@webassemblyjs/wasm-edit@1.11.6: 287 | resolution: {integrity: sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==} 288 | dependencies: 289 | '@webassemblyjs/ast': 1.11.6 290 | '@webassemblyjs/helper-buffer': 1.11.6 291 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 292 | '@webassemblyjs/helper-wasm-section': 1.11.6 293 | '@webassemblyjs/wasm-gen': 1.11.6 294 | '@webassemblyjs/wasm-opt': 1.11.6 295 | '@webassemblyjs/wasm-parser': 1.11.6 296 | '@webassemblyjs/wast-printer': 1.11.6 297 | dev: true 298 | 299 | /@webassemblyjs/wasm-gen@1.11.6: 300 | resolution: {integrity: sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==} 301 | dependencies: 302 | '@webassemblyjs/ast': 1.11.6 303 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 304 | '@webassemblyjs/ieee754': 1.11.6 305 | '@webassemblyjs/leb128': 1.11.6 306 | '@webassemblyjs/utf8': 1.11.6 307 | dev: true 308 | 309 | /@webassemblyjs/wasm-opt@1.11.6: 310 | resolution: {integrity: sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==} 311 | dependencies: 312 | '@webassemblyjs/ast': 1.11.6 313 | '@webassemblyjs/helper-buffer': 1.11.6 314 | '@webassemblyjs/wasm-gen': 1.11.6 315 | '@webassemblyjs/wasm-parser': 1.11.6 316 | dev: true 317 | 318 | /@webassemblyjs/wasm-parser@1.11.6: 319 | resolution: {integrity: sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==} 320 | dependencies: 321 | '@webassemblyjs/ast': 1.11.6 322 | '@webassemblyjs/helper-api-error': 1.11.6 323 | '@webassemblyjs/helper-wasm-bytecode': 1.11.6 324 | '@webassemblyjs/ieee754': 1.11.6 325 | '@webassemblyjs/leb128': 1.11.6 326 | '@webassemblyjs/utf8': 1.11.6 327 | dev: true 328 | 329 | /@webassemblyjs/wast-printer@1.11.6: 330 | resolution: {integrity: sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==} 331 | dependencies: 332 | '@webassemblyjs/ast': 1.11.6 333 | '@xtuc/long': 4.2.2 334 | dev: true 335 | 336 | /@xtuc/ieee754@1.2.0: 337 | resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} 338 | dev: true 339 | 340 | /@xtuc/long@4.2.2: 341 | resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} 342 | dev: true 343 | 344 | /acorn-import-assertions@1.9.0(acorn@8.11.2): 345 | resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} 346 | peerDependencies: 347 | acorn: ^8 348 | dependencies: 349 | acorn: 8.11.2 350 | dev: true 351 | 352 | /acorn@8.11.2: 353 | resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} 354 | engines: {node: '>=0.4.0'} 355 | hasBin: true 356 | dev: true 357 | 358 | /ajv-keywords@3.5.2(ajv@6.12.6): 359 | resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} 360 | peerDependencies: 361 | ajv: ^6.9.1 362 | dependencies: 363 | ajv: 6.12.6 364 | dev: true 365 | 366 | /ajv@6.12.6: 367 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 368 | dependencies: 369 | fast-deep-equal: 3.1.3 370 | fast-json-stable-stringify: 2.1.0 371 | json-schema-traverse: 0.4.1 372 | uri-js: 4.4.1 373 | dev: true 374 | 375 | /balanced-match@1.0.2: 376 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 377 | dev: true 378 | 379 | /brace-expansion@1.1.11: 380 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 381 | dependencies: 382 | balanced-match: 1.0.2 383 | concat-map: 0.0.1 384 | dev: true 385 | 386 | /browserslist@4.22.2: 387 | resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} 388 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 389 | hasBin: true 390 | dependencies: 391 | caniuse-lite: 1.0.30001570 392 | electron-to-chromium: 1.4.615 393 | node-releases: 2.0.14 394 | update-browserslist-db: 1.0.13(browserslist@4.22.2) 395 | dev: true 396 | 397 | /buffer-from@1.1.2: 398 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 399 | dev: true 400 | 401 | /builtin-modules@3.3.0: 402 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 403 | engines: {node: '>=6'} 404 | dev: true 405 | 406 | /caniuse-lite@1.0.30001570: 407 | resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==} 408 | dev: true 409 | 410 | /chrome-trace-event@1.0.3: 411 | resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} 412 | engines: {node: '>=6.0'} 413 | dev: true 414 | 415 | /commander@2.20.3: 416 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 417 | dev: true 418 | 419 | /commondir@1.0.1: 420 | resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} 421 | dev: true 422 | 423 | /concat-map@0.0.1: 424 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 425 | dev: true 426 | 427 | /csstype@3.1.3: 428 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 429 | dev: true 430 | 431 | /decky-frontend-lib@3.24.1: 432 | resolution: {integrity: sha512-VGxLTPetxx/pQVC+t8odTHrwQAh7uy4bO2Od2gGWSTfmUUoxtAcEtiXGyE9mKsoD6t7QNHrGvgXn78sf2i/IeQ==} 433 | dev: false 434 | 435 | /deepmerge@4.3.1: 436 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 437 | engines: {node: '>=0.10.0'} 438 | dev: true 439 | 440 | /electron-to-chromium@1.4.615: 441 | resolution: {integrity: sha512-/bKPPcgZVUziECqDc+0HkT87+0zhaWSZHNXqF8FLd2lQcptpmUFwoCSWjCdOng9Gdq+afKArPdEg/0ZW461Eng==} 442 | dev: true 443 | 444 | /enhanced-resolve@5.15.0: 445 | resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} 446 | engines: {node: '>=10.13.0'} 447 | dependencies: 448 | graceful-fs: 4.2.11 449 | tapable: 2.2.1 450 | dev: true 451 | 452 | /es-module-lexer@1.4.1: 453 | resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} 454 | dev: true 455 | 456 | /escalade@3.1.1: 457 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 458 | engines: {node: '>=6'} 459 | dev: true 460 | 461 | /eslint-scope@5.1.1: 462 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 463 | engines: {node: '>=8.0.0'} 464 | dependencies: 465 | esrecurse: 4.3.0 466 | estraverse: 4.3.0 467 | dev: true 468 | 469 | /esrecurse@4.3.0: 470 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 471 | engines: {node: '>=4.0'} 472 | dependencies: 473 | estraverse: 5.3.0 474 | dev: true 475 | 476 | /estraverse@4.3.0: 477 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 478 | engines: {node: '>=4.0'} 479 | dev: true 480 | 481 | /estraverse@5.3.0: 482 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 483 | engines: {node: '>=4.0'} 484 | dev: true 485 | 486 | /estree-walker@0.6.1: 487 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 488 | dev: true 489 | 490 | /estree-walker@1.0.1: 491 | resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} 492 | dev: true 493 | 494 | /estree-walker@2.0.2: 495 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 496 | dev: true 497 | 498 | /events@3.3.0: 499 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 500 | engines: {node: '>=0.8.x'} 501 | dev: true 502 | 503 | /fast-deep-equal@3.1.3: 504 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 505 | dev: true 506 | 507 | /fast-json-stable-stringify@2.1.0: 508 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 509 | dev: true 510 | 511 | /fs.realpath@1.0.0: 512 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 513 | dev: true 514 | 515 | /fsevents@2.3.3: 516 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 517 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 518 | os: [darwin] 519 | requiresBuild: true 520 | dev: true 521 | optional: true 522 | 523 | /function-bind@1.1.2: 524 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 525 | dev: true 526 | 527 | /glob-to-regexp@0.4.1: 528 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 529 | dev: true 530 | 531 | /glob@7.2.3: 532 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 533 | dependencies: 534 | fs.realpath: 1.0.0 535 | inflight: 1.0.6 536 | inherits: 2.0.4 537 | minimatch: 3.1.2 538 | once: 1.4.0 539 | path-is-absolute: 1.0.1 540 | dev: true 541 | 542 | /graceful-fs@4.2.11: 543 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 544 | dev: true 545 | 546 | /has-flag@4.0.0: 547 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 548 | engines: {node: '>=8'} 549 | dev: true 550 | 551 | /hasown@2.0.0: 552 | resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} 553 | engines: {node: '>= 0.4'} 554 | dependencies: 555 | function-bind: 1.1.2 556 | dev: true 557 | 558 | /inflight@1.0.6: 559 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 560 | dependencies: 561 | once: 1.4.0 562 | wrappy: 1.0.2 563 | dev: true 564 | 565 | /inherits@2.0.4: 566 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 567 | dev: true 568 | 569 | /interpret@1.4.0: 570 | resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} 571 | engines: {node: '>= 0.10'} 572 | dev: true 573 | 574 | /is-builtin-module@3.2.1: 575 | resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} 576 | engines: {node: '>=6'} 577 | dependencies: 578 | builtin-modules: 3.3.0 579 | dev: true 580 | 581 | /is-core-module@2.13.1: 582 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 583 | dependencies: 584 | hasown: 2.0.0 585 | dev: true 586 | 587 | /is-module@1.0.0: 588 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 589 | dev: true 590 | 591 | /is-reference@1.2.1: 592 | resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} 593 | dependencies: 594 | '@types/estree': 1.0.5 595 | dev: true 596 | 597 | /jest-worker@27.5.1: 598 | resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 599 | engines: {node: '>= 10.13.0'} 600 | dependencies: 601 | '@types/node': 20.10.5 602 | merge-stream: 2.0.0 603 | supports-color: 8.1.1 604 | dev: true 605 | 606 | /json-parse-even-better-errors@2.3.1: 607 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 608 | dev: true 609 | 610 | /json-schema-traverse@0.4.1: 611 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 612 | dev: true 613 | 614 | /loader-runner@4.3.0: 615 | resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} 616 | engines: {node: '>=6.11.5'} 617 | dev: true 618 | 619 | /magic-string@0.25.9: 620 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 621 | dependencies: 622 | sourcemap-codec: 1.4.8 623 | dev: true 624 | 625 | /merge-stream@2.0.0: 626 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 627 | dev: true 628 | 629 | /mime-db@1.52.0: 630 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 631 | engines: {node: '>= 0.6'} 632 | dev: true 633 | 634 | /mime-types@2.1.35: 635 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 636 | engines: {node: '>= 0.6'} 637 | dependencies: 638 | mime-db: 1.52.0 639 | dev: true 640 | 641 | /minimatch@3.1.2: 642 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 643 | dependencies: 644 | brace-expansion: 1.1.11 645 | dev: true 646 | 647 | /minimist@1.2.8: 648 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 649 | dev: true 650 | 651 | /neo-async@2.6.2: 652 | resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} 653 | dev: true 654 | 655 | /node-releases@2.0.14: 656 | resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} 657 | dev: true 658 | 659 | /once@1.4.0: 660 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 661 | dependencies: 662 | wrappy: 1.0.2 663 | dev: true 664 | 665 | /path-is-absolute@1.0.1: 666 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 667 | engines: {node: '>=0.10.0'} 668 | dev: true 669 | 670 | /path-parse@1.0.7: 671 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 672 | dev: true 673 | 674 | /picocolors@1.0.0: 675 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 676 | dev: true 677 | 678 | /picomatch@2.3.1: 679 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 680 | engines: {node: '>=8.6'} 681 | dev: true 682 | 683 | /punycode@2.3.1: 684 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 685 | engines: {node: '>=6'} 686 | dev: true 687 | 688 | /randombytes@2.1.0: 689 | resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} 690 | dependencies: 691 | safe-buffer: 5.2.1 692 | dev: true 693 | 694 | /react-icons@4.4.0: 695 | resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==} 696 | peerDependencies: 697 | react: '*' 698 | peerDependenciesMeta: 699 | react: 700 | optional: true 701 | dev: false 702 | 703 | /rechoir@0.6.2: 704 | resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} 705 | engines: {node: '>= 0.10'} 706 | dependencies: 707 | resolve: 1.22.8 708 | dev: true 709 | 710 | /resolve@1.22.8: 711 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 712 | hasBin: true 713 | dependencies: 714 | is-core-module: 2.13.1 715 | path-parse: 1.0.7 716 | supports-preserve-symlinks-flag: 1.0.0 717 | dev: true 718 | 719 | /rollup-plugin-import-assets@1.1.1(rollup@2.77.1): 720 | resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} 721 | peerDependencies: 722 | rollup: '>=1.9.0' 723 | dependencies: 724 | rollup: 2.77.1 725 | rollup-pluginutils: 2.8.2 726 | url-join: 4.0.1 727 | dev: true 728 | 729 | /rollup-pluginutils@2.8.2: 730 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 731 | dependencies: 732 | estree-walker: 0.6.1 733 | dev: true 734 | 735 | /rollup@2.77.1: 736 | resolution: {integrity: sha512-GhutNJrvTYD6s1moo+kyq7lD9DeR5HDyXo4bDFlDSkepC9kVKY+KK/NSZFzCmeXeia3kEzVuToQmHRdugyZHxw==} 737 | engines: {node: '>=10.0.0'} 738 | hasBin: true 739 | optionalDependencies: 740 | fsevents: 2.3.3 741 | dev: true 742 | 743 | /safe-buffer@5.2.1: 744 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 745 | dev: true 746 | 747 | /schema-utils@3.3.0: 748 | resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} 749 | engines: {node: '>= 10.13.0'} 750 | dependencies: 751 | '@types/json-schema': 7.0.15 752 | ajv: 6.12.6 753 | ajv-keywords: 3.5.2(ajv@6.12.6) 754 | dev: true 755 | 756 | /serialize-javascript@6.0.1: 757 | resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} 758 | dependencies: 759 | randombytes: 2.1.0 760 | dev: true 761 | 762 | /shelljs@0.8.5: 763 | resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} 764 | engines: {node: '>=4'} 765 | hasBin: true 766 | dependencies: 767 | glob: 7.2.3 768 | interpret: 1.4.0 769 | rechoir: 0.6.2 770 | dev: true 771 | 772 | /shx@0.3.4: 773 | resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} 774 | engines: {node: '>=6'} 775 | hasBin: true 776 | dependencies: 777 | minimist: 1.2.8 778 | shelljs: 0.8.5 779 | dev: true 780 | 781 | /source-map-support@0.5.21: 782 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 783 | dependencies: 784 | buffer-from: 1.1.2 785 | source-map: 0.6.1 786 | dev: true 787 | 788 | /source-map@0.6.1: 789 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 790 | engines: {node: '>=0.10.0'} 791 | dev: true 792 | 793 | /sourcemap-codec@1.4.8: 794 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 795 | deprecated: Please use @jridgewell/sourcemap-codec instead 796 | dev: true 797 | 798 | /supports-color@8.1.1: 799 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 800 | engines: {node: '>=10'} 801 | dependencies: 802 | has-flag: 4.0.0 803 | dev: true 804 | 805 | /supports-preserve-symlinks-flag@1.0.0: 806 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 807 | engines: {node: '>= 0.4'} 808 | dev: true 809 | 810 | /tapable@2.2.1: 811 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 812 | engines: {node: '>=6'} 813 | dev: true 814 | 815 | /terser-webpack-plugin@5.3.9(webpack@5.89.0): 816 | resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==} 817 | engines: {node: '>= 10.13.0'} 818 | peerDependencies: 819 | '@swc/core': '*' 820 | esbuild: '*' 821 | uglify-js: '*' 822 | webpack: ^5.1.0 823 | peerDependenciesMeta: 824 | '@swc/core': 825 | optional: true 826 | esbuild: 827 | optional: true 828 | uglify-js: 829 | optional: true 830 | dependencies: 831 | '@jridgewell/trace-mapping': 0.3.20 832 | jest-worker: 27.5.1 833 | schema-utils: 3.3.0 834 | serialize-javascript: 6.0.1 835 | terser: 5.26.0 836 | webpack: 5.89.0 837 | dev: true 838 | 839 | /terser@5.26.0: 840 | resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==} 841 | engines: {node: '>=10'} 842 | hasBin: true 843 | dependencies: 844 | '@jridgewell/source-map': 0.3.5 845 | acorn: 8.11.2 846 | commander: 2.20.3 847 | source-map-support: 0.5.21 848 | dev: true 849 | 850 | /tslib@2.4.0: 851 | resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} 852 | dev: true 853 | 854 | /typescript@4.7.4: 855 | resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} 856 | engines: {node: '>=4.2.0'} 857 | hasBin: true 858 | dev: true 859 | 860 | /undici-types@5.26.5: 861 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 862 | dev: true 863 | 864 | /update-browserslist-db@1.0.13(browserslist@4.22.2): 865 | resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} 866 | hasBin: true 867 | peerDependencies: 868 | browserslist: '>= 4.21.0' 869 | dependencies: 870 | browserslist: 4.22.2 871 | escalade: 3.1.1 872 | picocolors: 1.0.0 873 | dev: true 874 | 875 | /uri-js@4.4.1: 876 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 877 | dependencies: 878 | punycode: 2.3.1 879 | dev: true 880 | 881 | /url-join@4.0.1: 882 | resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} 883 | dev: true 884 | 885 | /watchpack@2.4.0: 886 | resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} 887 | engines: {node: '>=10.13.0'} 888 | dependencies: 889 | glob-to-regexp: 0.4.1 890 | graceful-fs: 4.2.11 891 | dev: true 892 | 893 | /webpack-sources@3.2.3: 894 | resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} 895 | engines: {node: '>=10.13.0'} 896 | dev: true 897 | 898 | /webpack@5.89.0: 899 | resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} 900 | engines: {node: '>=10.13.0'} 901 | hasBin: true 902 | peerDependencies: 903 | webpack-cli: '*' 904 | peerDependenciesMeta: 905 | webpack-cli: 906 | optional: true 907 | dependencies: 908 | '@types/eslint-scope': 3.7.7 909 | '@types/estree': 1.0.5 910 | '@webassemblyjs/ast': 1.11.6 911 | '@webassemblyjs/wasm-edit': 1.11.6 912 | '@webassemblyjs/wasm-parser': 1.11.6 913 | acorn: 8.11.2 914 | acorn-import-assertions: 1.9.0(acorn@8.11.2) 915 | browserslist: 4.22.2 916 | chrome-trace-event: 1.0.3 917 | enhanced-resolve: 5.15.0 918 | es-module-lexer: 1.4.1 919 | eslint-scope: 5.1.1 920 | events: 3.3.0 921 | glob-to-regexp: 0.4.1 922 | graceful-fs: 4.2.11 923 | json-parse-even-better-errors: 2.3.1 924 | loader-runner: 4.3.0 925 | mime-types: 2.1.35 926 | neo-async: 2.6.2 927 | schema-utils: 3.3.0 928 | tapable: 2.2.1 929 | terser-webpack-plugin: 5.3.9(webpack@5.89.0) 930 | watchpack: 2.4.0 931 | webpack-sources: 3.2.3 932 | transitivePeerDependencies: 933 | - '@swc/core' 934 | - esbuild 935 | - uglify-js 936 | dev: true 937 | 938 | /wrappy@1.0.2: 939 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 940 | dev: true 941 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import json from '@rollup/plugin-json'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import replace from '@rollup/plugin-replace'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import { defineConfig } from 'rollup'; 7 | import importAssets from 'rollup-plugin-import-assets'; 8 | 9 | import { name } from "./plugin.json"; 10 | 11 | export default defineConfig({ 12 | input: './src/index.tsx', 13 | plugins: [ 14 | commonjs(), 15 | nodeResolve(), 16 | typescript(), 17 | json(), 18 | replace({ 19 | preventAssignment: false, 20 | 'process.env.NODE_ENV': JSON.stringify('production'), 21 | }), 22 | importAssets({ 23 | publicPath: `http://127.0.0.1:1337/plugins/${name}/` 24 | }) 25 | ], 26 | context: 'window', 27 | external: ["react", "react-dom", "decky-frontend-lib"], 28 | output: { 29 | file: "dist/index.js", 30 | globals: { 31 | react: "SP_REACT", 32 | "react-dom": "SP_REACTDOM", 33 | "decky-frontend-lib": "DFL" 34 | }, 35 | format: 'iife', 36 | exports: 'default', 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/DiscordTab.tsx: -------------------------------------------------------------------------------- 1 | import { Router, ServerAPI } from "decky-frontend-lib" 2 | import { VFC, useLayoutEffect } from "react" 3 | 4 | export const DiscordTab: VFC<{serverAPI: ServerAPI}> = ({ serverAPI }) => { 5 | useLayoutEffect(() => { 6 | serverAPI.callPluginMethod("get_state", {}).then(res => { 7 | const state = (res.result as any); 8 | if (state?.loaded) { 9 | window.DISCORD_TAB.m_browserView.SetVisible(true); 10 | window.DISCORD_TAB.m_browserView.SetFocus(true); 11 | } 12 | else { 13 | serverAPI.toaster.toast({ 14 | title: "Deckcord", 15 | body: "Deckcord has not loaded yet!" 16 | }); 17 | Router.Navigate("/library/home"); 18 | } 19 | }) 20 | return () => { 21 | window.DISCORD_TAB.m_browserView.SetVisible(false); 22 | window.DISCORD_TAB.m_browserView.SetFocus(false); 23 | } 24 | }) 25 | return
26 | } -------------------------------------------------------------------------------- /src/components/UploadScreenshot.tsx: -------------------------------------------------------------------------------- 1 | import { DialogButton, Dropdown, DropdownOption, ServerAPI } from "decky-frontend-lib"; 2 | import { useEffect, useMemo, useState } from "react"; 3 | 4 | function urlContentToDataUri(url: string) { 5 | return fetch(url) 6 | .then((response) => response.blob()) 7 | .then( 8 | (blob) => 9 | new Promise((callback) => { 10 | let reader = new FileReader(); 11 | reader.onload = function () { 12 | callback(this.result); 13 | }; 14 | reader.readAsDataURL(blob); 15 | }) 16 | ); 17 | } 18 | 19 | export function UploadScreenshot(props: { serverAPI: ServerAPI; }) { 20 | const [screenshot, setScreenshot] = useState