diff --git a/package.json b/package.json index 5e6b8607..1aef5e66 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "idb": "8.0.0", "monaco-editor": "^0.52.2", "nanoid": "^5.0.9", + "socket.io": "^4.8.1", "usercss-meta": "^0.12.0", "openai": "^4.30.0", "virtual-merge": "^1.0.1" @@ -122,4 +123,4 @@ "node": ">=18", "pnpm": ">=9" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30659c58..25857e06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,9 @@ importers: openai: specifier: ^4.30.0 version: 4.52.7 + socket.io: + specifier: ^4.8.1 + version: 4.8.1 usercss-meta: specifier: ^0.12.0 version: 0.12.0 @@ -667,6 +670,9 @@ packages: '@sapphi-red/web-noise-suppressor@0.3.5': resolution: {integrity: sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA==} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@stylistic/eslint-plugin@2.12.1': resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -679,6 +685,9 @@ packages: '@types/chrome@0.0.287': resolution: {integrity: sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==} + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/diff@6.0.0': resolution: {integrity: sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==} @@ -819,6 +828,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -891,10 +904,6 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} - array.prototype.flatmap@1.3.3: resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} engines: {node: '>= 0.4'} @@ -956,6 +965,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} @@ -1057,6 +1070,14 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -1193,6 +1214,14 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2157,6 +2186,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + netmask@2.0.2: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} @@ -2217,10 +2250,6 @@ packages: resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} engines: {node: '>= 0.4'} - object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} - object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -2514,6 +2543,17 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -2780,6 +2820,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + virtual-merge@1.0.1: resolution: {integrity: sha512-h7rzV6n5fZJbDu2lP4iu+IOtsZ00uqECFUxFePK1uY0pz/S5B7FNDJpmdDVfyGL7poyJECEHfTaIpJaknNkU0Q==} @@ -2855,6 +2899,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -3214,6 +3270,8 @@ snapshots: '@sapphi-red/web-noise-suppressor@0.3.5': {} + '@socket.io/component-emitter@3.1.2': {} + '@stylistic/eslint-plugin@2.12.1(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.2)': dependencies: '@typescript-eslint/utils': 8.19.0(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.2) @@ -3233,6 +3291,10 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.15 + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.10.5 + '@types/diff@6.0.0': {} '@types/estree@1.0.6': {} @@ -3344,7 +3406,7 @@ snapshots: '@typescript-eslint/types': 8.19.0 '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.2) '@typescript-eslint/visitor-keys': 8.19.0 - debug: 4.3.6 + debug: 4.4.0 eslint: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4) typescript: 5.7.2 transitivePeerDependencies: @@ -3359,7 +3421,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.2) '@typescript-eslint/utils': 8.19.0(eslint@9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.7.2) - debug: 4.3.6 + debug: 4.4.0 eslint: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4) ts-api-utils: 1.3.0(typescript@5.7.2) typescript: 5.7.2 @@ -3372,7 +3434,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.19.0 '@typescript-eslint/visitor-keys': 8.19.0 - debug: 4.3.6 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -3412,6 +3474,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -3484,9 +3551,9 @@ snapshots: array.prototype.findlastindex@1.2.5: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.0.0 es-shim-unscopables: 1.0.2 @@ -3498,13 +3565,6 @@ snapshots: es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 - array.prototype.flatmap@1.3.2: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 @@ -3586,6 +3646,8 @@ snapshots: base64-js@1.5.1: {} + base64id@2.0.0: {} + basic-ftp@5.0.5: {} brace-expansion@1.1.11: @@ -3695,6 +3757,13 @@ snapshots: concat-map@0.0.1: {} + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cosmiconfig@9.0.0(typescript@5.7.2): dependencies: env-paths: 2.2.1 @@ -3829,6 +3898,24 @@ snapshots: dependencies: once: 1.4.0 + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.17 + '@types/node': 22.10.5 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.6 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + entities@4.5.0: {} env-paths@2.2.1: {} @@ -4178,7 +4265,7 @@ snapshots: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 + array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 9.17.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4) @@ -4190,7 +4277,7 @@ snapshots: minimatch: 3.1.2 object.fromentries: 2.0.8 object.groupby: 1.0.3 - object.values: 1.2.0 + object.values: 1.2.1 semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: @@ -4954,6 +5041,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + netmask@2.0.2: {} no-case@3.0.4: @@ -5008,15 +5097,9 @@ snapshots: object.groupby@1.0.3: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.values@1.2.0: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.0.0 + es-abstract: 1.23.9 object.values@1.2.1: dependencies: @@ -5376,6 +5459,36 @@ snapshots: smart-buffer@4.2.0: {} + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.6 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.6 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 @@ -5766,6 +5879,8 @@ snapshots: util-deprecate@1.0.2: {} + vary@1.1.2: {} + virtual-merge@1.0.1: {} vscode-oniguruma@1.7.0: {} @@ -5866,6 +5981,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@8.17.1: {} + ws@8.18.0: {} y18n@5.0.8: {} diff --git a/src/equicordplugins/premid/index.tsx b/src/equicordplugins/premid/index.tsx new file mode 100644 index 00000000..15720a1d --- /dev/null +++ b/src/equicordplugins/premid/index.tsx @@ -0,0 +1,327 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Link } from "@components/Link"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType, PluginNative } from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common"; + +interface ActivityAssets { + large_image: string; + large_text?: string | null; + small_image: string; + small_text: string; +} + +type ActivityButton = { + label: string; + url: string; +}; + +export interface Activity { + state: string; + details?: string; + timestamps?: { + start?: number; + end?: number; + }; + assets: ActivityAssets; + buttons?: Array; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +interface PremidActivity { + state: string; + details?: string; + startTimestamp?: number; + endTimestamp?: number; + largeImageKey: string; + largeImageText: string; + smallImageKey: string; + smallImageText: string; + buttons?: ActivityButton[]; + name?: string; + application_id: string; + type: number; + flags: number; +} + +interface PresenceData { + // Only relevant types - https://github.com/PreMiD/PreMiD/blob/main/%40types/PreMiD/PresenceData.d.ts + clientId: string; + presenceData: PremidActivity; +} + +const enum ActivityType { + PLAYING = 0, + LISTENING = 2, + WATCHING = 3, + COMPETING = 5 +} + +const enum ActivityFlag { + INSTANCE = 1 << 0 +} + +interface PublicApp { + id: string; + name: string; + icon: string; + statusType: ActivityType | undefined; + flags: number; +} + +const logger = new Logger("Vencord-PreMiD", "#8fd0ff"); + +const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application"); + +const apps: any = {}; +async function getApp(applicationId: string): Promise { + if (apps[applicationId]) return apps[applicationId]; + const socket: any = {}; + debugLog(`Looking up ${applicationId}`); + await fetchApplicationsRPC(socket, applicationId); + logger.debug(socket); + debugLog(`Lookup finished for ${socket.application.name}`); + const activityType = await determineStatusType(socket.application); + debugLog(`Activity type for ${socket.application.name}: ${activityType}`); + socket.application.statusType = settings.store.detectCategory ? activityType : ActivityType.PLAYING || ActivityType.PLAYING; + apps[applicationId] = socket.application; + return socket.application; +} + +const assetCache: Map = new Map(); +// memoized because this method isnt cached +async function getAppAsset(applicationId: string, key: string): Promise { + if (assetCache.has(applicationId + key)) { + return assetCache.get(applicationId + key)!; + } + const result = (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; + assetCache.set(applicationId + key, result); + return result; +} + +function setActivity(activity: Activity | undefined) { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity, + socketId: "PreMiD", + }); +} + +const settings = definePluginSettings({ + enableSet: { + description: "Should the plugin set presences?", + type: OptionType.BOOLEAN, + default: true, + onChange: (value: boolean) => { + if (!value) preMid.clearActivity(); + }, + }, + showButtons: { + description: "Show buttons", + type: OptionType.BOOLEAN, + default: true, + }, + detectCategory: { + description: "Set your Activity Type based on presence category", + type: OptionType.BOOLEAN, + default: true, + }, + hideViewChannel: { + description: "YouTube: Hide view channel button", + type: OptionType.BOOLEAN, + default: false, + } +}); + +const Native = VencordNative.pluginHelpers.PreMiD as PluginNative; + +const preMid = definePlugin({ + name: "PreMiD", + tags: ["presence", "premid", "rpc", "watching"], + description: "A PreMiD app replacement. Supports watching/listening status. Requires extra setup (see settings)", + authors: [Devs.Nyako], + toolboxActions: { + "Toggle presence sharing": () => { + settings.store.enableSet = !settings.store.enableSet; + showToast(`Presence sharing is now ${settings.store.enableSet ? "enabled" : "disabled"}`); + preMid.clearActivity(); + }, + }, + + settingsAboutComponent: () => ( + <> + How to use this plugin + + Install the PreMiD browser extension. (recommended version: 2.5.2 OR 2.6.11+) + + + This will not work with anything that has differing behavior (such as PreWrap) + + + That's all you need, if you followed the instructions in this plugin's README you should be good. This plugin replicates their electron tray process so no need to use allat. + + + ), + + settings, + logger, + + start() { + Native.init(); + }, + + stop() { + this.clearActivity(); + Native.disconnect(); + }, + + clearActivity() { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity: null, + socketId: "PreMiD", + }); + }, + + showToast, + + async receiveActivity(data: PresenceData) { + logger.debug("Received activity", data); + if (!settings.store.enableSet) { + this.clearActivity(); + return; + } + try { + const id = data.clientId; + if (!id) return; + const appInfo = await getApp(id); + const presence = { ...data.presenceData }; + if (appInfo.name === "PreMiD") return; + logger.debug(`Setting activity of ${appInfo.name} "${presence.details}"`); + + const { details, state, largeImageKey, smallImageKey, smallImageText } = presence; + const activity: Activity = { + application_id: id, + name: appInfo.name, + details: details ?? "", + state: state ?? "", + type: appInfo.statusType || ActivityType.PLAYING, + flags: ActivityFlag.INSTANCE, + assets: { + large_image: await getAppAsset(id, largeImageKey ?? "oops"), + small_image: await getAppAsset(id, smallImageKey ?? "oops"), + small_text: smallImageText || "hello there :3", + }, + buttons: presence.buttons?.map((b: { label: any; }) => b.label), + metadata: { + button_urls: presence.buttons?.map((b: { url: any; }) => b.url) + }, + timestamps: { + start: presence.startTimestamp, + end: presence.endTimestamp + } + }; + + + if (activity.type === ActivityType.PLAYING) { + activity.assets = { + large_image: await getAppAsset(id, largeImageKey ?? "guh"), + large_text: "vc-premid", + small_image: await getAppAsset(id, smallImageKey ?? "guhh"), + small_text: smallImageText || "hello there :3", + }; + } + + if (settings.store.showButtons && activity.buttons) { + if (appInfo.name === "YouTube" && settings.store.hideViewChannel) { + activity.buttons?.pop(); + if (activity.metadata && activity.metadata && activity.metadata.button_urls) { + activity.metadata.button_urls = [activity.metadata.button_urls[0]]; + } + } + } + + for (const k in activity) { + if (k === "type") continue; // without type, the presence is considered invalid. + const v = activity[k]; + if (!v || v.length === 0) + delete activity[k]; + } + + + setActivity(activity); + } catch (err) { + logger.error(err); + } + } +}); + +async function determineStatusType(info: PublicApp): Promise { + let firstCharacter = info.name.charAt(0); + if (firstCharacter.match(/[a-zA-Z]/)) { + firstCharacter = firstCharacter; + } else if (firstCharacter.match(/[0-9]/)) { + firstCharacter = "0-9"; + } else { + firstCharacter = "%23"; // # + } + + const res = await fetch(`https://raw.githubusercontent.com/PreMiD/Presences/main/websites/${firstCharacter}/${info.name}/metadata.json`); + if (!res.ok) return ActivityType.PLAYING; + + try { + const metadata = await res.json(); + switch (metadata.category) { + case "socials": + if (metadata.tags.includes("video")) { + return ActivityType.WATCHING; + } + break; + case "anime": + if (metadata.tags.some((tag: string) => ["video", "media", "streaming"].includes(tag))) { + return ActivityType.WATCHING; + } + break; + case "music": + return ActivityType.LISTENING; + case "videos": + return ActivityType.WATCHING; + } + } catch (e) { + logger.error(e); + return ActivityType.PLAYING; + } + return ActivityType.PLAYING; +} + +function debugLog(msg: string) { + if (IS_DEV) console.log(msg); +} + +function showToast(msg: string) { + Toasts.show({ + message: msg, + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + duration: 5000, + position: Toasts.Position.TOP + } + }); +} + +export default preMid; diff --git a/src/equicordplugins/premid/native.ts b/src/equicordplugins/premid/native.ts new file mode 100644 index 00000000..0ee330d2 --- /dev/null +++ b/src/equicordplugins/premid/native.ts @@ -0,0 +1,116 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { BrowserWindow, dialog, WebContents } from "electron"; +import { createServer, Server as HttpServer } from "http"; +import { Server, Socket } from "socket.io"; + +let io: Server; +let httpServer: HttpServer; +let hasInit = false; +let webFrame: WebContents; + +export function init() { + if (hasInit) return; + + const windows = BrowserWindow.getAllWindows(); + const discordUrls = ["https://discord.com", "https://ptb.discord.com", "https://canary.discord.com"]; + + for (const win of windows) { + const url = win.webContents.getURL(); + if (discordUrls.some(prefix => url.startsWith(prefix))) { + webFrame = win.webContents; + } + } + + httpServer = createServer(); + + io = new Server(httpServer, { + serveClient: false, + allowEIO3: true, + cors: { origin: "*" } + }); + httpServer.listen(3020, () => { + console.log("[vc-premid] SocketIO starting on 3020"); + logRenderer("SocketIO starting on 3020"); + }); + httpServer.on("error", onIOError); + io.on("connection", onConnect); + hasInit = true; +} + +export function disconnect() { + if (!hasInit) return; + io.close(); + httpServer.close(); + hasInit = false; +} + +async function onConnect(sio: Socket) { + try { + logRenderer("[vc-premid] PreMiD socket connected!"); + // Get current user from plugin & send to extension + const { + username, + globalName, + id, + avatar, + discriminator, + flags, + premiumType + } = JSON.parse(await webFrame.executeJavaScript("JSON.stringify(window.Vencord.Webpack.Common.UserStore.getCurrentUser());")); + sio.emit("discordUser", { username, global_name: globalName, discriminator, id, avatar, bot: false, flags, premium_type: premiumType }); + + // Extension requests Premid version + sio.on("getVersion", () => { + logRenderer("Extension requested version"); + sio.emit("receiveVersion", "221"); + }); + + sio.on("setActivity", setActivity); + sio.on("clearActivity", clearActivity); + sio.on("selectLocalPresence", () => { + logRenderer("Selecting local presence is not supported"); + dialog.showMessageBox({ message: "Selecting local presence is not supported right now!", title: "vc-premid: oops!" }); + }); + sio.once("disconnect", () => onIoDisconnect()); + } catch (e) { + logError("Error in onConnect: ", e); + } +} + +function logRenderer(message: string) { + if (webFrame) { + webFrame.executeJavaScript(`window.Vencord.Plugins.plugins.PreMiD.logger.info('${message}')`); + } else { + // just in case, dont worry about it pls + console.log(`[vc-premid (fallback)] ${message}`); + } +} + +function logError(message: string, ...args: any[]) { + console.error(`${message}`, args); +} + +function setActivity(activity: any) { + // hopefully this works + webFrame.executeJavaScript(`window.Vencord.Plugins.plugins.PreMiD.receiveActivity(${JSON.stringify(activity)})`).catch(console.error); +} + +function clearActivity() { + webFrame.executeJavaScript("window.Vencord.Plugins.plugins.PreMiD.clearActivity()"); +} + +function onIOError(e: { message: string; code: string; }) { + if (e.message.includes("EADDRINUSE")) return; // dont care, probably 2+ clients open + logError("SocketIO error", e); +} + +async function onIoDisconnect() { + console.log("[vc-premid] SocketIO disconnected"); + logRenderer("SocketIO disconnected"); + clearActivity(); +} diff --git a/src/equicordplugins/roleMembersViewer/index.tsx b/src/equicordplugins/roleMembersViewer/index.tsx new file mode 100644 index 00000000..182a098c --- /dev/null +++ b/src/equicordplugins/roleMembersViewer/index.tsx @@ -0,0 +1,151 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { + FluxDispatcher, + GuildMemberStore, + GuildStore, + Menu, + SelectedChannelStore, + SelectedGuildStore, + UserProfileActions, + UserStore +} from "@webpack/common"; +import { JSX } from "react"; + +function fetchMembersWithRole(guildId: string, roleId: string) { + const guildMembers = GuildMemberStore.getMembers(guildId); + let membersInRole = 0; + guildMembers.forEach(member => { + if (member.roles.includes(roleId)) { + membersInRole++; + } + }); + if (Object.keys(guildMembers).length < membersInRole) { + const chunk = 100; + const requestCount = Math.ceil(membersInRole / chunk); + for (let i = 0; i < requestCount; i++) { + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildId, + userIds: [], + query: "", + limit: chunk, + withPresences: true, + notifyOnLimit: true + }); + } + } + const updatedGuildMembers = GuildMemberStore.getMembers(guildId); + return Object.values(updatedGuildMembers) + .filter(m => m.roles.includes(roleId)) + .map(m => ({ + ...m, + user: UserStore.getUser(m.userId) + })) + .sort((a, b) => a.user.username.localeCompare(b.user.username)); +} + +export default definePlugin({ + name: "RoleMembersViewer", + description: "Shows members with a role when right clicking roles in user profiles or role mentions in messages", + authors: [EquicordDevs.okiso], + + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + const guild = GuildStore.getGuild(SelectedGuildStore.getGuildId()); + if (!guild) return; + + const role = GuildStore.getRole(guild.id, id); + if (!role) return; + + const guildId = guild.id; + const membersWithRole = fetchMembersWithRole(guildId, id); + + const memberItems = membersWithRole.map(member => ( + { + UserProfileActions.openUserProfileModal({ + userId: member.userId, + guildId, + channelId: SelectedChannelStore.getChannelId() + }); + }} + /> + )); + + children.push( + + {memberItems} + + ); + }, + + "message"(children, { message }: { message: any; }) { + const guild = GuildStore.getGuild(SelectedGuildStore.getGuildId()); + if (!guild) return; + + const roleMentions = message.content.match(/<@&(\d+)>/g); + if (!roleMentions?.length) return; + + // Extract unique role IDs from the mentions. + const roleIds = roleMentions.map(mention => mention.match(/<@&(\d+)>/)![1]); + const uniqueRoleIds = [...new Set(roleIds)]; + + const guildId = guild.id; + const roleMenuItems: JSX.Element[] = []; + + for (const roleId of uniqueRoleIds as string[]) { + const role = GuildStore.getRole(guildId, roleId); + if (!role) continue; + + const membersWithRole = fetchMembersWithRole(guildId, roleId); + const memberItems = membersWithRole.map(member => ( + { + UserProfileActions.openUserProfileModal({ + userId: member.userId, + guildId, + channelId: SelectedChannelStore.getChannelId() + }); + }} + /> + )); + + roleMenuItems.push( + + {memberItems} + + ); + } + + if (roleMenuItems.length > 0) { + children.push( + + {roleMenuItems} + + ); + } + } + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f372320a..4cd1d432 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1000,6 +1000,10 @@ export const EquicordDevs = Object.freeze({ name: "mochie", id: 1043599230247374869n, }, + okiso: { + name: "okiso", + id: 274178934143451137n, + }, } satisfies Record); // iife so #__PURE__ works correctly