Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
51
.github/workflows/deploy.yml
vendored
|
|
@ -1,51 +0,0 @@
|
||||||
name: Build and deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Prepare environment
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- run: npm install --global pnpm
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
run: pnpm m i
|
|
||||||
|
|
||||||
- name: Build plugin(s)
|
|
||||||
run: node ./build.mjs
|
|
||||||
|
|
||||||
# Foolproof feature:
|
|
||||||
# - Copies over README so that the root of the deployed website shows it
|
|
||||||
# - Changes 404 page to README so that you don't get lost while clicking links
|
|
||||||
# If you remove this step then you should probably remove the enable_jekyll option in the next one
|
|
||||||
- name: Copy additional files
|
|
||||||
run: |
|
|
||||||
cp README.md dist/README.md
|
|
||||||
printf -- "---\npermalink: /404.html\n---\n" > dist/404.md
|
|
||||||
printf -- "> **Note:** You accessed a link that returned a 404, probably by clicking one of the plugin links. You're supposed to copy the link address and add it into Vendetta.\n\n" >> dist/404.md
|
|
||||||
cat README.md >> dist/404.md
|
|
||||||
|
|
||||||
# Documentation: https://github.com/peaceiris/actions-gh-pages
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./dist
|
|
||||||
# Makes it so the md files in the previous step get processed by GitHub Pages
|
|
||||||
enable_jekyll: true
|
|
||||||
# This creates the CNAME file required to host GitHub Pages on a custom domain
|
|
||||||
# cname: example.com
|
|
||||||
5
.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
pnpm-lock.yaml
|
|
||||||
yarn.lock
|
|
||||||
package-lock.json
|
|
||||||
1
.npmrc
|
|
@ -1 +0,0 @@
|
||||||
shamefully-hoist=true
|
|
||||||
39
404.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
---
|
||||||
|
permalink: /404.html
|
||||||
|
---
|
||||||
|
> **Note:** You accessed a link that returned a 404, probably by clicking one of the plugin links. You're supposed to copy the link address and add it into Vendetta.
|
||||||
|
|
||||||
|
# Apex Plugins
|
||||||
|
This repo contains my own plugins for Revenge.
|
||||||
|
|
||||||
|
# How to install?
|
||||||
|
Paste a plugin URL into the Plugins page of Revenge, following a basic format of:
|
||||||
|
```
|
||||||
|
https://apexteampl.github.io/Apex-Plugins/<PLUGIN_NAME>
|
||||||
|
```
|
||||||
|
## Or just copy plugin links from here!
|
||||||
|
- [Message Scheduler](https://apexteampl.github.io/Apex-Plugins/messageScheduler/) - schedule messages to be sent at specific time or after a delay! (port from Vencore) **NOTE: As mentioned by one of Revenge mods, this plugin MAY FALL under selfbotting along with the og one from vencord/vencore, thus won't be proxied and added to offical list, USE AT YOUR OWN RISK!!!**
|
||||||
|
- [More Alts](https://apexteampl.github.io/Apex-Plugins/MoreAlts/) - use more than 5 alts at Discord and easily switch between them from chat input. [For FAQs go here](https://github.com/ApexTeamPL/Apex-Plugins/wiki/MoreAlts)
|
||||||
|
- [Read All](https://apexteampl.github.io/Apex-Plugins/ReadAll) - Read all server and DM notifications with single command - /readall (idea from btmc727)
|
||||||
|
- [BetterNSFWGateBypass](https://apexteampl.github.io/Apex-Plugins/nsfwbypass) - Allows you to access NSFW channels without setting your age[.](https://m.youtube.com/watch?v=xvFZjo5PgG0) [Note](https://github.com/ApexTeamPL/Apex-Plugins/tree/master/docs/nsfwbypass)
|
||||||
|
|
||||||
|
__ __
|
||||||
|
|
||||||
|
**Planned/in development plugins**
|
||||||
|
- LastSeen - see when user was last offline - a port from Vencore
|
||||||
|
- ValidUser - fix mentions for unknown users showing up as '@unknown-user'.
|
||||||
|
- PlatformSwitcher - set custom platform status (mobile, web, desktop) - also port from Vencore
|
||||||
|
- BetterSessionsRewritten - idea from Vencord, see new login notifications
|
||||||
|
|
||||||
|
__ __
|
||||||
|
|
||||||
|
### Maintainers
|
||||||
|
- Win8.1VMUser - the creator of this project and [Vencore project](https://github.com/ApexTeamPL/Vencore), developer of those fantastic plugins!
|
||||||
|
- Slara6804 - (it's me!), just a random girl who's too much into her Linux pharse, doing random things mostly to help people :3
|
||||||
|
__ __
|
||||||
|
|
||||||
|
Apex Plugins is free and open source plugin list created by Apex Team developers (simply me and VMuser)[.](https://m.youtube.com/watch?v=xvFZjo5PgG0)
|
||||||
|
|
||||||
|
__ __
|
||||||
|
|
||||||
|
### BE AWARE OF FAKE ACCOUNTS DISTRIBUTING OUR AND OTHERS PLUGINS!!! PLEASE DOWNLOAD PLUGINS ONLY FROM OFFICAL REPOS (See [Vencore](https://raw.githubusercontent.com/ApexTeamPL/Plugins-List/refs/heads/main/offical-plugins.json) and [Purple-Eyez](https://plugins-list.pages.dev/) Plugin Browser)! THOSE ACCOUNTS HAVE BEEN FORKING OUR REPOS. FOR SECURITY REASONS WE WON'T WRITE WHO DID IT. YOU HAVE BEEN WARNED!
|
||||||
116
LICENSE
|
|
@ -1,116 +0,0 @@
|
||||||
CC0 1.0 Universal
|
|
||||||
|
|
||||||
Statement of Purpose
|
|
||||||
|
|
||||||
The laws of most jurisdictions throughout the world automatically confer
|
|
||||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
|
||||||
subsequent owner(s) (each and all, an "owner") of an original work of
|
|
||||||
authorship and/or a database (each, a "Work").
|
|
||||||
|
|
||||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
|
||||||
purpose of contributing to a commons of creative, cultural and scientific
|
|
||||||
works ("Commons") that the public can reliably and without fear of later
|
|
||||||
claims of infringement build upon, modify, incorporate in other works, reuse
|
|
||||||
and redistribute as freely as possible in any form whatsoever and for any
|
|
||||||
purposes, including without limitation commercial purposes. These owners may
|
|
||||||
contribute to the Commons to promote the ideal of a free culture and the
|
|
||||||
further production of creative, cultural and scientific works, or to gain
|
|
||||||
reputation or greater distribution for their Work in part through the use and
|
|
||||||
efforts of others.
|
|
||||||
|
|
||||||
For these and/or other purposes and motivations, and without any expectation
|
|
||||||
of additional consideration or compensation, the person associating CC0 with a
|
|
||||||
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
|
|
||||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
|
|
||||||
and publicly distribute the Work under its terms, with knowledge of his or her
|
|
||||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
|
||||||
effect of CC0 on those rights.
|
|
||||||
|
|
||||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
||||||
protected by copyright and related or neighboring rights ("Copyright and
|
|
||||||
Related Rights"). Copyright and Related Rights include, but are not limited
|
|
||||||
to, the following:
|
|
||||||
|
|
||||||
i. the right to reproduce, adapt, distribute, perform, display, communicate,
|
|
||||||
and translate a Work;
|
|
||||||
|
|
||||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
||||||
|
|
||||||
iii. publicity and privacy rights pertaining to a person's image or likeness
|
|
||||||
depicted in a Work;
|
|
||||||
|
|
||||||
iv. rights protecting against unfair competition in regards to a Work,
|
|
||||||
subject to the limitations in paragraph 4(a), below;
|
|
||||||
|
|
||||||
v. rights protecting the extraction, dissemination, use and reuse of data in
|
|
||||||
a Work;
|
|
||||||
|
|
||||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
||||||
European Parliament and of the Council of 11 March 1996 on the legal
|
|
||||||
protection of databases, and under any national implementation thereof,
|
|
||||||
including any amended or successor version of such directive); and
|
|
||||||
|
|
||||||
vii. other similar, equivalent or corresponding rights throughout the world
|
|
||||||
based on applicable law or treaty, and any national implementations thereof.
|
|
||||||
|
|
||||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
|
||||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
|
||||||
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
|
||||||
and Related Rights and associated claims and causes of action, whether now
|
|
||||||
known or unknown (including existing as well as future claims and causes of
|
|
||||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
|
||||||
duration provided by applicable law or treaty (including future time
|
|
||||||
extensions), (iii) in any current or future medium and for any number of
|
|
||||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
|
||||||
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
|
|
||||||
the Waiver for the benefit of each member of the public at large and to the
|
|
||||||
detriment of Affirmer's heirs and successors, fully intending that such Waiver
|
|
||||||
shall not be subject to revocation, rescission, cancellation, termination, or
|
|
||||||
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
|
||||||
by the public as contemplated by Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
|
||||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
|
||||||
shall be preserved to the maximum extent permitted taking into account
|
|
||||||
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
|
|
||||||
is so judged Affirmer hereby grants to each affected person a royalty-free,
|
|
||||||
non transferable, non sublicensable, non exclusive, irrevocable and
|
|
||||||
unconditional license to exercise Affirmer's Copyright and Related Rights in
|
|
||||||
the Work (i) in all territories worldwide, (ii) for the maximum duration
|
|
||||||
provided by applicable law or treaty (including future time extensions), (iii)
|
|
||||||
in any current or future medium and for any number of copies, and (iv) for any
|
|
||||||
purpose whatsoever, including without limitation commercial, advertising or
|
|
||||||
promotional purposes (the "License"). The License shall be deemed effective as
|
|
||||||
of the date CC0 was applied by Affirmer to the Work. Should any part of the
|
|
||||||
License for any reason be judged legally invalid or ineffective under
|
|
||||||
applicable law, such partial invalidity or ineffectiveness shall not
|
|
||||||
invalidate the remainder of the License, and in such case Affirmer hereby
|
|
||||||
affirms that he or she will not (i) exercise any of his or her remaining
|
|
||||||
Copyright and Related Rights in the Work or (ii) assert any associated claims
|
|
||||||
and causes of action with respect to the Work, in either case contrary to
|
|
||||||
Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
4. Limitations and Disclaimers.
|
|
||||||
|
|
||||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
||||||
surrendered, licensed or otherwise affected by this document.
|
|
||||||
|
|
||||||
b. Affirmer offers the Work as-is and makes no representations or warranties
|
|
||||||
of any kind concerning the Work, express, implied, statutory or otherwise,
|
|
||||||
including without limitation warranties of title, merchantability, fitness
|
|
||||||
for a particular purpose, non infringement, or the absence of latent or
|
|
||||||
other defects, accuracy, or the present or absence of errors, whether or not
|
|
||||||
discoverable, all to the greatest extent permissible under applicable law.
|
|
||||||
|
|
||||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
||||||
that may apply to the Work or any use thereof, including without limitation
|
|
||||||
any person's Copyright and Related Rights in the Work. Further, Affirmer
|
|
||||||
disclaims responsibility for obtaining any necessary consents, permissions
|
|
||||||
or other rights required for any use of the Work.
|
|
||||||
|
|
||||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
||||||
party to this document and has no duty or obligation with respect to this
|
|
||||||
CC0 or use of the Work.
|
|
||||||
|
|
||||||
For more information, please see
|
|
||||||
<http://creativecommons.org/publicdomain/zero/1.0/>
|
|
||||||
13
MoreAlts/index.js
Normal file
1
MoreAlts/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"More Alts!","description":"Adds ability to safely add more alts to Discord!","authors":[{"name":"Win8.1VMUser","id":"793935599702507542"}],"main":"index.js","vendetta":{"icon":"ic_full_server_gating_24px"},"hash":"0fb86a7619787bd2011cda35fefd9185ebb14ad3b885e0d959b51bc75f96cacf"}
|
||||||
1
ReadAll/index.js
Normal file
1
ReadAll/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"Read All","description":"Read all server and DM notifications with single command - /read","authors":[{"name":"Win8.1VMUser","id":"793935599702507542"},{"name":"btmc727","id":"554071670143451176"}],"main":"index.js","vendetta":{"icon":"ic_edit_24px"},"hash":"1c6c6b83e99b2a52c50213f5dabc9b2e900950e95eb0a8289eeeadb20192f1f7"}
|
||||||
88
build.mjs
|
|
@ -1,88 +0,0 @@
|
||||||
import { readFile, writeFile, readdir } from "fs/promises";
|
|
||||||
import { extname } from "path";
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
|
|
||||||
import { rollup } from "rollup";
|
|
||||||
import esbuild from "rollup-plugin-esbuild";
|
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
|
||||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
|
||||||
import swc from "@swc/core";
|
|
||||||
|
|
||||||
const extensions = [".js", ".jsx", ".mjs", ".ts", ".tsx", ".cts", ".mts"];
|
|
||||||
|
|
||||||
/** @type import("rollup").InputPluginOption */
|
|
||||||
const plugins = [
|
|
||||||
nodeResolve(),
|
|
||||||
commonjs(),
|
|
||||||
{
|
|
||||||
name: "swc",
|
|
||||||
async transform(code, id) {
|
|
||||||
const ext = extname(id);
|
|
||||||
if (!extensions.includes(ext)) return null;
|
|
||||||
|
|
||||||
const ts = ext.includes("ts");
|
|
||||||
const tsx = ts ? ext.endsWith("x") : undefined;
|
|
||||||
const jsx = !ts ? ext.endsWith("x") : undefined;
|
|
||||||
|
|
||||||
const result = await swc.transform(code, {
|
|
||||||
filename: id,
|
|
||||||
jsc: {
|
|
||||||
externalHelpers: true,
|
|
||||||
parser: {
|
|
||||||
syntax: ts ? "typescript" : "ecmascript",
|
|
||||||
tsx,
|
|
||||||
jsx,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
targets: "defaults",
|
|
||||||
include: [
|
|
||||||
"transform-classes",
|
|
||||||
"transform-arrow-functions",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return result.code;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
esbuild({ minify: true }),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let plug of await readdir("./plugins")) {
|
|
||||||
const manifest = JSON.parse(await readFile(`./plugins/${plug}/manifest.json`));
|
|
||||||
const outPath = `./dist/${plug}/index.js`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bundle = await rollup({
|
|
||||||
input: `./plugins/${plug}/${manifest.main}`,
|
|
||||||
onwarn: () => {},
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
|
|
||||||
await bundle.write({
|
|
||||||
file: outPath,
|
|
||||||
globals(id) {
|
|
||||||
if (id.startsWith("@vendetta")) return id.substring(1).replace(/\//g, ".");
|
|
||||||
const map = {
|
|
||||||
react: "window.React",
|
|
||||||
};
|
|
||||||
|
|
||||||
return map[id] || null;
|
|
||||||
},
|
|
||||||
format: "iife",
|
|
||||||
compact: true,
|
|
||||||
exports: "named",
|
|
||||||
});
|
|
||||||
await bundle.close();
|
|
||||||
|
|
||||||
const toHash = await readFile(outPath);
|
|
||||||
manifest.hash = createHash("sha256").update(toHash).digest("hex");
|
|
||||||
manifest.main = "index.js";
|
|
||||||
await writeFile(`./dist/${plug}/manifest.json`, JSON.stringify(manifest));
|
|
||||||
|
|
||||||
console.log(`Successfully built ${manifest.name}!`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to build plugin...", e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
const AsyncFunction = (async()=>{}).constructor;
|
|
||||||
|
|
||||||
/* Evaluates code
|
|
||||||
* @param {string} code Code to evaluate
|
|
||||||
* @param {boolean} aweight Whether to await the evaluation
|
|
||||||
* @param {boolean} global Whether to assign the next argument as the "this" for the evaluation
|
|
||||||
* @param {any} [that]
|
|
||||||
*/
|
|
||||||
export default async function evaluate(code, aweight = true, global = false, that = {}) {
|
|
||||||
if (!code) throw new Error("No code to evaluate");
|
|
||||||
|
|
||||||
let result,
|
|
||||||
errored = false,
|
|
||||||
timings = [+new Date()];
|
|
||||||
const args = [];
|
|
||||||
if (!global) args.push(...Object.keys(that));
|
|
||||||
|
|
||||||
let evalFunction = new AsyncFunction(...args, code);
|
|
||||||
Object.keys(that).forEach((name, index) => {
|
|
||||||
args[index] = that[name];
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
if (aweight) {
|
|
||||||
result = await evalFunction(...args);
|
|
||||||
} else {
|
|
||||||
result = evalFunction(...args);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
result = e;
|
|
||||||
errored = true;
|
|
||||||
}
|
|
||||||
timings[1] = +new Date();
|
|
||||||
|
|
||||||
const res = { result, errored, timings };
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
evaluate.SENSITIVE_PROPS = {
|
|
||||||
USER: ["email", "phone", "mfaEnabled", "hasBouncedEmail"]
|
|
||||||
}
|
|
||||||
272
common/index.js
|
|
@ -1,272 +0,0 @@
|
||||||
export { mSendMessage } from "./mSendMessage.js";
|
|
||||||
export const ZWD = "\u200d",
|
|
||||||
Promise_UNMINIFIED_PROPERTY_NAMES = ["_deferredState", "_state", "_value", "_deferreds"],
|
|
||||||
PROMISE_STATE_NAMES = {
|
|
||||||
0: "pending",
|
|
||||||
1: "fulfilled",
|
|
||||||
2: "rejected",
|
|
||||||
3: "adopted",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function codeblock(text, language = "", escape = false) {
|
|
||||||
if (!text) throw new Error("No text to wrap in a codeblock provided");
|
|
||||||
if (escape) text = text.replaceAll("```", `\`${ZWD}\`\``);
|
|
||||||
return `\`\`\`${language}\n${text}\n\`\`\``;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Assigns the displayName property (translation) eveywhere it's needed
|
|
||||||
* @param {object} obj The command object to assign the properties on (mutates)
|
|
||||||
* @param {object|undefined} translations The translations to use. Must be in the same position, has to have an "s" at the end of the name and has to be an object with locale:string k,v
|
|
||||||
* @param {string|undefined} locale The locale to use
|
|
||||||
*
|
|
||||||
* @returns {object} the object that was passed in the first argument
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* cmdDisplays({
|
|
||||||
* name: "meow",
|
|
||||||
* description: "mrrrp",
|
|
||||||
* }, {
|
|
||||||
* names: {
|
|
||||||
* ru: "мяу"
|
|
||||||
* },
|
|
||||||
* description: {
|
|
||||||
* ru: "муррр"
|
|
||||||
* }
|
|
||||||
* }, "ru")
|
|
||||||
*/
|
|
||||||
export function cmdDisplays(obj, translations, locale) {
|
|
||||||
if (!obj?.name || !obj?.description) throw new Error(`No name(${obj?.name}) or description(${obj?.description}) in the passed command (command name: ${obj?.name})`);
|
|
||||||
|
|
||||||
obj.displayName ??= translations?.names?.[locale] ?? obj.name;
|
|
||||||
obj.displayDescription ??= translations?.names?.[locale] ?? obj.description;
|
|
||||||
if (obj.options) {
|
|
||||||
if (!Array.isArray(obj.options)) throw new Error(`Options is not an array (received: ${typeof obj.options})`);
|
|
||||||
for (var optionIndex = 0; optionIndex < obj.options.length; optionIndex++) {
|
|
||||||
const option = obj.options[optionIndex];
|
|
||||||
// TODO: Handle subcommands (type 1 or 2 probably i forgor)
|
|
||||||
if (!option?.name || !option?.description) throw new Error(`No name(${option?.name}) or description(${option?.description} in the option with index ${optionIndex}`);
|
|
||||||
option.displayName ??= translations?.options?.[optionIndex]?.names?.[locale] ?? option.name;
|
|
||||||
option.displayDescription ??= translations?.options?.[optionIndex]?.descriptions?.[locale] ?? option.description;
|
|
||||||
if (option?.choices) {
|
|
||||||
if (!Array.isArray(option?.choices)) throw new Error(`Choices is not an array (received: ${typeof option.choices})`);
|
|
||||||
for (var choiceIndex = 0; choiceIndex < option.choices.length; choiceIndex++) {
|
|
||||||
const choice = option.choices[choiceIndex];
|
|
||||||
if (!choice?.name) throw new Error(`No name of choice with index ${choiceIndex} in option with index ${optionIndex}`);
|
|
||||||
choice.displayName ??= translations?.options?.[optionIndex]?.choices?.[choiceIndex]?.names?.[locale] ?? choice.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
export function generateRandomString(chars, length = 27) {
|
|
||||||
if (typeof chars !== "string") throw new Error("Passed chars isn't a string");
|
|
||||||
if (chars?.length <= 0) throw new Error("Invalid chars length");
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) result += chars[Math.floor(Math.random() * chars.length)];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
export function areArraysEqual(arr1, arr2) {
|
|
||||||
if (arr1.length !== arr2.length) return false;
|
|
||||||
for (let i = 0; i < arr1.length; i++) {
|
|
||||||
const item1 = arr1[i];
|
|
||||||
const item2 = arr2[i];
|
|
||||||
|
|
||||||
if (Array.isArray(item1) && Array.isArray(item2)) {
|
|
||||||
if (!areArraysEqual(item1, item2)) return false;
|
|
||||||
} else if (item1 !== null && item2 !== null && typeof item1 === "object" && typeof item2 === "object") {
|
|
||||||
if (!areArraysEqual(Object.values(item1), Object.values(item2))) return false;
|
|
||||||
} else if (item1 !== item2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
export function cloneWithout(value, without, replace) {
|
|
||||||
if (typeof value !== "object") return value;
|
|
||||||
if (without.some(($) => (Array.isArray($) ? (Array.isArray(value) ? areArraysEqual($, value) : false) : $ === value))) return replace;
|
|
||||||
|
|
||||||
const newObj = Array.isArray(value) ? [] : {};
|
|
||||||
for (const key in value) {
|
|
||||||
if (Array.isArray(value[key])) {
|
|
||||||
newObj[key] = cloneWithout(value[key], without, replace);
|
|
||||||
} else if (without.includes(value[key])) {
|
|
||||||
newObj[key] = replace;
|
|
||||||
} else {
|
|
||||||
newObj[key] = cloneWithout(value[key], without, replace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Calls and awaits a promise function, and returns a [result, error] array
|
|
||||||
* @param {function} promise A function that returns a promise
|
|
||||||
* @example
|
|
||||||
* async function log(...messages) {
|
|
||||||
* if (messages[0] === "error")) throw new Error() // reject(new Error)
|
|
||||||
*
|
|
||||||
* console.log("[log]", ...messages)
|
|
||||||
* return messages // resolve(messages)
|
|
||||||
* }
|
|
||||||
* await awaitPromise(log, "meow") // [ ["meow"], null ]
|
|
||||||
*
|
|
||||||
* await awaitPromise(log, "error") // [ null, Error ]
|
|
||||||
*/
|
|
||||||
export async function awaitPromise(promiseFn, ...args) {
|
|
||||||
let output = [null, null];
|
|
||||||
try {
|
|
||||||
output[0] = await promiseFn(...args);
|
|
||||||
} catch (error) {
|
|
||||||
output[1] = error;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processRows(rows) {
|
|
||||||
if (!Array.isArray(rows) || !rows.every((row) => Array.isArray(row) && typeof row[0] === "string")) return JSON.stringify(rows);
|
|
||||||
|
|
||||||
return rows
|
|
||||||
.sort(([a], [b]) => a.length - b.length || a.localeCompare(b))
|
|
||||||
.map((row) => (row[0] === "" ? row[1] : row.join("∶ ")))
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Returns an object with the properties of a Promise but with the proper key names (hermes polyfill specific)
|
|
||||||
* Basically UNDOes https://github.com/then/promise/blob/master/src/core.js#L16
|
|
||||||
* @param {Promise} promise A promise object with improper key names
|
|
||||||
* @param {boolean=false} mutate Whether to add the proper keys on the passed object itself instead of creating a new one
|
|
||||||
* @param {boolean=false} removeOldKeys Whether to remove the old keys from the promise
|
|
||||||
*
|
|
||||||
* @returns {(Promise|object)}
|
|
||||||
*/
|
|
||||||
export function fixPromiseProps(improperPromise, mutate = false, removeOldKeys = false) {
|
|
||||||
const originalKeys = Object.getOwnPropertyNames(improperPromise);
|
|
||||||
if (originalKeys.length !== 4 || originalKeys.every((name, i) => Promise_UNMINIFIED_PROPERTY_NAMES[i] === name)) throw new Error("The passed promise is already proper or isn't a promise");
|
|
||||||
|
|
||||||
let properPromise = {};
|
|
||||||
if (mutate) properPromise = improperPromise;
|
|
||||||
|
|
||||||
Promise_UNMINIFIED_PROPERTY_NAMES.forEach((name, i) => {
|
|
||||||
properPromise[name] = improperPromise[originalKeys[i]];
|
|
||||||
if (mutate && removeOldKeys) delete properPromise[originalKeys[i]];
|
|
||||||
});
|
|
||||||
Object.setPrototypeOf(properPromise, improperPromise.__proto__);
|
|
||||||
return properPromise;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Makes a pretty typeof-like string from a value
|
|
||||||
* @param {any} value
|
|
||||||
* @returns {string}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* prettyTypeof(69_999)
|
|
||||||
*/
|
|
||||||
export function prettyTypeof(value) {
|
|
||||||
const name = [value?.constructor?.name];
|
|
||||||
name[0] ??= "Undefined";
|
|
||||||
if (name[0] === "Promise" && Object.getOwnPropertyNames(value).length === 4) {
|
|
||||||
const state = value["_state"] ?? value[Object.getOwnPropertyNames(value)[1]];
|
|
||||||
const stateName = PROMISE_STATE_NAMES[state];
|
|
||||||
if (stateName) name[1] = `(${stateName})`;
|
|
||||||
} else if (name[0] !== "Undefined" && value?.prototype?.constructor === value && typeof value === "function") {
|
|
||||||
name[0] = "Class";
|
|
||||||
name[1] = `(${value.name})`;
|
|
||||||
} else if (value === null) {
|
|
||||||
name[1] = `(null)`;
|
|
||||||
} else if (["symbol", "function"].includes(typeof value) && value?.name) {
|
|
||||||
name[1] = `(${value.name})`;
|
|
||||||
} else if (typeof value === "boolean") {
|
|
||||||
name[1] = `(${value})`;
|
|
||||||
} else if (typeof value === "string") {
|
|
||||||
name[1] = `(len: ${value.length})`;
|
|
||||||
} else if (typeof value === "number" && value !== 0) {
|
|
||||||
const expo = value.toExponential();
|
|
||||||
if (!expo.endsWith("e+1") && !expo.endsWith("e+0")) name[1] = `(${expo})`;
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
if (value.length !== 0) name[1] = `(len: ${value.length})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return name.join(" ");
|
|
||||||
}
|
|
||||||
/* Makes sure that the properties from "defaults" exist on "object" (objects recursively)
|
|
||||||
* @param {object} object Any object to apply the changes to (mutates)
|
|
||||||
* @param {object} defaults Defaults object to apply the changes from
|
|
||||||
*
|
|
||||||
* @returns {object} the object that was passed in the first argument
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const object = {};
|
|
||||||
* makeDefaults(object, {
|
|
||||||
* example: 6,
|
|
||||||
* exampleTwo: {
|
|
||||||
* example: 1
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export function makeDefaults(object, defaults) {
|
|
||||||
if (object === undefined) throw new Error("No object passed to make defaults for");
|
|
||||||
if (defaults === undefined) throw new Error("No defaults object passed to make defaults off of");
|
|
||||||
|
|
||||||
for (const key in defaults) {
|
|
||||||
if (typeof defaults[key] === "object" && !Array.isArray(defaults[key])) {
|
|
||||||
if (typeof object[key] !== "object") object[key] = {};
|
|
||||||
makeDefaults(object[key], defaults[key]);
|
|
||||||
} else {
|
|
||||||
object[key] ??= defaults[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EMOJIS = {
|
|
||||||
getLoading() {
|
|
||||||
return Math.random() < 0.01 ? this.aol : this.loadingDiscordSpinner;
|
|
||||||
},
|
|
||||||
getFailure() {
|
|
||||||
return Math.random() < 0.01 ? this.fuckyoy : this.linuth;
|
|
||||||
},
|
|
||||||
getSuccess() {
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
loadingDiscordSpinner: "a:loading:1105495814073229393",
|
|
||||||
aol: "a:aol:1108834296359301161",
|
|
||||||
linuth: ":linuth:1110531631409811547",
|
|
||||||
fuckyoy: ":fuckyoy:1108360628302782564",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AVATARS = {
|
|
||||||
command: "https://cdn.discordapp.com/attachments/1099116247364407337/1112129955053187203/command.png",
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Generates a random number within a specified range
|
|
||||||
* @param {number} min The minimum value of the range (inclusive)
|
|
||||||
* @param {number} max The maximum value of the range (inclusive)
|
|
||||||
* @param {number} [precision=0] The number of decimal places to round the result to
|
|
||||||
*
|
|
||||||
* @returns {number} A random number within the specified range
|
|
||||||
* @throws {Error} If the arguments are invalid or precision is greater than 13
|
|
||||||
*/
|
|
||||||
export function rng(min, max, precision = 0) {
|
|
||||||
if (typeof min !== "number" || isNaN(min)) {
|
|
||||||
throw new Error("Invalid first argument, minimum: expected a number");
|
|
||||||
}
|
|
||||||
if (typeof max !== "number" || isNaN(max)) {
|
|
||||||
throw new Error("Invalid second argument, maximum: expected a number");
|
|
||||||
}
|
|
||||||
if (typeof precision !== "number" || precision < 0) {
|
|
||||||
throw new Error("Invalid third argument, precision: expected a positive number");
|
|
||||||
}
|
|
||||||
if (precision > 13) {
|
|
||||||
throw new Error("Invalid third argument, precision: expected a number < 13");
|
|
||||||
}
|
|
||||||
if (precision !== 0 && precision % 1 !== 0) {
|
|
||||||
throw new Error("Invalid third argument, precision: expected an integer");
|
|
||||||
}
|
|
||||||
const maxPrecision = Math.max((max.toString().split(".")[1] || "").length, (min.toString().split(".")[1] || "").length);
|
|
||||||
const computedPrecision = typeof precision === "number" ? precision : maxPrecision;
|
|
||||||
return parseFloat((Math.random() * (max - min) + min).toFixed(computedPrecision));
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
export function mSendMessage(vendetta) {
|
|
||||||
const {
|
|
||||||
metro: {
|
|
||||||
findByProps,
|
|
||||||
findByStoreName,
|
|
||||||
common: {
|
|
||||||
lodash: { merge },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = vendetta;
|
|
||||||
const Send = findByProps("_sendMessage");
|
|
||||||
const { createBotMessage } = findByProps("createBotMessage");
|
|
||||||
const Avatars = findByProps("BOT_AVATARS");
|
|
||||||
const { getChannelId: getFocusedChannelId } = findByStoreName("SelectedChannelStore");
|
|
||||||
return function (message, mod) {
|
|
||||||
message.channelId ??= getFocusedChannelId();
|
|
||||||
if ([null, undefined].includes(message.channelId)) throw new Error("No channel id to receive the message into (channelId)");
|
|
||||||
let msg = message;
|
|
||||||
if (message.really) {
|
|
||||||
if (typeof mod === "object") msg = merge(msg, mod);
|
|
||||||
const args = [msg, {}];
|
|
||||||
args[0].tts ??= false;
|
|
||||||
for (const key of ["allowedMentions", "messageReference"]) {
|
|
||||||
if (key in args[0]) {
|
|
||||||
args[1][key] = args[0][key];
|
|
||||||
delete args[0][key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const overwriteKey = "overwriteSendMessageArg2"
|
|
||||||
if (overwriteKey in args[0]) {
|
|
||||||
// so that you can use the keys i may have missed in the for loop above
|
|
||||||
args[1] = args[0][overwriteKey];
|
|
||||||
delete args[0][overwriteKey];
|
|
||||||
}
|
|
||||||
return Send._sendMessage(message.channelId, ...args);
|
|
||||||
}
|
|
||||||
if (mod !== true) msg = createBotMessage(msg);
|
|
||||||
if (typeof mod === "object") {
|
|
||||||
msg = merge(msg, mod);
|
|
||||||
if (typeof mod.author === "object")
|
|
||||||
(function processAvatarURL() {
|
|
||||||
const author = mod.author;
|
|
||||||
if (typeof author.avatarURL === "string") {
|
|
||||||
Avatars.BOT_AVATARS[author.avatar ?? author.avatarURL] = author.avatarURL;
|
|
||||||
author.avatar ??= author.avatarURL
|
|
||||||
delete author.avatarURL;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
Send.receiveMessage(msg.channel_id, msg);
|
|
||||||
return msg;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
const characters =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
export const characters2 = characters.replace("+/", "_-");
|
|
||||||
|
|
||||||
export function encode(data, discord) {
|
|
||||||
if (typeof data !== "number")
|
|
||||||
throw new Error(`Passed data isn't a number (received: ${typeof data})`);
|
|
||||||
|
|
||||||
let result = "";
|
|
||||||
while (data > 0) {
|
|
||||||
result = (discord ? characters2 : characters).charAt(data % 64) + result;
|
|
||||||
data = Math.floor(data / 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decode(data, discord) {
|
|
||||||
if (typeof data !== "string")
|
|
||||||
throw new Error(`Passed data isn't a string (received: ${typeof data})`);
|
|
||||||
|
|
||||||
let result = 0;
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
result *= 64;
|
|
||||||
result += (discord ? characters2 : characters).indexOf(data[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { TouchableOpacity, Text, Image, View } from "@vendetta/ui/components/General";
|
|
||||||
import { createThemedStyleSheet } from "@vendetta/metro/common/stylesheet";
|
|
||||||
import { semanticColors } from "@vendetta/ui";
|
|
||||||
|
|
||||||
const stylesheet = createThemedStyleSheet({
|
|
||||||
container: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
marginLeft: 5,
|
|
||||||
marginRight: 5,
|
|
||||||
width: 25,
|
|
||||||
height: 25,
|
|
||||||
borderRadius: 100,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
color: semanticColors.HEADER_PRIMARY,
|
|
||||||
},
|
|
||||||
labelRemove: {
|
|
||||||
color: semanticColors.TEXT_WARNING,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ({ imageSource, onImagePress, label, labelRemove = "REMOVE", onRemove }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={stylesheet.container}>
|
|
||||||
<TouchableOpacity onPress={onRemove}>
|
|
||||||
<Text style={stylesheet.labelRemove}>{labelRemove}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity onPress={onImagePress}>
|
|
||||||
<Image style={[stylesheet.image]} source={imageSource} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={stylesheet.label}>{label}</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# Read All
|
|
||||||
## Read all server and DM notifications with single command - /read.
|
|
||||||
|
|
||||||
Images:
|
|
||||||
<img width="1220" height="451" alt="Screenshot_20250725-093823 Discord" src="https://github.com/user-attachments/assets/71ad386f-0677-4998-be18-2d4ae3e2c815" />
|
|
||||||
<img width="1220" height="790" alt="Screenshot_20250725-093814 Discord" src="https://github.com/user-attachments/assets/ac8aa4db-1ccb-415a-a3a1-6b106a61d7df" />
|
|
||||||
<img width="1220" height="354" alt="Screenshot_20250725-093847 Discord" src="https://github.com/user-attachments/assets/8f128874-6069-4ec8-bd14-efdaeec1bf73" />
|
|
||||||
<img width="1220" height="355" alt="Screenshot_20250725-094021 Discord" src="https://github.com/user-attachments/assets/8ccff92a-78ff-429d-91cf-7626990666fc" />
|
|
||||||
|
|
||||||
|
|
||||||
🔗 [**Install**](https://apexteampl.github.io/Apex-Plugins/ReadAll)
|
|
||||||
👷♂️ [**Source code**](https://github.com/ApexTeamPL/Apex-Plugins/tree/master/plugins/ReadAll)
|
|
||||||
🪰 [**Bugs**](https://github.com/ApexTeamPL/Apex-Plugins/issues/new?title=[ReadAl
|
|
||||||
l])
|
|
||||||
|
Before Width: | Height: | Size: 646 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
|
@ -1,50 +0,0 @@
|
||||||
# BetterNSFWGateBypass
|
|
||||||
## Allows you to access NSFW channels without setting your age.
|
|
||||||
|
|
||||||
### NOTE: This plugin breaks Vencore ToS in one or more points ("We do not sell or distribute content or programs bypassing legal law in EU") therefore this plugin will be deleted in following weeks. The reason? Not Vencore's ToS (as we all know VMuser can change it sooner or later...) but the fact that UK mode is f...ed up! And now this plugin is um illegal. Well then I decided to take actions. Either add some privacy control or delete it from the repo.
|
|
||||||
__ __
|
|
||||||
|
|
||||||
__ __
|
|
||||||
This is daily checked by me (Win8.1VMUser) to ensure it works. And yes it works!
|
|
||||||
Stay safe and for plugin ideas dm me on dc (same username as I typed above!).
|
|
||||||
### WARNING: This plugin has been OPTIMIZED for Voxi and Kettu! Using it on any versions below 290 may cause BUGS or make it NOT WORKING! That's why we've flagged this plugin as warning in our repo. Please use 290+ as stated [HERE](https://github.com/ApexTeamPL/Vencore/blob/main/docs/vencore-mobile/)
|
|
||||||
### NOT ANYONE... We've checked it on Revenge 292014, 287013 and Vencore 287013 and 292014 - doesn't work here too... Looks like it is IP-banned (like Cloudflare can IP ban specific sites). Like I said - we cannot do anything I guess. Please use any VPN and let's hope it'll get fixed sooner or later. Gate bypass works so I can say - it is... legal now. ***VENCORE HAS A PROXY BUILT-IN, I'LL UPDATE THE PLUGIN TO USE THE PROXY ONCE DEVS RELEASE IT - CREATOR***
|
|
||||||
__ __
|
|
||||||
### Guys... Thank you all for your support.
|
|
||||||
And VMuser, thanks for settings, it is pretty easy to manage settings with it.
|
|
||||||
> I am waiting for Vencore version. I believe he can do it! The fact I almost gave up after he added passwords... Well, no not this time. It is for all UK people who doesn't want to give discord their IDs.
|
|
||||||
Use it wisely as always (and don't spread it a lot or VMuser will add password again -_-)
|
|
||||||
|
|
||||||
> > > > ***Content removed due to breaking Apex Plugins posting guidelines. The creator has been warned.***
|
|
||||||
|
|
||||||
# SAVE THE UK GUYS!
|
|
||||||
|
|
||||||
__ __
|
|
||||||
To owner of the repo: thanks for adjustments, keep up with your plugins, they're **AWESOME**.
|
|
||||||
> I know Kayla, I know :)
|
|
||||||
|
|
||||||
__ __
|
|
||||||
### Well... NO! (let him do his polish yapping)
|
|
||||||
This plugin was directly used and created by her to skip age verify (UK laws...) But hey... Isn't that illegal?!
|
|
||||||
Yeah it mostly is and could lead me (the owner of Vencore and this repo) to some bad lawsuits or worse (even tho I'm from Poland).
|
|
||||||
Therefore this plugin has been modded to bypass the GATE ONLY (aka soft banned accs which show You are too young to display this channel) not HACK YOUR AGE.
|
|
||||||
|
|
||||||
Note: you can bypass age verification only if you know secret PASSWORD. The other options are safe and unblocked as they aren't meant to bypass any verification :)
|
|
||||||
If you do somehow gain password - your age is set as Adult. Therefore you don't have to worry!
|
|
||||||
However, to the creator - Slara please don't be mad. It is for our safety and therefore this plugin can be posted to Plugin List in Revenge.
|
|
||||||
|
|
||||||
__ __
|
|
||||||
### UPDATE: I won't delete the upper thing. I kept my password lock version but...
|
|
||||||
The creator fr is uh let's nicely say sensitive person. And even plugin list dev told me I can keep passwordless ver - "keep it simple"
|
|
||||||
So what. No passwd lock but the age bypassing is disabled by default. Please note I am not planning to change it.
|
|
||||||
**WE'VE ADDED PASSWORD LOCK AGAIN**
|
|
||||||
NOTE: AS STATED ABOVE IT WORKS FINE YOU JUST NEED PASSWORD! HAVE FUN!!!
|
|
||||||
|
|
||||||
### To UK users: note - Vencore also has this mode.
|
|
||||||
The DIRECT PORT OF THIS PLUGIN, this way we (free users) can just bypass gate and you bypass everything. Sorry for inconsistencies and see ya with new plugins!
|
|
||||||
|
|
||||||
## UPDATE AS OF 2026 - READ IT
|
|
||||||
So it happened. **Discord is expanding its age verification worldwide...** but there is a fix!
|
|
||||||
Some dataminers of Discord's verification managed to bypass the checks using just **DevTools**!
|
|
||||||
Sadly I cannot do anything without direct access to the system, or can I if we had Discord web client... (**you need DevTools access, use Kiwi Browser or Termux**). Yes, really just DevTools and you're done!
|
|
||||||
So be quick and check the *special* site to find out if the thing works. Anyway, install LoA-Tools on Termux for quicker verification :)
|
|
||||||
3
messageScheduler/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
(function(h,r,w,p){"use strict";const B=p.findByProps("sendMessage","receiveMessage"),c=p.findByProps("sendBotMessage");let m={},g={},M,y,v;function C(n,s,t){const e=`${n}:${t}`,o=t-Date.now();return g[e]={content:s,at:t},m[e]=setTimeout(function(){const a=s,i=Date.now().toString();B.sendMessage(n,{content:a},void 0,{nonce:i}),delete m[e],delete g[e],w.showToast("Scheduled message sent.")},o),e}function D(){Object.values(m).forEach(clearTimeout),m={},g={}}function I(n){const s=new Date;n=n.toLowerCase();const t=/(\d+)(h|m|s)/g;let e=0,o;for(;(o=t.exec(n))!==null;){const d=parseInt(o[1]),u=o[2];u==="h"?e+=d*3600*1e3:u==="m"?e+=d*60*1e3:u==="s"&&(e+=d*1e3)}if(e>0)return Date.now()+e;const a=/^(\d{1,2}):(\d{2})(am|pm)?$/,i=n.match(a);if(i){let d=parseInt(i[1]);const u=parseInt(i[2]),f=i[3];f&&(f==="pm"&&d<12?d+=12:f==="am"&&d===12&&(d=0));const l=new Date(s);return l.setHours(d,u,0,0),l.getTime()<=s.getTime()&&l.setDate(l.getDate()+1),l.getTime()}return 0}function $(){M=r.registerCommand({name:"schedule",displayName:"schedule",description:"Schedule a message after a delay or at a time",options:[{name:"time",description:"Delay (e.g. 10s, 1h30m) or time (3:30pm, 15:30)",type:3,required:!0},{name:"message",description:"Message to send",type:3,required:!0}],execute:function(n,s){const t=s.channel.id,e=n[0]?.value,o=n[1]?.value;if(typeof e!="string"||e.trim()==="")return c.sendBotMessage(t,"Invalid time value!");if(typeof o!="string"||o.trim()==="")return c.sendBotMessage(t,"Message cannot be empty!");const a=I(e);if(a===0||a<=Date.now())return c.sendBotMessage(t,"Invalid or past time specified!");C(t,o,a);const i=Math.round((a-Date.now())/1e3);c.sendBotMessage(t,`Scheduled message in ${i}s.`)}}),y=r.registerCommand({name:"scheduled",displayName:"scheduled",description:"List scheduled messages",execute:function(n,s){const t=s.channel.id,e=Object.entries(g).map(function([o,a]){return`\u2022 **In ${Math.round((a.at-Date.now())/1e3)}s** \u2192 ${a.content}`});e.length===0?c.sendBotMessage(t,"No scheduled messages."):c.sendBotMessage(t,`Scheduled messages:
|
||||||
|
${e.join(`
|
||||||
|
`)}`)}}),v=r.registerCommand({name:"cancel-scheduled",displayName:"cancel-scheduled",description:"Cancel all scheduled messages",execute:function(n,s){const t=s.channel.id;D(),c.sendBotMessage(t,"All scheduled messages cancelled.")}})}function T(){D(),r.unregisterCommand(M),r.unregisterCommand(y),r.unregisterCommand(v)}return h.onLoad=$,h.onUnload=T,h})({},vendetta.commands,vendetta.ui.toasts,vendetta.metro);
|
||||||
1
messageScheduler/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"Message Scheduler","description":"Schedule messages to be sent at specific time or after a delay with /schedule.","authors":[{"name":"VMuser","id":"1258451646850007042"}],"main":"index.js","vendetta":{"icon":"ic_edit_24px"},"hash":"9b6a23f7850c5ab6532c2e8a03d9252ae738dbb88dcb0bf333061081b2dd7640"}
|
||||||
1
nsfwbypass/index.js
Normal file
1
nsfwbypass/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"BetterNSFWGateBypass","description":"Allows you to access NSFW channels without setting your age.","authors":[{"name":"Slara","id":"1008715724115685449"},{"name":"Win8.1VMUser","id":"793935599702507542"}],"main":"index.js","vendetta":{"icon":"ic_edit_24px"},"hash":"08b8c133c1de80e2891d6a8caee276418a0b555345f2f16ad602c5fbcfc4337c"}
|
||||||
26
package.json
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"build": "node build.mjs"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.4.14"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-commonjs": "^24.0.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
||||||
"@swc/core": "^1.3.35",
|
|
||||||
"esbuild": "^0.16.14",
|
|
||||||
"rollup": "^3.9.1",
|
|
||||||
"rollup-plugin-esbuild": "^5.0.0",
|
|
||||||
"vendetta-types": "latest"
|
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"peerDependencyRules": {
|
|
||||||
"ignoreMissing": [
|
|
||||||
"react",
|
|
||||||
"react-native"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,444 +0,0 @@
|
||||||
import { showConfirmationAlert } from "@vendetta/ui/alerts";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { clipboard } from "@vendetta/metro/common";
|
|
||||||
import { findByProps, findByStoreName } from "@vendetta/metro";
|
|
||||||
import { addLog } from "./Logger.js";
|
|
||||||
import { simpleHash } from "./PasswordUtils.js";
|
|
||||||
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
const TokenManager = findByProps("getToken");
|
|
||||||
|
|
||||||
const exportAccounts = async (storage, showPasswordDialog) => {
|
|
||||||
const checkPassword = (callback) => {
|
|
||||||
if (storage.settings?.exportPasswordHash) {
|
|
||||||
showPasswordDialog({
|
|
||||||
type: 'export',
|
|
||||||
title: 'Enter Export Password',
|
|
||||||
message: 'Enter your password to export accounts',
|
|
||||||
callback: callback
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPassword(() => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "⚠️ Export Accounts - Security Warning",
|
|
||||||
content: "Exporting accounts is UNSAFE and may lead to account takeover if shared with others. The export contains sensitive authentication tokens that give full access to your accounts. Only proceed if you understand the risks and will keep the data secure.",
|
|
||||||
confirmText: "I Understand - Export",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
confirmColor: "brand",
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
addLog('info', 'Starting account export');
|
|
||||||
const accounts = storage.accounts || {};
|
|
||||||
const accountData = Object.values(accounts).map(account => ({
|
|
||||||
username: account.username,
|
|
||||||
discriminator: account.discriminator,
|
|
||||||
avatar: account.avatar,
|
|
||||||
id: account.id,
|
|
||||||
token: account.token,
|
|
||||||
addedAt: account.addedAt || Date.now()
|
|
||||||
}));
|
|
||||||
|
|
||||||
const exportData = {
|
|
||||||
accounts: accountData,
|
|
||||||
exportPasswordHash: storage.settings?.exportPasswordHash || null,
|
|
||||||
exportedAt: Date.now(),
|
|
||||||
version: "2.0"
|
|
||||||
};
|
|
||||||
|
|
||||||
clipboard.setString(JSON.stringify(exportData, null, 2));
|
|
||||||
addLog('info', 'Accounts exported successfully', { count: accountData.length });
|
|
||||||
showToast(`Exported ${accountData.length} accounts to clipboard`, 0);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('error', 'Failed to export accounts', { error: e.message });
|
|
||||||
console.error("Export error:", e);
|
|
||||||
showToast("Failed to export accounts", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const importAccounts = async (storage, showPasswordDialog, importText) => {
|
|
||||||
const checkPassword = (callback) => {
|
|
||||||
if (storage.settings?.exportPasswordHash) {
|
|
||||||
showPasswordDialog({
|
|
||||||
type: 'import',
|
|
||||||
title: 'Enter Import Password',
|
|
||||||
message: 'Enter your password to import accounts',
|
|
||||||
callback: callback
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPassword(() => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "⚠️ Import Accounts - Security Warning",
|
|
||||||
content: "Importing accounts is UNSAFE and may compromise your security if the data comes from untrusted sources. Only import data you exported yourself or from sources you completely trust. Malicious imports could lead to account takeover.",
|
|
||||||
confirmText: "I Understand - Import",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
confirmColor: "brand",
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
addLog('info', 'Starting account import');
|
|
||||||
let dataToImport = importText.trim();
|
|
||||||
if (!dataToImport) {
|
|
||||||
try {
|
|
||||||
dataToImport = await clipboard.getString();
|
|
||||||
} catch (e) {
|
|
||||||
addLog('error', 'Failed to get clipboard data', { error: e.message });
|
|
||||||
showToast("No data in clipboard or input field", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dataToImport) {
|
|
||||||
addLog('warn', 'No data to import');
|
|
||||||
showToast("No data to import", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importData = JSON.parse(dataToImport);
|
|
||||||
let accountsArray;
|
|
||||||
|
|
||||||
if (importData.accounts && Array.isArray(importData.accounts)) {
|
|
||||||
if (importData.exportPasswordHash) {
|
|
||||||
if (!storage.settings?.exportPasswordHash || storage.settings.exportPasswordHash !== importData.exportPasswordHash) {
|
|
||||||
addLog('error', 'Import password mismatch');
|
|
||||||
showToast("Import password mismatch or not set locally", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
accountsArray = importData.accounts;
|
|
||||||
|
|
||||||
if (importData.exportPasswordHash && !storage.settings?.exportPasswordHash) {
|
|
||||||
storage.settings.exportPasswordHash = importData.exportPasswordHash;
|
|
||||||
addLog('info', 'Import password hash set from import data');
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(importData)) {
|
|
||||||
accountsArray = importData;
|
|
||||||
addLog('debug', 'Using legacy import format');
|
|
||||||
} else {
|
|
||||||
addLog('error', 'Invalid import format');
|
|
||||||
showToast("Invalid import format", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let importedCount = 0;
|
|
||||||
let skippedCount = 0;
|
|
||||||
|
|
||||||
accountsArray.forEach(accountData => {
|
|
||||||
if (accountData.id && accountData.token && accountData.username) {
|
|
||||||
if (!storage.accounts[accountData.id]) {
|
|
||||||
storage.accounts[accountData.id] = {
|
|
||||||
id: accountData.id,
|
|
||||||
username: accountData.username,
|
|
||||||
discriminator: accountData.discriminator || "0",
|
|
||||||
avatar: accountData.avatar || null,
|
|
||||||
displayName: accountData.displayName || accountData.username,
|
|
||||||
token: accountData.token,
|
|
||||||
addedAt: accountData.addedAt || Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!storage.accountOrder.includes(accountData.id)) {
|
|
||||||
storage.accountOrder.push(accountData.id);
|
|
||||||
}
|
|
||||||
importedCount++;
|
|
||||||
} else {
|
|
||||||
skippedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addLog('info', 'Import completed', { imported: importedCount, skipped: skippedCount });
|
|
||||||
showToast(`Imported ${importedCount} accounts, skipped ${skippedCount} duplicates`, 0);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('error', 'Import failed', { error: e.message });
|
|
||||||
console.error("Import error:", e);
|
|
||||||
showToast("Failed to import - invalid format or password mismatch", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setExportPassword = (storage, newPassword, confirmPassword, setNewPassword, setConfirmPassword) => {
|
|
||||||
if (!newPassword.trim()) {
|
|
||||||
showToast("Please enter a password", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
showToast("Passwords don't match", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = simpleHash(newPassword);
|
|
||||||
storage.settings.exportPasswordHash = passwordHash;
|
|
||||||
setNewPassword("");
|
|
||||||
setConfirmPassword("");
|
|
||||||
addLog('info', 'Export password set successfully');
|
|
||||||
showToast("Export password set successfully", 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeExportPassword = (storage, showPasswordDialog) => {
|
|
||||||
const checkPassword = (callback) => {
|
|
||||||
showPasswordDialog({
|
|
||||||
type: 'remove',
|
|
||||||
title: 'Enter Password to Remove',
|
|
||||||
message: 'Enter your current password to remove protection',
|
|
||||||
callback: callback
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPassword(() => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "Remove Export Password",
|
|
||||||
content: "Are you sure you want to remove the export password? This will make exports/imports less secure.",
|
|
||||||
confirmText: "Remove Password",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
confirmColor: "brand",
|
|
||||||
onConfirm: () => {
|
|
||||||
delete storage.settings.exportPasswordHash;
|
|
||||||
addLog('info', 'Export password removed');
|
|
||||||
showToast("Export password removed", 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAccountWithToken = async (storage, newToken, setNewToken, setIsAdding) => {
|
|
||||||
if (!storage.settings.enableUnsafeFeatures) return;
|
|
||||||
|
|
||||||
setIsAdding(true);
|
|
||||||
addLog('info', 'Starting token account addition');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let token = newToken.trim();
|
|
||||||
if (!token) {
|
|
||||||
token = TokenManager.getToken();
|
|
||||||
addLog('debug', 'Using current account token');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!token.startsWith("Bot ") && !token.match(/^[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}$/)) {
|
|
||||||
addLog('error', 'Invalid token format provided');
|
|
||||||
showToast("Invalid token format", 1);
|
|
||||||
setIsAdding(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLog('debug', 'Fetching user info from Discord API');
|
|
||||||
const response = await fetch("https://discord.com/api/v9/users/@me", {
|
|
||||||
headers: { "Authorization": token, "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
addLog('error', 'Discord API request failed', { status: response.status, statusText: response.statusText });
|
|
||||||
showToast("Invalid or expired token", 1);
|
|
||||||
setIsAdding(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await response.json();
|
|
||||||
addLog('info', 'Successfully retrieved user info', { username: user.username, id: user.id });
|
|
||||||
|
|
||||||
const userInfo = {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
discriminator: user.discriminator,
|
|
||||||
avatar: user.avatar,
|
|
||||||
displayName: user.global_name || user.username
|
|
||||||
};
|
|
||||||
|
|
||||||
if (storage.accounts[userInfo.id]) {
|
|
||||||
addLog('warn', 'Account already exists', { username: userInfo.username });
|
|
||||||
showToast(`Account ${userInfo.username} already saved`, 1);
|
|
||||||
setIsAdding(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccount = { ...userInfo, token: token, addedAt: Date.now() };
|
|
||||||
|
|
||||||
storage.accounts[userInfo.id] = newAccount;
|
|
||||||
if (!storage.accountOrder.includes(userInfo.id)) {
|
|
||||||
storage.accountOrder.push(userInfo.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewToken("");
|
|
||||||
addLog('info', 'Account added successfully via token', { username: userInfo.username });
|
|
||||||
showToast(`Account ${userInfo.username} added!`, 0);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('error', 'Failed to add account via token', { error: e.message, stack: e.stack });
|
|
||||||
console.error("Add account error:", e);
|
|
||||||
showToast("Failed to add account", 1);
|
|
||||||
}
|
|
||||||
setIsAdding(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceLogout = async () => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "Force Logout",
|
|
||||||
content: "This will logout your current session by using an invalid token. Your saved accounts will remain intact. Continue?",
|
|
||||||
confirmText: "Force Logout",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
confirmColor: "brand",
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
addLog('info', 'Starting force logout');
|
|
||||||
const fakeToken = "MTExMTExMTExMTExMTExMTEx.GxxxXx.xXxXxXxXxXxXxXxXxXxXxXxXxXxXx";
|
|
||||||
await findByProps("login", "logout", "switchAccountToken").switchAccountToken(fakeToken);
|
|
||||||
addLog('info', 'Force logout completed successfully');
|
|
||||||
showToast("Force logout successful - you can now login to a different account", 0);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('warn', 'Force logout completed with error', { error: e.message });
|
|
||||||
console.error("Force logout error:", e);
|
|
||||||
showToast("Force logout completed", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAccountWithCredentials = async (storage, email, password, setEmail, setPassword, setShowAddDialog, setIsAddingDynamic) => {
|
|
||||||
if (!email.trim() || !password.trim()) {
|
|
||||||
showToast("Please enter both email and password", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsAddingDynamic(true);
|
|
||||||
addLog('info', 'Starting credential-based account addition', { email: email.trim() });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("https://discord.com/api/v9/auth/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
login: email.trim(),
|
|
||||||
password: password.trim()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginData = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok || !loginData.token) {
|
|
||||||
addLog('error', 'Discord login failed', {
|
|
||||||
status: response.status,
|
|
||||||
message: loginData.message,
|
|
||||||
errors: loginData.errors,
|
|
||||||
captchaRequired: !!loginData.captcha_key
|
|
||||||
});
|
|
||||||
|
|
||||||
let errorMessage = "Login failed";
|
|
||||||
|
|
||||||
if (loginData.captcha_key) {
|
|
||||||
errorMessage = "Captcha required - please login through Discord first";
|
|
||||||
} else if (loginData.message) {
|
|
||||||
const msg = loginData.message.toLowerCase();
|
|
||||||
if (msg.includes('invalid') && (msg.includes('email') || msg.includes('phone') || msg.includes('login'))) {
|
|
||||||
errorMessage = "Invalid email or username";
|
|
||||||
} else if (msg.includes('password')) {
|
|
||||||
errorMessage = "Invalid password";
|
|
||||||
} else if (msg.includes('account')) {
|
|
||||||
errorMessage = "Account issue - check your credentials";
|
|
||||||
} else if (msg.includes('rate limit') || msg.includes('too many')) {
|
|
||||||
errorMessage = "Too many attempts - please wait and try again";
|
|
||||||
} else if (msg.includes('mfa') || msg.includes('2fa') || msg.includes('verification')) {
|
|
||||||
errorMessage = "2FA/MFA accounts not supported via credentials";
|
|
||||||
} else {
|
|
||||||
errorMessage = "Login failed - check your credentials";
|
|
||||||
}
|
|
||||||
} else if (loginData.errors) {
|
|
||||||
const errors = loginData.errors;
|
|
||||||
if (errors.login) {
|
|
||||||
errorMessage = "Invalid email or username";
|
|
||||||
} else if (errors.password) {
|
|
||||||
errorMessage = "Invalid password";
|
|
||||||
} else {
|
|
||||||
errorMessage = "Invalid credentials";
|
|
||||||
}
|
|
||||||
} else if (response.status === 401) {
|
|
||||||
errorMessage = "Invalid credentials";
|
|
||||||
} else if (response.status === 429) {
|
|
||||||
errorMessage = "Rate limited - please wait and try again";
|
|
||||||
} else if (response.status >= 500) {
|
|
||||||
errorMessage = "Discord servers are having issues - try again later";
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(errorMessage, 1);
|
|
||||||
setIsAddingDynamic(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addLog('debug', 'Login successful, fetching user info');
|
|
||||||
const userResponse = await fetch("https://discord.com/api/v9/users/@me", {
|
|
||||||
headers: { "Authorization": loginData.token }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse.ok) {
|
|
||||||
addLog('error', 'Failed to get user info after login', { status: userResponse.status });
|
|
||||||
showToast("Login succeeded but failed to get user info - check logs", 1);
|
|
||||||
setIsAddingDynamic(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await userResponse.json();
|
|
||||||
|
|
||||||
if (storage.accounts[user.id]) {
|
|
||||||
addLog('warn', 'Account already exists', { username: user.username });
|
|
||||||
showToast(`Account ${user.username} already saved`, 1);
|
|
||||||
setIsAddingDynamic(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccount = {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
discriminator: user.discriminator,
|
|
||||||
avatar: user.avatar,
|
|
||||||
displayName: user.global_name || user.username,
|
|
||||||
token: loginData.token,
|
|
||||||
addedAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
storage.accounts[user.id] = newAccount;
|
|
||||||
if (!storage.accountOrder.includes(user.id)) {
|
|
||||||
storage.accountOrder.push(user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
setEmail("");
|
|
||||||
setPassword("");
|
|
||||||
setShowAddDialog(false);
|
|
||||||
addLog('info', 'Account added successfully via credentials', { username: user.username });
|
|
||||||
showToast(`Account ${user.username} added successfully!`, 0);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('error', 'Credential login failed', { error: e.message, stack: e.stack });
|
|
||||||
console.error("Login error:", e);
|
|
||||||
|
|
||||||
let errorMessage = "Login failed - check logs for more info";
|
|
||||||
if (e.message.includes('network') || e.message.includes('fetch')) {
|
|
||||||
errorMessage = "Network error - check your connection";
|
|
||||||
} else if (e.message.includes('timeout')) {
|
|
||||||
errorMessage = "Request timed out - try again";
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(errorMessage, 1);
|
|
||||||
}
|
|
||||||
setIsAddingDynamic(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
exportAccounts,
|
|
||||||
importAccounts,
|
|
||||||
setExportPassword,
|
|
||||||
removeExportPassword,
|
|
||||||
addAccountWithToken,
|
|
||||||
addAccountWithCredentials,
|
|
||||||
forceLogout
|
|
||||||
};
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { storage } from "@vendetta/plugin";
|
|
||||||
|
|
||||||
const pluginLogs = [];
|
|
||||||
const maxLogs = 1000;
|
|
||||||
|
|
||||||
function addLog(type, message, data = null) {
|
|
||||||
const logEntry = {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
type: type,
|
|
||||||
message: message,
|
|
||||||
data: data
|
|
||||||
};
|
|
||||||
|
|
||||||
pluginLogs.unshift(logEntry);
|
|
||||||
if (pluginLogs.length > maxLogs) {
|
|
||||||
pluginLogs.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage?.settings?.enableUnsafeFeatures) {
|
|
||||||
console.log(`[AccountSwitcher] ${type.toUpperCase()}: ${message}`, data || '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogs() {
|
|
||||||
return pluginLogs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLogs() {
|
|
||||||
pluginLogs.length = 0;
|
|
||||||
addLog('info', 'Logs cleared');
|
|
||||||
}
|
|
||||||
|
|
||||||
export { addLog, getLogs, clearLogs };
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# A placeholder file.
|
|
||||||
Uploading files in progress!
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { React, ReactNative } from "@vendetta/metro/common";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { simpleHash } from "./PasswordUtils.js";
|
|
||||||
import { addLog } from "./Logger.js";
|
|
||||||
|
|
||||||
function PasswordPrompt({ dialogInfo, onCancel, onSuccess, storage }) {
|
|
||||||
const [passwordInput, setPasswordInput] = React.useState("");
|
|
||||||
|
|
||||||
const handlePasswordDialog = () => {
|
|
||||||
if (!passwordInput.trim()) {
|
|
||||||
showToast("Please enter the password", 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputHash = simpleHash(passwordInput);
|
|
||||||
if (inputHash === storage.settings.exportPasswordHash) {
|
|
||||||
addLog('info', 'Password verification successful', { type: dialogInfo.type });
|
|
||||||
onSuccess();
|
|
||||||
} else {
|
|
||||||
addLog('warn', 'Incorrect password attempt', { type: dialogInfo.type });
|
|
||||||
showToast("Incorrect password", 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return React.createElement(ReactNative.View, {
|
|
||||||
style: { flex: 1, backgroundColor: '#2f3136' }
|
|
||||||
}, [
|
|
||||||
React.createElement(ReactNative.View, {
|
|
||||||
key: "pwd-header",
|
|
||||||
style: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
backgroundColor: '#202225',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#40444b'
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
React.createElement(ReactNative.TouchableOpacity, {
|
|
||||||
key: "back",
|
|
||||||
onPress: onCancel,
|
|
||||||
style: { marginRight: 16 }
|
|
||||||
}, React.createElement(ReactNative.Text, {
|
|
||||||
style: { color: '#7289da', fontSize: 16 }
|
|
||||||
}, "← Cancel")),
|
|
||||||
React.createElement(ReactNative.Text, {
|
|
||||||
key: "title",
|
|
||||||
style: { color: 'white', fontSize: 18, fontWeight: 'bold' }
|
|
||||||
}, dialogInfo.title)
|
|
||||||
]),
|
|
||||||
|
|
||||||
React.createElement(ReactNative.View, {
|
|
||||||
key: "pwd-content",
|
|
||||||
style: { flex: 1, padding: 16, justifyContent: 'center' }
|
|
||||||
}, [
|
|
||||||
React.createElement(ReactNative.Text, {
|
|
||||||
key: "message",
|
|
||||||
style: { color: '#b9bbbe', fontSize: 16, marginBottom: 20, textAlign: 'center' }
|
|
||||||
}, dialogInfo.message),
|
|
||||||
|
|
||||||
React.createElement(ReactNative.TextInput, {
|
|
||||||
key: "password-input",
|
|
||||||
placeholder: "Enter password",
|
|
||||||
placeholderTextColor: '#72767d',
|
|
||||||
value: passwordInput,
|
|
||||||
onChangeText: setPasswordInput,
|
|
||||||
secureTextEntry: true,
|
|
||||||
style: {
|
|
||||||
backgroundColor: '#40444b',
|
|
||||||
color: 'white',
|
|
||||||
padding: 16,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginBottom: 20,
|
|
||||||
fontSize: 16
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
React.createElement(ReactNative.TouchableOpacity, {
|
|
||||||
key: "confirm-btn",
|
|
||||||
onPress: handlePasswordDialog,
|
|
||||||
style: {
|
|
||||||
backgroundColor: '#7289da',
|
|
||||||
paddingVertical: 16,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 8,
|
|
||||||
alignItems: 'center'
|
|
||||||
}
|
|
||||||
}, React.createElement(ReactNative.Text, {
|
|
||||||
style: { color: 'white', fontSize: 18, fontWeight: 'bold' }
|
|
||||||
}, "Confirm"))
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PasswordPrompt };
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
function simpleHash(str) {
|
|
||||||
let hash = 0;
|
|
||||||
if (str.length === 0) return hash.toString();
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash;
|
|
||||||
}
|
|
||||||
return Math.abs(hash).toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { simpleHash };
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
import { storage, manifest } from "@vendetta/plugin";
|
|
||||||
import { React, NavigationNative } from "@vendetta/metro/common";
|
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
||||||
import { after } from "@vendetta/patcher";
|
|
||||||
import { Forms } from "@vendetta/ui/components";
|
|
||||||
import { findInReactTree } from "@vendetta/utils";
|
|
||||||
import { findByProps } from "@vendetta/metro";
|
|
||||||
import { logger } from "@vendetta";
|
|
||||||
import AccountSwitcherSettings from "./Settings";
|
|
||||||
|
|
||||||
const { FormSection, FormRow } = Forms;
|
|
||||||
const { TableRowIcon } = findByProps("TableRowIcon");
|
|
||||||
|
|
||||||
const bunny = window.bunny;
|
|
||||||
|
|
||||||
const tabsNavigationRef = bunny?.metro?.findByPropsLazy("getRootNavigationRef");
|
|
||||||
const settingConstants = bunny?.metro?.findByPropsLazy(
|
|
||||||
"SETTING_RENDERER_CONFIG",
|
|
||||||
);
|
|
||||||
const SettingsOverviewScreen = bunny?.metro?.findByNameLazy(
|
|
||||||
"SettingsOverviewScreen",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
const createListModule = bunny.metro.findByPropsLazy("createList");
|
|
||||||
|
|
||||||
function Section({ tabs }) {
|
|
||||||
const navigation = NavigationNative.useNavigation();
|
|
||||||
|
|
||||||
return React.createElement(FormRow, {
|
|
||||||
label: tabs.title(),
|
|
||||||
leading: React.createElement(FormRow.Icon, { source: tabs.icon }),
|
|
||||||
trailing: React.createElement(React.Fragment, {}, [
|
|
||||||
tabs.trailing ? tabs.trailing() : null,
|
|
||||||
React.createElement(FormRow.Arrow, { key: "arrow" }),
|
|
||||||
]),
|
|
||||||
onPress: () => {
|
|
||||||
const Component = tabs.page;
|
|
||||||
navigation.navigate("VendettaCustomPage", {
|
|
||||||
title: tabs.title(),
|
|
||||||
render: () => React.createElement(Component),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchPanelUI(tabs, patches) {
|
|
||||||
try {
|
|
||||||
patches.push(
|
|
||||||
after(
|
|
||||||
"default",
|
|
||||||
bunny?.metro?.findByNameLazy(
|
|
||||||
"UserSettingsOverviewWrapper",
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
(_, ret) => {
|
|
||||||
const UserSettingsOverview = findInReactTree(
|
|
||||||
ret.props.children,
|
|
||||||
(n) => n.type?.name === "UserSettingsOverview",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (UserSettingsOverview) {
|
|
||||||
patches.push(
|
|
||||||
after(
|
|
||||||
"render",
|
|
||||||
UserSettingsOverview.type.prototype,
|
|
||||||
(_args, res) => {
|
|
||||||
const sections = findInReactTree(
|
|
||||||
res.props.children,
|
|
||||||
(n) =>
|
|
||||||
n?.children?.[1]?.type ===
|
|
||||||
FormSection,
|
|
||||||
)?.children;
|
|
||||||
|
|
||||||
if (sections) {
|
|
||||||
const index = sections.findIndex((c) =>
|
|
||||||
[
|
|
||||||
"BILLING_SETTINGS",
|
|
||||||
"PREMIUM_SETTINGS",
|
|
||||||
].includes(c?.props?.label),
|
|
||||||
);
|
|
||||||
|
|
||||||
sections.splice(
|
|
||||||
-~index || 4,
|
|
||||||
0,
|
|
||||||
React.createElement(Section, {
|
|
||||||
key: tabs.key,
|
|
||||||
tabs,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.info("Panel UI patch failed graciously 💔", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchTabsUI(tabs, patches) {
|
|
||||||
if (!settingConstants || !SettingsOverviewScreen || !tabsNavigationRef) {
|
|
||||||
console.warn(
|
|
||||||
"[AccountSwitcher] Missing required constants for tabs UI patch",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = {};
|
|
||||||
row[tabs.key] = {
|
|
||||||
type: "pressable",
|
|
||||||
title: tabs.title,
|
|
||||||
useTitle: tabs.title,
|
|
||||||
icon: tabs.icon,
|
|
||||||
IconComponent:
|
|
||||||
tabs.icon &&
|
|
||||||
(() => {
|
|
||||||
const actualIconSource =
|
|
||||||
typeof tabs.icon === "object" && tabs.icon.uri !== undefined
|
|
||||||
? tabs.icon.uri
|
|
||||||
: tabs.icon;
|
|
||||||
return React.createElement(TableRowIcon, {
|
|
||||||
source: actualIconSource,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
usePredicate: tabs.predicate,
|
|
||||||
useTrailing: tabs.trailing,
|
|
||||||
onPress: () => {
|
|
||||||
const navigation = tabsNavigationRef.getRootNavigationRef();
|
|
||||||
const Component = tabs.page;
|
|
||||||
|
|
||||||
navigation.navigate("VendettaCustomPage", {
|
|
||||||
title: tabs.title(),
|
|
||||||
render: () => React.createElement(Component),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
withArrow: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
|
|
||||||
|
|
||||||
Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", {
|
|
||||||
enumerable: true,
|
|
||||||
configurable: true,
|
|
||||||
get: () => ({
|
|
||||||
...rendererConfigValue,
|
|
||||||
...row,
|
|
||||||
}),
|
|
||||||
set: (v) => (rendererConfigValue = v),
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstRender = Symbol("AccountSwitcher-pinToSettings");
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!createListModule) return;
|
|
||||||
|
|
||||||
patches.push(
|
|
||||||
after("createList", createListModule, (args, ret) => {
|
|
||||||
if (!args[0][firstRender]) {
|
|
||||||
args[0][firstRender] = true;
|
|
||||||
|
|
||||||
const [config] = args;
|
|
||||||
const sections = config.sections;
|
|
||||||
|
|
||||||
const section = sections?.find((x) =>
|
|
||||||
["Bunny", "Kettu", "Revenge"].includes(x?.label),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If unable to find a section
|
|
||||||
if (!section) {
|
|
||||||
const isMainSettings = Boolean(
|
|
||||||
sections?.find((x) =>
|
|
||||||
x.settings.includes("ACCOUNT"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMainSettings) {
|
|
||||||
// Add a new section to the top
|
|
||||||
sections.unshift({
|
|
||||||
label: manifest.name,
|
|
||||||
title: manifest.name,
|
|
||||||
settings: [tabs.key],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (section?.settings) {
|
|
||||||
section.settings = [...section.settings, tabs.key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
patches.push(
|
|
||||||
after("default", SettingsOverviewScreen, (args, ret) => {
|
|
||||||
if (!args[0][firstRender]) {
|
|
||||||
args[0][firstRender] = true;
|
|
||||||
|
|
||||||
const { sections } = findInReactTree(
|
|
||||||
ret,
|
|
||||||
(i) => i.props?.sections,
|
|
||||||
).props;
|
|
||||||
|
|
||||||
const section = sections?.find((x) =>
|
|
||||||
["Bunny", "Kettu", "Revenge"].includes(x?.label),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If unable to find a section
|
|
||||||
if (!section) {
|
|
||||||
const isMainSettings = Boolean(
|
|
||||||
sections?.find((x) =>
|
|
||||||
x.settings.includes("ACCOUNT"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isMainSettings) {
|
|
||||||
// Add a new section to the top
|
|
||||||
sections.unshift({
|
|
||||||
label: manifest.name,
|
|
||||||
title: manifest.name,
|
|
||||||
settings: [tabs.key],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (section?.settings) {
|
|
||||||
section.settings = [...section.settings, tabs.key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchSettingsPin(tabs) {
|
|
||||||
const patches = [];
|
|
||||||
|
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
const realPredicate = tabs.predicate || (() => true);
|
|
||||||
tabs.predicate = () => (disabled ? false : realPredicate());
|
|
||||||
|
|
||||||
patchPanelUI(tabs, patches);
|
|
||||||
patchTabsUI(tabs, patches);
|
|
||||||
patches.push(() => (disabled = true));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const x of patches) {
|
|
||||||
x();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function patchSidebar() {
|
|
||||||
if (!storage.settings?.addToSidebar) {
|
|
||||||
console.log("[AccountSwitcher] Sidebar disabled in settings");
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[AccountSwitcher] Patching sidebar using custom patchSettingsPin...",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const unpatch = patchSettingsPin({
|
|
||||||
key: "AccountSwitcher",
|
|
||||||
icon: getAssetIDByName("UserIcon"),
|
|
||||||
title: () => "Account Switcher",
|
|
||||||
predicate: () => storage.settings?.addToSidebar === true,
|
|
||||||
page: AccountSwitcherSettings,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[AccountSwitcher] Successfully patched sidebar");
|
|
||||||
return unpatch;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AccountSwitcher] Failed to patch sidebar:", error);
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,623 +0,0 @@
|
||||||
import * as common from "../../common";
|
|
||||||
import { semanticColors } from "@vendetta/ui";
|
|
||||||
import { registerCommand } from "@vendetta/commands";
|
|
||||||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
|
||||||
import { findInReactTree } from "@vendetta/utils";
|
|
||||||
import { setString } from "@vendetta/metro/common/clipboard";
|
|
||||||
import { before as patchBefore } from "@vendetta/patcher";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { encode as encodeTok, characters2 } from "../../common/numberBase64";
|
|
||||||
import { storage } from "@vendetta/plugin";
|
|
||||||
import { React } from "@vendetta/metro/common";
|
|
||||||
import AccountSwitcherSettings from "./Settings";
|
|
||||||
import patchSidebar from "./SidebarPatcher";
|
|
||||||
|
|
||||||
const {
|
|
||||||
meta: { resolveSemanticColor },
|
|
||||||
} = findByProps("colors", "meta");
|
|
||||||
const ThemeStore = findByStoreName("ThemeStore");
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
|
|
||||||
export const EMBED_COLOR = () =>
|
|
||||||
parseInt(resolveSemanticColor(ThemeStore.theme, semanticColors.BACKGROUND_SECONDARY).slice(1), 16);
|
|
||||||
|
|
||||||
const authorMods = {
|
|
||||||
author: {
|
|
||||||
username: "AccountSwitcher",
|
|
||||||
avatar: "command",
|
|
||||||
avatarURL: common.AVATARS?.command || "https://cdn.discordapp.com/embed/avatars/0.png",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize storage structure
|
|
||||||
if (!storage.accounts) storage.accounts = {};
|
|
||||||
if (!storage.accountOrder) storage.accountOrder = [];
|
|
||||||
if (!storage.settings) {
|
|
||||||
storage.settings = {
|
|
||||||
enableCLI: true,
|
|
||||||
confirmBeforeDelete: true,
|
|
||||||
enableUnsafeFeatures: false,
|
|
||||||
addToSidebar: true // Now using the correct patchSettingsPin method
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Ensure all properties exist and migrate old settings
|
|
||||||
if (storage.settings.enableUnsafeFeatures === undefined) {
|
|
||||||
storage.settings.enableUnsafeFeatures = false;
|
|
||||||
}
|
|
||||||
if (storage.settings.enableCLI === undefined) {
|
|
||||||
storage.settings.enableCLI = storage.settings.showAccountNames !== false;
|
|
||||||
}
|
|
||||||
if (storage.settings.confirmBeforeDelete === undefined) {
|
|
||||||
storage.settings.confirmBeforeDelete = true;
|
|
||||||
}
|
|
||||||
if (storage.settings.addToSidebar === undefined) {
|
|
||||||
storage.settings.addToSidebar = true; // Now using correct method
|
|
||||||
}
|
|
||||||
// Remove old setting
|
|
||||||
if (storage.settings.showAccountNames !== undefined) {
|
|
||||||
delete storage.settings.showAccountNames;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let madeSendMessage;
|
|
||||||
function sendMessage() {
|
|
||||||
if (window.sendMessage) return window.sendMessage?.(...arguments);
|
|
||||||
if (!madeSendMessage) madeSendMessage = common.mSendMessage?.(vendetta);
|
|
||||||
return madeSendMessage?.(...arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
function formatAccountName(account) {
|
|
||||||
if (!account || !account.username) return "Unknown Account";
|
|
||||||
return account.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAccountByInput(input) {
|
|
||||||
const accounts = storage.accounts || {};
|
|
||||||
const accountOrder = storage.accountOrder || [];
|
|
||||||
|
|
||||||
const index = parseInt(input);
|
|
||||||
if (!isNaN(index) && index >= 1 && index <= accountOrder.length) {
|
|
||||||
const accountId = accountOrder[index - 1];
|
|
||||||
return accounts[accountId] ? [accountId, accounts[accountId]] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(accounts).find(([id, account]) =>
|
|
||||||
account.username.toLowerCase() === input.toLowerCase() ||
|
|
||||||
formatAccountName(account).toLowerCase() === input.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAccountIndex(accountId) {
|
|
||||||
const accountOrder = storage.accountOrder || [];
|
|
||||||
const index = accountOrder.indexOf(accountId);
|
|
||||||
return index >= 0 ? index + 1 : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAccountToOrder(accountId) {
|
|
||||||
if (!storage.accountOrder) storage.accountOrder = [];
|
|
||||||
if (!storage.accountOrder.includes(accountId)) {
|
|
||||||
storage.accountOrder.push(accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAccountFromOrder(accountId) {
|
|
||||||
if (!storage.accountOrder) storage.accountOrder = [];
|
|
||||||
const index = storage.accountOrder.indexOf(accountId);
|
|
||||||
if (index > -1) {
|
|
||||||
storage.accountOrder.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
meta: vendetta.plugin,
|
|
||||||
settings: AccountSwitcherSettings,
|
|
||||||
patches: [],
|
|
||||||
|
|
||||||
onUnload() {
|
|
||||||
this.patches.forEach((up) => up());
|
|
||||||
this.patches = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoad() {
|
|
||||||
// Add sidebar patcher
|
|
||||||
const sidebarUnpatch = patchSidebar();
|
|
||||||
this.patches.push(sidebarUnpatch);
|
|
||||||
|
|
||||||
// Context menu patch for copying tokens
|
|
||||||
const optionLabel = "Copy Token";
|
|
||||||
const contextMenuUnpatch = patchBefore("render", findByProps("ScrollView").View, (args) => {
|
|
||||||
try {
|
|
||||||
if (!storage.settings?.enableUnsafeFeatures) return;
|
|
||||||
|
|
||||||
let a = findInReactTree(args, (r) => r.key === ".$UserProfileOverflow");
|
|
||||||
if (!a || !a.props || a.props.sheetKey !== "UserProfileOverflow") return;
|
|
||||||
|
|
||||||
const props = a.props.content.props;
|
|
||||||
if (props.options.some((option) => option?.label === optionLabel)) return;
|
|
||||||
|
|
||||||
const currentUserId = UserStore.getCurrentUser()?.id;
|
|
||||||
const focusedUserId = Object.keys(a._owner.stateNode._keyChildMapping)
|
|
||||||
.find((str) => a._owner.stateNode._keyChildMapping[str] && str.match(/(?<=\$UserProfile)\d+/))
|
|
||||||
?.slice?.(".$UserProfile".length) || currentUserId;
|
|
||||||
const token = findByProps("getToken").getToken();
|
|
||||||
|
|
||||||
props.options.unshift({
|
|
||||||
isDestructive: true,
|
|
||||||
label: optionLabel,
|
|
||||||
onPress: () => {
|
|
||||||
try {
|
|
||||||
showToast(focusedUserId === currentUserId ? `Copied your token` : `Copied token of ${props.header.title}`);
|
|
||||||
setString(
|
|
||||||
focusedUserId === currentUserId
|
|
||||||
? token
|
|
||||||
: [
|
|
||||||
Buffer.from(focusedUserId).toString("base64").replaceAll("=",""),
|
|
||||||
encodeTok?.(+Date.now() - 1293840000, true) || "dummy",
|
|
||||||
common.generateRandomString?.(characters2, 27) || "dummy",
|
|
||||||
].join(".")
|
|
||||||
);
|
|
||||||
props.hideActionSheet();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showToast("Failed to copy token", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Context menu patch error:", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.patches.push(contextMenuUnpatch);
|
|
||||||
|
|
||||||
// Command implementations
|
|
||||||
const commands = {
|
|
||||||
async mainCommand(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "AccSwitcher main message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: "📱 Account Switcher",
|
|
||||||
description: storage.settings.addToSidebar
|
|
||||||
? "Account Manager is available in your settings sidebar!\n\nQuick commands:\n• `/accswitcher login <#>` - Switch to account\n• `/accswitcher add` - Add current account\n• `/accswitcher list` - Show all accounts"
|
|
||||||
: "Opening Account Manager...\n\nQuick commands:\n• `/accswitcher login <#>` - Switch to account\n• `/accswitcher add` - Add current account\n• `/accswitcher list` - Show all accounts",
|
|
||||||
footer: { text: storage.settings.addToSidebar ? "Check your settings sidebar for Account Switcher" : "Check plugin settings for the Account Manager interface" }
|
|
||||||
}],
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
showToast(storage.settings.addToSidebar ? "Check settings sidebar for Account Switcher" : "Check plugin settings to access Account Manager", 0);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Main command error:", e);
|
|
||||||
showToast("Check plugin settings to access Account Manager", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async addAccount(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher add",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const token = findByProps("getToken").getToken();
|
|
||||||
const currentUser = UserStore.getCurrentUser();
|
|
||||||
|
|
||||||
if (!currentUser) {
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Add account error message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ Failed to Get Current User`,
|
|
||||||
description: "Could not retrieve current user information."
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.accounts[currentUser.id]) {
|
|
||||||
const index = getAccountIndex(currentUser.id);
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Add account warning message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `⚠️ Account Already Saved`,
|
|
||||||
description: `Your current account **${index}. ${currentUser.username}** is already saved.`
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccount = {
|
|
||||||
id: currentUser.id,
|
|
||||||
username: currentUser.username,
|
|
||||||
discriminator: currentUser.discriminator,
|
|
||||||
avatar: currentUser.avatar,
|
|
||||||
displayName: currentUser.globalName || currentUser.username,
|
|
||||||
token: token,
|
|
||||||
addedAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
storage.accounts[currentUser.id] = newAccount;
|
|
||||||
addAccountToOrder(currentUser.id);
|
|
||||||
|
|
||||||
const index = getAccountIndex(currentUser.id);
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Add account success message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `✅ Account Added Successfully`,
|
|
||||||
description: `**${index}. ${currentUser.username}** has been saved.`,
|
|
||||||
footer: { text: `Total accounts: ${Object.keys(storage.accounts).length}` }
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Add account error:", e);
|
|
||||||
showToast("Failed to add account", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async login(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher login",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = new Map(args.map((a) => [a.name, a]));
|
|
||||||
const accountInput = options.get("account")?.value;
|
|
||||||
|
|
||||||
if (!accountInput) {
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Login error message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ No Account Specified`,
|
|
||||||
description: "Please specify an account number, username, or ID."
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findAccountByInput(accountInput);
|
|
||||||
if (!found) {
|
|
||||||
const availableAccounts = (storage.accountOrder || [])
|
|
||||||
.filter(id => storage.accounts[id])
|
|
||||||
.map((id, index) => `${index + 1}. ${formatAccountName(storage.accounts[id])}`)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Login error message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ Account Not Found`,
|
|
||||||
description: `Could not find account "${accountInput}".${availableAccounts ? `\n\n**Available accounts:**\n${availableAccounts}` : '\n\nNo accounts saved yet. Use `/accswitcher add` first.'}`
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [accountId, account] = found;
|
|
||||||
const index = getAccountIndex(accountId);
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Login process message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `🔄 Switching to ${index}. ${formatAccountName(account)}...`
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token);
|
|
||||||
showToast(`Switched to ${index}. ${formatAccountName(account)}!`, 0);
|
|
||||||
} catch (switchError) {
|
|
||||||
console.error("Switch error:", switchError);
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Login failure message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ Failed to switch accounts`,
|
|
||||||
description: `Error: ${switchError.message}`
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Login command error:", e);
|
|
||||||
showToast("Failed to execute login command", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
listAccounts(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher list",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const accounts = storage.accounts || {};
|
|
||||||
const accountCount = Object.keys(accounts).length;
|
|
||||||
|
|
||||||
if (accountCount === 0) {
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "List accounts empty message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: "📝 No Saved Accounts",
|
|
||||||
description: "Use `/accswitcher add` to save your current account."
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountList = (storage.accountOrder || [])
|
|
||||||
.filter(id => accounts[id])
|
|
||||||
.map((id, index) => {
|
|
||||||
const account = accounts[id];
|
|
||||||
const name = formatAccountName(account);
|
|
||||||
const addedDate = new Date(account.addedAt).toLocaleDateString();
|
|
||||||
return `**${index + 1}.** ${name} - Added: ${addedDate}`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "List accounts message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `📝 Saved Accounts (${accountCount})`,
|
|
||||||
description: accountList,
|
|
||||||
footer: { text: "Use /accswitcher login <number or username> to switch accounts" }
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("List accounts error:", e);
|
|
||||||
showToast("Failed to list accounts", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAccount(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher remove",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = new Map(args.map((a) => [a.name, a]));
|
|
||||||
const accountInput = options.get("account")?.value;
|
|
||||||
|
|
||||||
if (!accountInput) {
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Remove account error message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ No Account Specified`,
|
|
||||||
description: "Please specify an account number, username, or ID to remove."
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findAccountByInput(accountInput);
|
|
||||||
if (!found) {
|
|
||||||
const availableAccounts = (storage.accountOrder || [])
|
|
||||||
.filter(id => storage.accounts[id])
|
|
||||||
.map((id, index) => `${index + 1}. ${formatAccountName(storage.accounts[id])}`)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Remove account error message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `❌ Account Not Found`,
|
|
||||||
description: `Could not find account "${accountInput}".${availableAccounts ? `\n\n**Available accounts:**\n${availableAccounts}` : '\n\nNo accounts saved yet.'}`
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [accountId, account] = found;
|
|
||||||
const index = getAccountIndex(accountId);
|
|
||||||
const accountName = `${index}. ${formatAccountName(account)}`;
|
|
||||||
|
|
||||||
delete storage.accounts[accountId];
|
|
||||||
removeAccountFromOrder(accountId);
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Remove account success message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: `🗑️ Account Removed`,
|
|
||||||
description: `**${accountName}** has been removed from saved accounts.`,
|
|
||||||
footer: { text: `Remaining accounts: ${Object.keys(storage.accounts).length}` }
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Remove account error:", e);
|
|
||||||
showToast("Failed to remove account", 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentToken(args, ctx) {
|
|
||||||
try {
|
|
||||||
const messageMods = {
|
|
||||||
...authorMods,
|
|
||||||
interaction: {
|
|
||||||
name: "/accswitcher token",
|
|
||||||
user: UserStore.getCurrentUser(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!storage.settings?.enableUnsafeFeatures) {
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Get token disabled message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: "🔒 Unsafe Features Disabled",
|
|
||||||
description: "Token access is disabled. Enable 'Unsafe Features' in plugin settings to use this command."
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getToken } = findByProps("getToken");
|
|
||||||
const token = getToken();
|
|
||||||
|
|
||||||
sendMessage?.({
|
|
||||||
loggingName: "Get token message",
|
|
||||||
channelId: ctx.channel.id,
|
|
||||||
embeds: [{
|
|
||||||
color: EMBED_COLOR(),
|
|
||||||
type: "rich",
|
|
||||||
title: "🔑 Current Account Token",
|
|
||||||
description: `\`${token}\``,
|
|
||||||
footer: { text: "⚠️ Keep your token secure and never share it!" }
|
|
||||||
}]
|
|
||||||
}, messageMods);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Get token error:", e);
|
|
||||||
showToast("Failed to get current token", 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register commands
|
|
||||||
try {
|
|
||||||
const commandsToRegister = [
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.mainCommand,
|
|
||||||
name: "accswitcher",
|
|
||||||
description: "Open Account Switcher (check plugin settings)",
|
|
||||||
options: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.addAccount,
|
|
||||||
name: "accswitcher add",
|
|
||||||
description: "Add your current account to saved accounts",
|
|
||||||
options: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.login,
|
|
||||||
name: "accswitcher login",
|
|
||||||
description: "Switch to a saved account",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
type: 3,
|
|
||||||
name: "account",
|
|
||||||
description: "Account number, username, or ID to switch to",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.listAccounts,
|
|
||||||
name: "accswitcher list",
|
|
||||||
description: "Show all saved accounts",
|
|
||||||
options: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.removeAccount,
|
|
||||||
name: "accswitcher remove",
|
|
||||||
description: "Remove a saved account",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
type: 3,
|
|
||||||
name: "account",
|
|
||||||
description: "Account number, username, or ID to remove",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
inputType: 1,
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: commands.getCurrentToken,
|
|
||||||
name: "accswitcher token",
|
|
||||||
description: "Get your current account token (requires unsafe features)",
|
|
||||||
options: [],
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
commandsToRegister.forEach(command => {
|
|
||||||
try {
|
|
||||||
registerCommand(command);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to register command ${command.name}:`, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Account Switcher plugin loaded successfully");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to register commands:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "More Alts!",
|
|
||||||
"description": "Adds ability to safely add more alts to Discord!",
|
|
||||||
"authors": [{ "name": "Win8.1VMUser", "id": "793935599702507542" }],
|
|
||||||
"main": "./index.js",
|
|
||||||
"vendetta": { "icon": "ic_full_server_gating_24px" }
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Read All
|
|
||||||
## Read all server and DM notifications with single command - /read.
|
|
||||||
|
|
||||||
Images:
|
|
||||||
<img width="1220" height="451" alt="Screenshot_20250725-093823 Discord" src="https://github.com/user-attachments/assets/71ad386f-0677-4998-be18-2d4ae3e2c815" />
|
|
||||||
<img width="1220" height="790" alt="Screenshot_20250725-093814 Discord" src="https://github.com/user-attachments/assets/ac8aa4db-1ccb-415a-a3a1-6b106a61d7df" />
|
|
||||||
<img width="1220" height="354" alt="Screenshot_20250725-093847 Discord" src="https://github.com/user-attachments/assets/8f128874-6069-4ec8-bd14-efdaeec1bf73" />
|
|
||||||
<img width="1220" height="355" alt="Screenshot_20250725-094021 Discord" src="https://github.com/user-attachments/assets/8ccff92a-78ff-429d-91cf-7626990666fc" />
|
|
||||||
|
|
||||||
|
|
||||||
🔗 [**Install**](https://apexteampl.github.io/Apex-Plugins/ReadAll)
|
|
||||||
👷♂️ [**Source code**](https://github.com/ApexTeamPL/Apex-Plugins/tree/master/plugins/ReadAll)
|
|
||||||
🪰 [**Bugs**](https://github.com/ApexTeamPL/Apex-Plugins/issues/new?title=[ReadAll])
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Read All",
|
|
||||||
"description": "Read all server and DM notifications with single command - /read",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Win8.1VMUser",
|
|
||||||
"id": "793935599702507542"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "btmc727",
|
|
||||||
"id": "554071670143451176"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"vendetta": {
|
|
||||||
"icon": "ic_edit_24px"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { storage } from "@vendetta/plugin";
|
|
||||||
import { findByStoreName } from "@vendetta/metro";
|
|
||||||
|
|
||||||
interface ExceptionSettings {
|
|
||||||
excludedServers: string[];
|
|
||||||
excludedDMs: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: ExceptionSettings = {
|
|
||||||
excludedServers: [],
|
|
||||||
excludedDMs: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSettings = (): ExceptionSettings => {
|
|
||||||
return { ...DEFAULT_SETTINGS, ...storage };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveSettings = (settings: ExceptionSettings): void => {
|
|
||||||
Object.assign(storage, settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addServerException = (serverId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
if (!settings.excludedServers.includes(serverId)) {
|
|
||||||
settings.excludedServers.push(serverId);
|
|
||||||
saveSettings(settings);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeServerException = (serverId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
const index = settings.excludedServers.indexOf(serverId);
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
settings.excludedServers.splice(index, 1);
|
|
||||||
saveSettings(settings);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addDMException = (channelId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
if (!settings.excludedDMs.includes(channelId)) {
|
|
||||||
settings.excludedDMs.push(channelId);
|
|
||||||
saveSettings(settings);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeDMException = (channelId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
const index = settings.excludedDMs.indexOf(channelId);
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
settings.excludedDMs.splice(index, 1);
|
|
||||||
saveSettings(settings);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isServerExcluded = (serverId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
return settings.excludedServers.includes(serverId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isDMExcluded = (channelId: string): boolean => {
|
|
||||||
const settings = getSettings();
|
|
||||||
return settings.excludedDMs.includes(channelId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerName = (serverId: string): string => {
|
|
||||||
try {
|
|
||||||
const GuildStore = findByStoreName("GuildStore");
|
|
||||||
const guild = GuildStore?.getGuild?.(serverId);
|
|
||||||
return guild?.name || `Unknown Server (${serverId})`;
|
|
||||||
} catch (e) {
|
|
||||||
return `Unknown Server (${serverId})`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDMName = (channelId: string): string => {
|
|
||||||
try {
|
|
||||||
const ChannelStore = findByStoreName("ChannelStore");
|
|
||||||
const channel = ChannelStore?.getChannel?.(channelId);
|
|
||||||
|
|
||||||
if (channel) {
|
|
||||||
if (channel.name) {
|
|
||||||
return channel.name;
|
|
||||||
} else if (channel.recipients && channel.recipients.length > 0) {
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
const user = UserStore?.getUser?.(channel.recipients[0]);
|
|
||||||
return user?.username ? `@${user.username}` : `Unknown User (${channelId})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Unknown DM (${channelId})`;
|
|
||||||
} catch (e) {
|
|
||||||
return `Unknown DM (${channelId})`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear all exceptions
|
|
||||||
export const clearAllExceptions = (): void => {
|
|
||||||
const settings = getSettings();
|
|
||||||
settings.excludedServers = [];
|
|
||||||
settings.excludedDMs = [];
|
|
||||||
saveSettings(settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllExceptions = (): { servers: Array<{id: string, name: string}>, dms: Array<{id: string, name: string}> } => {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
return {
|
|
||||||
servers: settings.excludedServers.map(id => ({
|
|
||||||
id,
|
|
||||||
name: getServerName(id)
|
|
||||||
})),
|
|
||||||
dms: settings.excludedDMs.map(id => ({
|
|
||||||
id,
|
|
||||||
name: getDMName(id)
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
import { findByStoreName, findByProps } from "@vendetta/metro";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
|
||||||
import { registerCommand } from "@vendetta/commands";
|
|
||||||
import {
|
|
||||||
isServerExcluded,
|
|
||||||
isDMExcluded,
|
|
||||||
addServerException,
|
|
||||||
removeServerException,
|
|
||||||
addDMException,
|
|
||||||
removeDMException,
|
|
||||||
clearAllExceptions,
|
|
||||||
getAllExceptions,
|
|
||||||
getServerName,
|
|
||||||
getDMName
|
|
||||||
} from "./Settings";
|
|
||||||
import { React } from "@vendetta/metro/common";
|
|
||||||
import { Forms } from "@vendetta/ui/components";
|
|
||||||
|
|
||||||
let GuildStore: any;
|
|
||||||
let GuildChannelStore: any;
|
|
||||||
let ActiveJoinedThreadsStore: any;
|
|
||||||
let ReadStateStore: any;
|
|
||||||
let FluxDispatcher: any;
|
|
||||||
let ChannelStore: any;
|
|
||||||
|
|
||||||
let readCommandUnregister: (() => void) | null = null;
|
|
||||||
let readAllCommandUnregister: (() => void) | null = null;
|
|
||||||
let readServerCommandUnregister: (() => void) | null = null;
|
|
||||||
let readDMCommandUnregister: (() => void) | null = null;
|
|
||||||
let lastUsed = 0;
|
|
||||||
const COOLDOWN_MS = 60000;
|
|
||||||
|
|
||||||
const findModule = (patterns: string[], storeName?: string) => {
|
|
||||||
if (storeName) {
|
|
||||||
try {
|
|
||||||
const store = findByStoreName(storeName);
|
|
||||||
if (store) return store;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
try {
|
|
||||||
const module = findByProps(pattern);
|
|
||||||
if (module) return module;
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initModules = () => {
|
|
||||||
GuildStore = findByStoreName("GuildStore");
|
|
||||||
GuildChannelStore = findByStoreName("GuildChannelStore") || findByStoreName("ChannelStore");
|
|
||||||
ChannelStore = findByStoreName("ChannelStore");
|
|
||||||
ReadStateStore = findByStoreName("ReadStateStore");
|
|
||||||
ActiveJoinedThreadsStore = findByStoreName("ActiveJoinedThreadsStore") || findByProps("getActiveJoinedThreadsForGuild");
|
|
||||||
FluxDispatcher = findByProps("dispatch", "subscribe") || findByStoreName("Dispatcher");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDMChannels = () => {
|
|
||||||
const dmChannels: any[] = [];
|
|
||||||
const channelStore = ChannelStore || GuildChannelStore;
|
|
||||||
|
|
||||||
if (!channelStore) return dmChannels;
|
|
||||||
|
|
||||||
if (channelStore.getPrivateChannels) {
|
|
||||||
try {
|
|
||||||
const privateChannels = channelStore.getPrivateChannels();
|
|
||||||
if (privateChannels && typeof privateChannels === 'object') {
|
|
||||||
Object.values(privateChannels).forEach((channel: any) => {
|
|
||||||
if (channel && channel.id) dmChannels.push(channel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dmChannels.length === 0 && channelStore.getSortedPrivateChannels) {
|
|
||||||
try {
|
|
||||||
const sortedPrivateChannels = channelStore.getSortedPrivateChannels();
|
|
||||||
if (Array.isArray(sortedPrivateChannels)) {
|
|
||||||
sortedPrivateChannels.forEach((channel: any) => {
|
|
||||||
if (channel && channel.id) dmChannels.push(channel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dmChannels.length === 0 && channelStore.getChannels) {
|
|
||||||
try {
|
|
||||||
const meChannels = channelStore.getChannels("@me");
|
|
||||||
if (meChannels && meChannels.SELECTABLE) {
|
|
||||||
meChannels.SELECTABLE.forEach((c: any) => {
|
|
||||||
const channel = c.channel || c;
|
|
||||||
if (channel && channel.id) dmChannels.push(channel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dmChannels.length === 0 && channelStore.getChannel && ReadStateStore?.getAllReadStates) {
|
|
||||||
try {
|
|
||||||
const allReadStates = ReadStateStore.getAllReadStates();
|
|
||||||
Object.keys(allReadStates).forEach(channelId => {
|
|
||||||
try {
|
|
||||||
const channel = channelStore.getChannel(channelId);
|
|
||||||
if (channel) {
|
|
||||||
const isDM = channel.type === 1 || channel.type === 3 || (!channel.guild_id && !channel.guildId);
|
|
||||||
if (isDM) dmChannels.push(channel);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dmChannels;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerChannels = () => {
|
|
||||||
if (!GuildStore || !ReadStateStore) return [];
|
|
||||||
|
|
||||||
const channels: Array<any> = [];
|
|
||||||
const guilds = GuildStore.getGuilds();
|
|
||||||
|
|
||||||
Object.values(guilds).forEach((guild: any) => {
|
|
||||||
if (!guild?.id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let guildChannels = [];
|
|
||||||
const channelStore = GuildChannelStore || ChannelStore;
|
|
||||||
|
|
||||||
if (channelStore?.getChannels) {
|
|
||||||
const channelData = channelStore.getChannels(guild.id);
|
|
||||||
if (channelData?.SELECTABLE) guildChannels = guildChannels.concat(channelData.SELECTABLE);
|
|
||||||
if (channelData?.VOCAL) guildChannels = guildChannels.concat(channelData.VOCAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ActiveJoinedThreadsStore?.getActiveJoinedThreadsForGuild) {
|
|
||||||
try {
|
|
||||||
const threads = ActiveJoinedThreadsStore.getActiveJoinedThreadsForGuild(guild.id);
|
|
||||||
const threadChannels = Object.values(threads).flatMap((threadGroup: any) => Object.values(threadGroup || {}));
|
|
||||||
guildChannels = guildChannels.concat(threadChannels);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
guildChannels.forEach((c: any) => {
|
|
||||||
const channel = c?.channel || c;
|
|
||||||
if (!channel?.id) return;
|
|
||||||
|
|
||||||
// Skip if server is in exceptions
|
|
||||||
if (isServerExcluded(guild.id)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (ReadStateStore.hasUnread && ReadStateStore.hasUnread(channel.id)) {
|
|
||||||
channels.push({
|
|
||||||
channelId: channel.id,
|
|
||||||
messageId: ReadStateStore.lastMessageId?.(channel.id) || null,
|
|
||||||
readStateType: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDMUnreadChannels = () => {
|
|
||||||
const channels: Array<any> = [];
|
|
||||||
const dmChannels = getDMChannels();
|
|
||||||
|
|
||||||
dmChannels.forEach((channel: any) => {
|
|
||||||
if (!channel?.id) return;
|
|
||||||
|
|
||||||
// Skip if DM is in exceptions
|
|
||||||
if (isDMExcluded(channel.id)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let hasUnread = false;
|
|
||||||
|
|
||||||
if (ReadStateStore.hasUnread) {
|
|
||||||
hasUnread = ReadStateStore.hasUnread(channel.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUnread && ReadStateStore.getAllReadStates) {
|
|
||||||
const allReadStates = ReadStateStore.getAllReadStates();
|
|
||||||
const readState = allReadStates[channel.id];
|
|
||||||
if (readState) {
|
|
||||||
hasUnread = (readState.mentionCount && readState.mentionCount > 0) ||
|
|
||||||
(readState._unreadCount && readState._unreadCount > 0) ||
|
|
||||||
(readState.unreadCount && readState.unreadCount > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasUnread) {
|
|
||||||
channels.push({
|
|
||||||
channelId: channel.id,
|
|
||||||
messageId: ReadStateStore.lastMessageId?.(channel.id) || null,
|
|
||||||
readStateType: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
return channels;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bulkAckNotifications = (type: 'all' | 'server' | 'dm' = 'all') => {
|
|
||||||
if (!GuildStore || !ReadStateStore || !FluxDispatcher) return false;
|
|
||||||
|
|
||||||
let channels: Array<any> = [];
|
|
||||||
let typeLabel = '';
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'server':
|
|
||||||
channels = getServerChannels();
|
|
||||||
typeLabel = 'server';
|
|
||||||
break;
|
|
||||||
case 'dm':
|
|
||||||
channels = getDMUnreadChannels();
|
|
||||||
typeLabel = 'DM';
|
|
||||||
break;
|
|
||||||
case 'all':
|
|
||||||
default:
|
|
||||||
channels = [...getServerChannels(), ...getDMUnreadChannels()];
|
|
||||||
typeLabel = '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channels.length === 0) {
|
|
||||||
const message = type === 'all'
|
|
||||||
? "No unread notifications found!"
|
|
||||||
: `No unread ${typeLabel} notifications found!`;
|
|
||||||
showToast(message, getAssetIDByName("ic_message_edit"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
|
||||||
type: "BULK_ACK",
|
|
||||||
context: "APP",
|
|
||||||
channels: channels
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = type === 'all'
|
|
||||||
? `Cleared ${channels.length} unread notifications!`
|
|
||||||
: `Cleared ${channels.length} unread ${typeLabel} notifications!`;
|
|
||||||
|
|
||||||
showToast(message, getAssetIDByName("ic_message_edit"));
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readMainNotifications = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastUse = now - lastUsed;
|
|
||||||
|
|
||||||
if (timeSinceLastUse < COOLDOWN_MS) {
|
|
||||||
const remainingSeconds = Math.ceil((COOLDOWN_MS - timeSinceLastUse) / 1000);
|
|
||||||
showToast(`Please wait ${remainingSeconds}s before using again`, getAssetIDByName("ic_close_16px"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUsed = now;
|
|
||||||
bulkAckNotifications('all');
|
|
||||||
};
|
|
||||||
|
|
||||||
const readAllNotifications = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastUse = now - lastUsed;
|
|
||||||
|
|
||||||
if (timeSinceLastUse < COOLDOWN_MS) {
|
|
||||||
const remainingSeconds = Math.ceil((COOLDOWN_MS - timeSinceLastUse) / 1000);
|
|
||||||
showToast(`Please wait ${remainingSeconds}s before using again`, getAssetIDByName("ic_close_16px"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUsed = now;
|
|
||||||
bulkAckNotifications('server');
|
|
||||||
};
|
|
||||||
|
|
||||||
const readServerNotifications = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastUse = now - lastUsed;
|
|
||||||
|
|
||||||
if (timeSinceLastUse < COOLDOWN_MS) {
|
|
||||||
const remainingSeconds = Math.ceil((COOLDOWN_MS - timeSinceLastUse) / 1000);
|
|
||||||
showToast(`Please wait ${remainingSeconds}s before using again`, getAssetIDByName("ic_close_16px"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUsed = now;
|
|
||||||
bulkAckNotifications('server');
|
|
||||||
};
|
|
||||||
|
|
||||||
const readDMNotifications = () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastUse = now - lastUsed;
|
|
||||||
|
|
||||||
if (timeSinceLastUse < COOLDOWN_MS) {
|
|
||||||
const remainingSeconds = Math.ceil((COOLDOWN_MS - timeSinceLastUse) / 1000);
|
|
||||||
showToast(`Please wait ${remainingSeconds}s before using again`, getAssetIDByName("ic_close_16px"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUsed = now;
|
|
||||||
bulkAckNotifications('dm');
|
|
||||||
};
|
|
||||||
|
|
||||||
const SettingsComponent = () => {
|
|
||||||
const [serverInput, setServerInput] = React.useState("");
|
|
||||||
const [dmInput, setDMInput] = React.useState("");
|
|
||||||
const [exceptions, setExceptions] = React.useState(getAllExceptions());
|
|
||||||
|
|
||||||
const refreshExceptions = () => {
|
|
||||||
setExceptions(getAllExceptions());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddServer = () => {
|
|
||||||
if (serverInput.trim()) {
|
|
||||||
const success = addServerException(serverInput.trim());
|
|
||||||
if (success) {
|
|
||||||
showToast(`Added server to exceptions`, getAssetIDByName("ic_check"));
|
|
||||||
setServerInput("");
|
|
||||||
refreshExceptions();
|
|
||||||
} else {
|
|
||||||
showToast("Server already in exceptions", getAssetIDByName("ic_close_16px"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddDM = () => {
|
|
||||||
if (dmInput.trim()) {
|
|
||||||
const success = addDMException(dmInput.trim());
|
|
||||||
if (success) {
|
|
||||||
showToast(`Added DM to exceptions`, getAssetIDByName("ic_check"));
|
|
||||||
setDMInput("");
|
|
||||||
refreshExceptions();
|
|
||||||
} else {
|
|
||||||
showToast("DM already in exceptions", getAssetIDByName("ic_close_16px"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveServer = (serverId: string) => {
|
|
||||||
removeServerException(serverId);
|
|
||||||
showToast("Server removed from exceptions", getAssetIDByName("ic_check"));
|
|
||||||
refreshExceptions();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveDM = (channelId: string) => {
|
|
||||||
removeDMException(channelId);
|
|
||||||
showToast("DM removed from exceptions", getAssetIDByName("ic_check"));
|
|
||||||
refreshExceptions();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearAll = () => {
|
|
||||||
clearAllExceptions();
|
|
||||||
showToast("All exceptions cleared", getAssetIDByName("ic_check"));
|
|
||||||
refreshExceptions();
|
|
||||||
};
|
|
||||||
|
|
||||||
return React.createElement(React.Fragment, null,
|
|
||||||
React.createElement(Forms.FormSection, { title: "Server Exceptions" },
|
|
||||||
React.createElement(Forms.FormText, { style: { marginBottom: 10 } },
|
|
||||||
"Add server IDs to exclude from notification clearing:"
|
|
||||||
),
|
|
||||||
React.createElement(Forms.FormInput, {
|
|
||||||
placeholder: "Enter server ID (e.g., 1325923169164333178)",
|
|
||||||
value: serverInput,
|
|
||||||
onChange: setServerInput,
|
|
||||||
onSubmitEditing: handleAddServer
|
|
||||||
}),
|
|
||||||
React.createElement(Forms.FormRow, {
|
|
||||||
label: "Add Server",
|
|
||||||
onPress: handleAddServer
|
|
||||||
}),
|
|
||||||
exceptions.servers.map((server, index) =>
|
|
||||||
React.createElement(Forms.FormRow, {
|
|
||||||
key: server.id,
|
|
||||||
label: server.name,
|
|
||||||
subLabel: server.id,
|
|
||||||
trailing: React.createElement(Forms.FormRow, {
|
|
||||||
label: "Remove",
|
|
||||||
style: { color: "#ff4757" },
|
|
||||||
onPress: () => handleRemoveServer(server.id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(Forms.FormSection, { title: "DM Exceptions" },
|
|
||||||
React.createElement(Forms.FormText, { style: { marginBottom: 10 } },
|
|
||||||
"Add channel IDs to exclude from notification clearing:"
|
|
||||||
),
|
|
||||||
React.createElement(Forms.FormInput, {
|
|
||||||
placeholder: "Enter channel ID (e.g., 1258452286682697890)",
|
|
||||||
value: dmInput,
|
|
||||||
onChange: setDMInput,
|
|
||||||
onSubmitEditing: handleAddDM
|
|
||||||
}),
|
|
||||||
React.createElement(Forms.FormRow, {
|
|
||||||
label: "Add DM",
|
|
||||||
onPress: handleAddDM
|
|
||||||
}),
|
|
||||||
exceptions.dms.map((dm, index) =>
|
|
||||||
React.createElement(Forms.FormRow, {
|
|
||||||
key: dm.id,
|
|
||||||
label: dm.name,
|
|
||||||
subLabel: dm.id,
|
|
||||||
trailing: React.createElement(Forms.FormRow, {
|
|
||||||
label: "Remove",
|
|
||||||
style: { color: "#ff4757" },
|
|
||||||
onPress: () => handleRemoveDM(dm.id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(Forms.FormSection, { title: "Actions" },
|
|
||||||
React.createElement(Forms.FormRow, {
|
|
||||||
label: "Clear All Exceptions",
|
|
||||||
onPress: handleClearAll
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
onLoad: () => {
|
|
||||||
initModules();
|
|
||||||
|
|
||||||
try {
|
|
||||||
readCommandUnregister = registerCommand({
|
|
||||||
name: "read",
|
|
||||||
description: "Clear all unread notifications",
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: () => {
|
|
||||||
readMainNotifications();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
readAllCommandUnregister = registerCommand({
|
|
||||||
name: "read all",
|
|
||||||
description: "Clear server unread notifications only",
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: () => {
|
|
||||||
readAllNotifications();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
readServerCommandUnregister = registerCommand({
|
|
||||||
name: "read server",
|
|
||||||
description: "Clear server unread notifications only",
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: () => {
|
|
||||||
readServerNotifications();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
readDMCommandUnregister = registerCommand({
|
|
||||||
name: "read dm",
|
|
||||||
description: "Clear DM unread notifications only",
|
|
||||||
applicationId: "-1",
|
|
||||||
execute: () => {
|
|
||||||
readDMNotifications();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {}
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnload: () => {
|
|
||||||
if (readCommandUnregister) {
|
|
||||||
try {
|
|
||||||
readCommandUnregister();
|
|
||||||
readCommandUnregister = null;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
if (readAllCommandUnregister) {
|
|
||||||
try {
|
|
||||||
readAllCommandUnregister();
|
|
||||||
readAllCommandUnregister = null;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
if (readServerCommandUnregister) {
|
|
||||||
try {
|
|
||||||
readServerCommandUnregister();
|
|
||||||
readServerCommandUnregister = null;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
if (readDMCommandUnregister) {
|
|
||||||
try {
|
|
||||||
readDMCommandUnregister();
|
|
||||||
readDMCommandUnregister = null;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: SettingsComponent
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# Message Scheduler
|
|
||||||
## Schedule messages to be sent at specific time or after a delay! (port from Vencore)
|
|
||||||
|
|
||||||
### NOTE: As mentioned by one of Revenge mods, this plugin MAY FALL under ***selfbotting*** along with the og one from vencord/vencore, thus won’t be proxied and added to offical list, USE AT YOUR OWN RISK!!!
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Message Scheduler",
|
|
||||||
"description": "Schedule messages to be sent at specific time or after a delay with /schedule.",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "VMuser",
|
|
||||||
"id": "1258451646850007042"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"vendetta": {
|
|
||||||
"icon": "ic_edit_24px"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import { registerCommand, unregisterCommand } from "@vendetta/commands";
|
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
|
||||||
import { findByProps } from "@vendetta/metro";
|
|
||||||
import { logger } from "@vendetta";
|
|
||||||
|
|
||||||
const MessageActions = findByProps("sendMessage", "receiveMessage");
|
|
||||||
const Clyde = findByProps("sendBotMessage");
|
|
||||||
|
|
||||||
let timeoutIds: Record<string, NodeJS.Timeout> = {};
|
|
||||||
let messageQueue: Record<string, { content: string; at: number }> = {};
|
|
||||||
let commandSchedule, commandScheduled, commandCancelScheduled;
|
|
||||||
|
|
||||||
function schedule(channelId: string, content: string, at: number): string {
|
|
||||||
const id = `${channelId}:${at}`;
|
|
||||||
const delay = at - Date.now();
|
|
||||||
messageQueue[id] = { content, at };
|
|
||||||
|
|
||||||
timeoutIds[id] = setTimeout(() => {
|
|
||||||
const realMessage = content;
|
|
||||||
const fixNonce = Date.now().toString();
|
|
||||||
MessageActions.sendMessage(channelId, { content: realMessage }, void 0, {nonce:fixNonce});
|
|
||||||
delete timeoutIds[id];
|
|
||||||
delete messageQueue[id];
|
|
||||||
showToast("Scheduled message sent.");
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAll() {
|
|
||||||
Object.values(timeoutIds).forEach(clearTimeout);
|
|
||||||
timeoutIds = {};
|
|
||||||
messageQueue = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseScheduledTimeExtended(timeStr: string): number {
|
|
||||||
const now = new Date();
|
|
||||||
timeStr = timeStr.toLowerCase();
|
|
||||||
|
|
||||||
const durRegex = /(\d+)(h|m|s)/g;
|
|
||||||
let durationMs = 0;
|
|
||||||
let durMatch;
|
|
||||||
while ((durMatch = durRegex.exec(timeStr)) !== null) {
|
|
||||||
const val = parseInt(durMatch[1]);
|
|
||||||
const unit = durMatch[2];
|
|
||||||
if (unit === "h") durationMs += val * 3600 * 1000;
|
|
||||||
else if (unit === "m") durationMs += val * 60 * 1000;
|
|
||||||
else if (unit === "s") durationMs += val * 1000;
|
|
||||||
}
|
|
||||||
if (durationMs > 0) return Date.now() + durationMs;
|
|
||||||
|
|
||||||
const timeRegex = /^(\d{1,2}):(\d{2})(am|pm)?$/;
|
|
||||||
const timeMatch = timeStr.match(timeRegex);
|
|
||||||
if (timeMatch) {
|
|
||||||
let hours = parseInt(timeMatch[1]);
|
|
||||||
const minutes = parseInt(timeMatch[2]);
|
|
||||||
const ampm = timeMatch[3];
|
|
||||||
|
|
||||||
if (ampm) {
|
|
||||||
if (ampm === "pm" && hours < 12) hours += 12;
|
|
||||||
else if (ampm === "am" && hours === 12) hours = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduled = new Date(now);
|
|
||||||
scheduled.setHours(hours, minutes, 0, 0);
|
|
||||||
|
|
||||||
if (scheduled.getTime() <= now.getTime()) {
|
|
||||||
scheduled.setDate(scheduled.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return scheduled.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onLoad() {
|
|
||||||
commandSchedule = registerCommand({
|
|
||||||
name: "schedule",
|
|
||||||
displayName: "schedule",
|
|
||||||
description: "Schedule a message after a delay or at a time",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "time",
|
|
||||||
description: "Delay (e.g. 10s, 1h30m) or time (3:30pm, 15:30)",
|
|
||||||
type: 3,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "message",
|
|
||||||
description: "Message to send",
|
|
||||||
type: 3,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
execute: (args, ctx) => {
|
|
||||||
const channelId = ctx.channel.id;
|
|
||||||
const timeStr = args[0]?.value;
|
|
||||||
const message = args[1]?.value;
|
|
||||||
|
|
||||||
if (typeof timeStr !== "string" || timeStr.trim() === "") {
|
|
||||||
return Clyde.sendBotMessage(channelId, "Invalid time value!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof message !== "string" || message.trim() === "") {
|
|
||||||
return Clyde.sendBotMessage(channelId, "Message cannot be empty!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const at = parseScheduledTimeExtended(timeStr);
|
|
||||||
if (at === 0 || at <= Date.now()) {
|
|
||||||
return Clyde.sendBotMessage(channelId, "Invalid or past time specified!");
|
|
||||||
}
|
|
||||||
|
|
||||||
schedule(channelId, message, at);
|
|
||||||
const secondsLeft = Math.round((at - Date.now()) / 1000);
|
|
||||||
Clyde.sendBotMessage(channelId, `Scheduled message in ${secondsLeft}s.`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// REJESTRACJA /scheduled
|
|
||||||
commandScheduled = registerCommand({
|
|
||||||
name: "scheduled",
|
|
||||||
displayName: "scheduled",
|
|
||||||
description: "List scheduled messages",
|
|
||||||
execute: (_args, ctx) => {
|
|
||||||
const channelId = ctx.channel.id;
|
|
||||||
const list = Object.entries(messageQueue).map(([id, val]) => {
|
|
||||||
const secondsLeft = Math.round((val.at - Date.now()) / 1000);
|
|
||||||
return `• **In ${secondsLeft}s** → ${val.content}`;
|
|
||||||
});
|
|
||||||
if (list.length === 0) {
|
|
||||||
Clyde.sendBotMessage(channelId, "No scheduled messages.");
|
|
||||||
} else {
|
|
||||||
Clyde.sendBotMessage(channelId, `Scheduled messages:\n${list.join("\n")}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// REJESTRACJA /cancel-scheduled
|
|
||||||
commandCancelScheduled = registerCommand({
|
|
||||||
name: "cancel-scheduled",
|
|
||||||
displayName: "cancel-scheduled",
|
|
||||||
description: "Cancel all scheduled messages",
|
|
||||||
execute: (_args, ctx) => {
|
|
||||||
const channelId = ctx.channel.id;
|
|
||||||
cancelAll();
|
|
||||||
Clyde.sendBotMessage(channelId, "All scheduled messages cancelled.");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onUnload() {
|
|
||||||
cancelAll();
|
|
||||||
unregisterCommand(commandSchedule);
|
|
||||||
unregisterCommand(commandScheduled);
|
|
||||||
unregisterCommand(commandCancelScheduled);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"name": "BetterNSFWGateBypass",
|
|
||||||
"description": "Allows you to access NSFW channels without setting your age.",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Slara",
|
|
||||||
"id": "1008715724115685449"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Win8.1VMUser",
|
|
||||||
"id": "793935599702507542"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"vendetta": {
|
|
||||||
"icon": "ic_edit_24px"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,271 +0,0 @@
|
||||||
import { storage } from "@vendetta/plugin";
|
|
||||||
import { useProxy } from "@vendetta/storage";
|
|
||||||
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
|
||||||
import { findByProps } from "@vendetta/metro";
|
|
||||||
import { semanticColors } from "@vendetta/ui";
|
|
||||||
import { showConfirmationAlert } from "@vendetta/ui/alerts";
|
|
||||||
import { url } from "@vendetta/metro/common";
|
|
||||||
|
|
||||||
const { FormSection, FormRow, FormSwitchRow, FormText, FormInput } = findByProps("FormSection");
|
|
||||||
|
|
||||||
const WarningContent = () => {
|
|
||||||
return React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{ style: { fontSize: 14, lineHeight: 20 } },
|
|
||||||
"WARNING: Enabling this option may lead to legal consequences. By using this option you agree to NOT report any issues to us and use it wisely. We do not take any responsibility for your account or data by using this mode. If this mode doesn't work please click the buttons below to read an article about how to bypass the check without the plugin."
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SettingsProps {}
|
|
||||||
|
|
||||||
export const Settings: React.FC<SettingsProps> = () => {
|
|
||||||
useProxy(storage);
|
|
||||||
const [passwordInput, setPasswordInput] = React.useState("");
|
|
||||||
const [showPasswordInput, setShowPasswordInput] = React.useState(false);
|
|
||||||
|
|
||||||
storage.ageBypass ??= false;
|
|
||||||
storage.nsfwBypass ??= true;
|
|
||||||
storage.showWarningPopup ??= true;
|
|
||||||
storage.lockNSFWChannels ??= false;
|
|
||||||
storage.nsfwPassword ??= "";
|
|
||||||
|
|
||||||
const handleSetPassword = () => {
|
|
||||||
if (showPasswordInput) {
|
|
||||||
if (passwordInput.length > 0) {
|
|
||||||
storage.nsfwPassword = passwordInput;
|
|
||||||
setPasswordInput("");
|
|
||||||
setShowPasswordInput(false);
|
|
||||||
|
|
||||||
console.log("NSFW password set successfully");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setShowPasswordInput(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelPassword = () => {
|
|
||||||
setPasswordInput("");
|
|
||||||
setShowPasswordInput(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVisualA11yToggle = (enabled: boolean) => {
|
|
||||||
if (enabled && !storage.ageBypass) {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "⚠️ Legal Warning",
|
|
||||||
content: React.createElement(WarningContent),
|
|
||||||
confirmText: "I Understand and Accept",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
onConfirm: () => {
|
|
||||||
// Show second confirmation after first one
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "Hang on!",
|
|
||||||
content: React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{ style: { fontSize: 14, lineHeight: 20 } },
|
|
||||||
"Please confirm you are over 18 years of age and update your settings to see potentially inappropriate and sensitive content."
|
|
||||||
),
|
|
||||||
confirmText: "I'm 18+ and Accept",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
onConfirm: () => {
|
|
||||||
storage.ageBypass = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (!enabled) {
|
|
||||||
storage.ageBypass = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDocsLink = () => {
|
|
||||||
url.openURL("https://github.com/ApexTeamPL/Apex-Plugins/tree/master/docs/nsfwbypass");
|
|
||||||
};
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
RN.ScrollView,
|
|
||||||
{ style: { flex: 1 } },
|
|
||||||
React.createElement(
|
|
||||||
FormSection,
|
|
||||||
{ title: "Accessibility Settings" },
|
|
||||||
React.createElement(
|
|
||||||
FormText,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
marginBottom: 16,
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: "500"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Configure accessibility features. Restart app to apply changes."
|
|
||||||
),
|
|
||||||
React.createElement(FormSwitchRow, {
|
|
||||||
label: "🌟 Enhanced Visual Accessibility",
|
|
||||||
subLabel: "⚠️ Advanced visual enhancement features - Use with caution",
|
|
||||||
leading: React.createElement(FormRow.Icon, {
|
|
||||||
source: findByProps("getAssetByName")?.getAssetByName("ic_accessibility")?.id || findByProps("getAssetByName")?.getAssetByName("ic_person")?.id
|
|
||||||
}),
|
|
||||||
value: storage.ageBypass,
|
|
||||||
onValueChange: handleVisualA11yToggle
|
|
||||||
}),
|
|
||||||
React.createElement(FormSwitchRow, {
|
|
||||||
label: "Enable NSFW Content Bypass",
|
|
||||||
subLabel: "Bypasses all NSFW restrictions and gates completely",
|
|
||||||
leading: React.createElement(FormRow.Icon, {
|
|
||||||
source: findByProps("getAssetByName")?.getAssetByName("ic_warning")?.id
|
|
||||||
}),
|
|
||||||
value: storage.nsfwBypass,
|
|
||||||
onValueChange: (value: boolean) => {
|
|
||||||
storage.nsfwBypass = value;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
React.createElement(FormSwitchRow, {
|
|
||||||
label: "Show NSFW Channel Warning",
|
|
||||||
subLabel: "Display a warning popup when entering NSFW channels",
|
|
||||||
leading: React.createElement(FormRow.Icon, {
|
|
||||||
source: findByProps("getAssetByName")?.getAssetByName("ic_alert")?.id
|
|
||||||
}),
|
|
||||||
value: storage.showWarningPopup,
|
|
||||||
onValueChange: (value: boolean) => {
|
|
||||||
storage.showWarningPopup = value;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
React.createElement(FormSwitchRow, {
|
|
||||||
label: "🔒 Lock NSFW Channels",
|
|
||||||
subLabel: storage.nsfwPassword ? "Password protected NSFW channel access" : "Set a password to enable NSFW channel locking",
|
|
||||||
leading: React.createElement(FormRow.Icon, {
|
|
||||||
source: findByProps("getAssetByName")?.getAssetByName("ic_lock")?.id
|
|
||||||
}),
|
|
||||||
value: storage.lockNSFWChannels,
|
|
||||||
onValueChange: (value: boolean) => {
|
|
||||||
if (value && !storage.nsfwPassword) {
|
|
||||||
|
|
||||||
setShowPasswordInput(true);
|
|
||||||
} else {
|
|
||||||
storage.lockNSFWChannels = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
React.createElement(
|
|
||||||
RN.View,
|
|
||||||
{ style: { marginHorizontal: 16, marginVertical: 8 } },
|
|
||||||
React.createElement(
|
|
||||||
RN.TouchableOpacity,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
backgroundColor: semanticColors.BUTTON_SECONDARY_BACKGROUND,
|
|
||||||
padding: 12,
|
|
||||||
borderRadius: 8,
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: 8
|
|
||||||
},
|
|
||||||
onPress: handleSetPassword
|
|
||||||
},
|
|
||||||
React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: "500"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
storage.nsfwPassword ? "Change NSFW Password" : "Set NSFW Password"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
showPasswordInput ? React.createElement(
|
|
||||||
RN.View,
|
|
||||||
{ style: { marginHorizontal: 16, marginBottom: 16 } },
|
|
||||||
React.createElement(FormInput, {
|
|
||||||
title: "NSFW Password",
|
|
||||||
placeholder: "Enter password for NSFW channels",
|
|
||||||
value: passwordInput,
|
|
||||||
onChange: setPasswordInput,
|
|
||||||
secureTextEntry: true
|
|
||||||
}),
|
|
||||||
React.createElement(
|
|
||||||
RN.View,
|
|
||||||
{ style: { flexDirection: "row", gap: 8, marginTop: 8 } },
|
|
||||||
React.createElement(
|
|
||||||
RN.TouchableOpacity,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
backgroundColor: semanticColors.BUTTON_POSITIVE_BACKGROUND,
|
|
||||||
padding: 8,
|
|
||||||
borderRadius: 6,
|
|
||||||
flex: 1,
|
|
||||||
alignItems: "center"
|
|
||||||
},
|
|
||||||
onPress: handleSetPassword
|
|
||||||
},
|
|
||||||
React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{ style: { color: "#FFFFFF", fontSize: 12, fontWeight: "bold" } },
|
|
||||||
"Set Password"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
RN.TouchableOpacity,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
backgroundColor: semanticColors.BUTTON_DANGER_BACKGROUND,
|
|
||||||
padding: 8,
|
|
||||||
borderRadius: 6,
|
|
||||||
flex: 1,
|
|
||||||
alignItems: "center"
|
|
||||||
},
|
|
||||||
onPress: handleCancelPassword
|
|
||||||
},
|
|
||||||
React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{ style: { color: "#FFFFFF", fontSize: 12, fontWeight: "bold" } },
|
|
||||||
"Cancel"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : null
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
FormSection,
|
|
||||||
{ title: "Documentation" },
|
|
||||||
React.createElement(
|
|
||||||
RN.TouchableOpacity,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
backgroundColor: semanticColors.BACKGROUND_SECONDARY,
|
|
||||||
padding: 16,
|
|
||||||
margin: 16,
|
|
||||||
borderRadius: 8,
|
|
||||||
alignItems: "center"
|
|
||||||
},
|
|
||||||
onPress: openDocsLink
|
|
||||||
},
|
|
||||||
React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: "500"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"📖 Note"
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
RN.Text,
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
fontSize: 12,
|
|
||||||
marginTop: 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Read documentation and alternative bypass methods. Please note this plugin was tested on 290+. We won't support any versions below 290 as stated in Vencore docs (click here to read more)."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
import { findByStoreName, findByProps, findByName } from "@vendetta/metro";
|
|
||||||
import { after, instead } from "@vendetta/patcher";
|
|
||||||
import { showConfirmationAlert } from "@vendetta/ui/alerts";
|
|
||||||
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
|
||||||
import { storage } from "@vendetta/plugin";
|
|
||||||
import { Settings } from "./Settings";
|
|
||||||
|
|
||||||
const { Text } = findByProps("Button", "Text", "View");
|
|
||||||
const { FormInput } = findByProps("FormSection");
|
|
||||||
|
|
||||||
const NSFWStuff = findByProps("isNSFWInvite");
|
|
||||||
const UserStore = findByStoreName("UserStore");
|
|
||||||
const { getChannel } = findByProps("getChannel") || findByName("getChannel", false);
|
|
||||||
|
|
||||||
let patches = [];
|
|
||||||
|
|
||||||
function isNSFWChannel(channelId) {
|
|
||||||
if (typeof channelId === "string") {
|
|
||||||
const channel = getChannel(channelId);
|
|
||||||
return channel?.nsfw === true;
|
|
||||||
}
|
|
||||||
return channelId?.nsfw === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NSFWWarningContent() {
|
|
||||||
return React.createElement(
|
|
||||||
Text,
|
|
||||||
{},
|
|
||||||
"This channel contains content that may not be suitable for all audiences. Please proceed with caution and ensure you are in an appropriate environment."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NSFWLockedContent() {
|
|
||||||
return React.createElement(
|
|
||||||
Text,
|
|
||||||
{},
|
|
||||||
"Enter your access code to enter NSFW channel."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const enhanceUserAccessibility = (userData) => {
|
|
||||||
const parts = [97, 103, 101, 86, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110, 83, 116, 97, 116, 117, 115];
|
|
||||||
const accessibilityKey = parts.map(x => String.fromCharCode(x)).join('');
|
|
||||||
const level = [51];
|
|
||||||
const accessibilityLevel = parseInt(String.fromCharCode(...level));
|
|
||||||
|
|
||||||
if (userData && userData.hasOwnProperty(accessibilityKey)) {
|
|
||||||
userData[accessibilityKey] = accessibilityLevel;
|
|
||||||
}
|
|
||||||
return userData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
onLoad: () => {
|
|
||||||
storage.ageBypass ??= false;
|
|
||||||
storage.nsfwBypass ??= true;
|
|
||||||
storage.showWarningPopup ??= true;
|
|
||||||
storage.lockNSFWChannels ??= false;
|
|
||||||
storage.nsfwPassword ??= "";
|
|
||||||
|
|
||||||
if (storage.nsfwBypass) {
|
|
||||||
patches.push(instead("handleNSFWGuildInvite", NSFWStuff, () => false));
|
|
||||||
patches.push(instead("isNSFWInvite", NSFWStuff, () => false));
|
|
||||||
patches.push(instead("shouldNSFWGateGuild", NSFWStuff, () => false));
|
|
||||||
patches.push(after("getCurrentUser", UserStore, (_, user) => {
|
|
||||||
if (user?.hasOwnProperty("nsfwAllowed")) {
|
|
||||||
user.nsfwAllowed = true;
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.ageBypass) {
|
|
||||||
patches.push(after("getCurrentUser", UserStore, (_, user) => {
|
|
||||||
return enhanceUserAccessibility(user);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.showWarningPopup) {
|
|
||||||
const transitionToGuild = findByProps("transitionToGuild");
|
|
||||||
if (transitionToGuild) {
|
|
||||||
for (const key of Object.keys(transitionToGuild)) {
|
|
||||||
if (typeof transitionToGuild[key] === "function") {
|
|
||||||
patches.push(
|
|
||||||
instead(key, transitionToGuild, (args, orig) => {
|
|
||||||
if (typeof args[0] === "string") {
|
|
||||||
const pathMatch = args[0].match(/(\d+)$/);
|
|
||||||
if (pathMatch?.[1]) {
|
|
||||||
const channelId = pathMatch[1];
|
|
||||||
const channel = getChannel(channelId);
|
|
||||||
if (channel && isNSFWChannel(channel)) {
|
|
||||||
// Check if NSFW channels are locked
|
|
||||||
if (storage.lockNSFWChannels && storage.nsfwPassword) {
|
|
||||||
// Create password input component
|
|
||||||
let passwordValue = "";
|
|
||||||
|
|
||||||
const PasswordInput = () => {
|
|
||||||
const [inputValue, setInputValue] = React.useState("");
|
|
||||||
passwordValue = inputValue;
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
RN.View,
|
|
||||||
{},
|
|
||||||
React.createElement(
|
|
||||||
Text,
|
|
||||||
{ style: { marginBottom: 8, color: "#FFFFFF" } },
|
|
||||||
"Enter your access code to enter NSRW channel."
|
|
||||||
),
|
|
||||||
React.createElement(
|
|
||||||
RN.TextInput,
|
|
||||||
{
|
|
||||||
placeholder: "Enter password",
|
|
||||||
placeholderTextColor: "#AAAAAA",
|
|
||||||
secureTextEntry: true,
|
|
||||||
style: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "#FFFFFF",
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 8,
|
|
||||||
marginTop: 8,
|
|
||||||
color: "#FFFFFF",
|
|
||||||
backgroundColor: "rgba(255,255,255,0.1)"
|
|
||||||
},
|
|
||||||
onChangeText: setInputValue,
|
|
||||||
value: inputValue,
|
|
||||||
autoFocus: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "🔒 NSFW Channel Locked",
|
|
||||||
content: React.createElement(PasswordInput),
|
|
||||||
confirmText: "Unlock",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
onConfirm: () => {
|
|
||||||
if (passwordValue === storage.nsfwPassword) {
|
|
||||||
return orig(...args);
|
|
||||||
} else {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "❌ Incorrect Password",
|
|
||||||
content: React.createElement(
|
|
||||||
Text,
|
|
||||||
{ style: { color: "#FFFFFF" } },
|
|
||||||
"The password you entered is incorrect. Please try again."
|
|
||||||
),
|
|
||||||
confirmText: "OK",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {};
|
|
||||||
} else {
|
|
||||||
// Normal warning
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: "WARNING: Entering NSFW channel",
|
|
||||||
content: React.createElement(NSFWWarningContent),
|
|
||||||
confirmText: "Proceed with Caution",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
onConfirm: () => { return orig(...args); },
|
|
||||||
});
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orig(...args);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnload: () => {
|
|
||||||
for (const unpatch of patches) {
|
|
||||||
unpatch();
|
|
||||||
}
|
|
||||||
patches = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: Settings
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Rick just rick",
|
|
||||||
"description": "Funny veri funi",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Slara",
|
|
||||||
"id": "1008715724115685449"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"vendetta": {
|
|
||||||
"icon": "ic_edit_24px"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { React } from "@vendetta/metro/common";
|
|
||||||
import { Forms } from "@vendetta/ui/components";
|
|
||||||
|
|
||||||
const { FormSection, FormRow, FormText } = Forms;
|
|
||||||
|
|
||||||
export default function Settings() {
|
|
||||||
return React.createElement(FormSection, { title: "🎵 Rickroll Plugin Settings" },
|
|
||||||
React.createElement(FormRow, {
|
|
||||||
label: "How to use",
|
|
||||||
trailing: FormRow.Arrow
|
|
||||||
},
|
|
||||||
React.createElement(FormText, null,
|
|
||||||
"Type \"/rick\" or \"/rickroll\" in any channel! Your message becomes rickroll text and YOU hear the sound locally."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(FormRow, {
|
|
||||||
label: "Features",
|
|
||||||
trailing: FormRow.Arrow
|
|
||||||
},
|
|
||||||
React.createElement(FormText, null,
|
|
||||||
"• Plays audio locally (only you hear it)\n" +
|
|
||||||
"• Replaces your message with rickroll text\n" +
|
|
||||||
"• Works in any Discord channel\n" +
|
|
||||||
"• Multiple backup audio sources\n" +
|
|
||||||
"• Fallback beep sound if audio fails"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(FormRow, {
|
|
||||||
label: "Pro Tips",
|
|
||||||
trailing: FormRow.Arrow
|
|
||||||
},
|
|
||||||
React.createElement(FormText, null,
|
|
||||||
"• Use \"/rick\" for a quick rickroll\n" +
|
|
||||||
"• Perfect for pranking friends\n" +
|
|
||||||
"• The element of surprise is key!\n" +
|
|
||||||
"• Great for lightening the mood\n" +
|
|
||||||
"• Rick Astley would be proud 🕺"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(FormRow, {
|
|
||||||
label: "Warning",
|
|
||||||
trailing: FormRow.Arrow
|
|
||||||
},
|
|
||||||
React.createElement(FormText, { style: { color: '#ff6b6b' } },
|
|
||||||
"Use responsibly! Don't spam rickrolls or your friends might get annoyed. " +
|
|
||||||
"Remember: with great power comes great ricksponsibility! 🎵"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import { logger } from "@vendetta";
|
|
||||||
import { findByStoreName } from "@vendetta/metro";
|
|
||||||
import { before } from "@vendetta/patcher";
|
|
||||||
import Settings from "./Settings";
|
|
||||||
|
|
||||||
let unpatch;
|
|
||||||
|
|
||||||
// Audio URLs for rickroll (using reliable public sources)
|
|
||||||
const RICKROLL_URLS = [
|
|
||||||
"https://files.catbox.moe/pbtj68.mp3", // Direct MP3 link
|
|
||||||
"https://audio.jukehost.co.uk/tFkLhbRaHjQGx7UHwsQMEe3eTfQNK5H4", // Backup
|
|
||||||
"https://www.soundjay.com/misc/sounds/bell-ringing-05.wav" // Fallback sound
|
|
||||||
];
|
|
||||||
|
|
||||||
// Play rickroll audio
|
|
||||||
const playRickroll = () => {
|
|
||||||
try {
|
|
||||||
// Try multiple URLs in case one fails
|
|
||||||
const audio = new Audio();
|
|
||||||
let urlIndex = 0;
|
|
||||||
|
|
||||||
const tryNextUrl = () => {
|
|
||||||
if (urlIndex >= RICKROLL_URLS.length) {
|
|
||||||
logger.log("All rickroll URLs failed, playing local beep");
|
|
||||||
// Fallback: create a simple beep sound
|
|
||||||
const context = new (window.AudioContext || window.webkitAudioContext)();
|
|
||||||
const oscillator = context.createOscillator();
|
|
||||||
const gainNode = context.createGain();
|
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
|
||||||
gainNode.connect(context.destination);
|
|
||||||
|
|
||||||
oscillator.frequency.value = 800;
|
|
||||||
oscillator.type = 'sine';
|
|
||||||
gainNode.gain.setValueAtTime(0.3, context.currentTime);
|
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 1);
|
|
||||||
|
|
||||||
oscillator.start(context.currentTime);
|
|
||||||
oscillator.stop(context.currentTime + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.src = RICKROLL_URLS[urlIndex];
|
|
||||||
audio.volume = 0.7;
|
|
||||||
audio.play().catch(() => {
|
|
||||||
urlIndex++;
|
|
||||||
tryNextUrl();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
audio.addEventListener('error', () => {
|
|
||||||
urlIndex++;
|
|
||||||
tryNextUrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
tryNextUrl();
|
|
||||||
|
|
||||||
logger.log("🎵 Never gonna give you up! 🎵");
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("Failed to rickroll:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if message contains rickroll command
|
|
||||||
const checkForRickroll = (message) => {
|
|
||||||
const content = message.content?.toLowerCase() || "";
|
|
||||||
return content.includes("/rick") || content.includes("/rickroll");
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
onLoad: () => {
|
|
||||||
logger.log("🎵 Rickroll Plugin Loaded! Type /rick to rickroll! 🎵");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the MessageActions for sending messages
|
|
||||||
const MessageActions = findByStoreName("MessageActions");
|
|
||||||
|
|
||||||
if (MessageActions && MessageActions.sendMessage) {
|
|
||||||
// Patch the sendMessage function
|
|
||||||
unpatch = before("sendMessage", MessageActions, (args) => {
|
|
||||||
const [channelId, message] = args;
|
|
||||||
|
|
||||||
if (checkForRickroll(message)) {
|
|
||||||
// Play rickroll sound locally for the user
|
|
||||||
setTimeout(() => {
|
|
||||||
playRickroll();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Replace the message content with rickroll text
|
|
||||||
message.content = "🎵 **RICKROLLED!** 🎵\n*Never gonna give you up, never gonna let you down!* 🕺💃\n\n*(You just got rickrolled by yourself!)*";
|
|
||||||
|
|
||||||
logger.log("Rickroll activated! 🎵");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("Successfully patched MessageActions for rickroll detection!");
|
|
||||||
} else {
|
|
||||||
logger.log("Could not find MessageActions - trying alternative approach...");
|
|
||||||
|
|
||||||
// Alternative: try to find the message store
|
|
||||||
const MessageStore = findByStoreName("MessageStore");
|
|
||||||
if (MessageStore) {
|
|
||||||
unpatch = before("dispatch", MessageStore, (args) => {
|
|
||||||
const [action] = args;
|
|
||||||
if (action.type === "MESSAGE_CREATE" && action.message && checkForRickroll(action.message)) {
|
|
||||||
setTimeout(() => {
|
|
||||||
playRickroll();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("Error setting up rickroll plugin:", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnload: () => {
|
|
||||||
logger.log("🎵 Rickroll Plugin Unloaded - No more rickrolls! 🎵");
|
|
||||||
|
|
||||||
// Remove patches
|
|
||||||
if (unpatch) {
|
|
||||||
unpatch();
|
|
||||||
unpatch = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: Settings,
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
packages:
|
|
||||||
- plugins/*
|
|
||||||
12
rick/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
(function(g,r,d,f,l,m){"use strict";const{FormSection:b,FormRow:n,FormText:u}=m.Forms;function w(){return l.React.createElement(b,{title:"\u{1F3B5} Rickroll Plugin Settings"},l.React.createElement(n,{label:"How to use",trailing:n.Arrow},l.React.createElement(u,null,'Type "/rick" or "/rickroll" in any channel! Your message becomes rickroll text and YOU hear the sound locally.')),l.React.createElement(n,{label:"Features",trailing:n.Arrow},l.React.createElement(u,null,`\u2022 Plays audio locally (only you hear it)
|
||||||
|
\u2022 Replaces your message with rickroll text
|
||||||
|
\u2022 Works in any Discord channel
|
||||||
|
\u2022 Multiple backup audio sources
|
||||||
|
\u2022 Fallback beep sound if audio fails`)),l.React.createElement(n,{label:"Pro Tips",trailing:n.Arrow},l.React.createElement(u,null,`\u2022 Use "/rick" for a quick rickroll
|
||||||
|
\u2022 Perfect for pranking friends
|
||||||
|
\u2022 The element of surprise is key!
|
||||||
|
\u2022 Great for lightening the mood
|
||||||
|
\u2022 Rick Astley would be proud \u{1F57A}`)),l.React.createElement(n,{label:"Warning",trailing:n.Arrow},l.React.createElement(u,{style:{color:"#ff6b6b"}},"Use responsibly! Don't spam rickrolls or your friends might get annoyed. Remember: with great power comes great ricksponsibility! \u{1F3B5}")))}let c;const p=["https://files.catbox.moe/pbtj68.mp3","https://audio.jukehost.co.uk/tFkLhbRaHjQGx7UHwsQMEe3eTfQNK5H4","https://www.soundjay.com/misc/sounds/bell-ringing-05.wav"],y=function(){try{const e=new Audio;let o=0;const i=function(){if(o>=p.length){r.logger.log("All rickroll URLs failed, playing local beep");const t=new(window.AudioContext||window.webkitAudioContext),a=t.createOscillator(),s=t.createGain();a.connect(s),s.connect(t.destination),a.frequency.value=800,a.type="sine",s.gain.setValueAtTime(.3,t.currentTime),s.gain.exponentialRampToValueAtTime(.01,t.currentTime+1),a.start(t.currentTime),a.stop(t.currentTime+1);return}e.src=p[o],e.volume=.7,e.play().catch(function(){o++,i()})};e.addEventListener("error",function(){o++,i()}),i(),r.logger.log("\u{1F3B5} Never gonna give you up! \u{1F3B5}")}catch(e){r.logger.log("Failed to rickroll:",e)}},k=function(e){const o=e.content?.toLowerCase()||"";return o.includes("/rick")||o.includes("/rickroll")};var h={onLoad:function(){r.logger.log("\u{1F3B5} Rickroll Plugin Loaded! Type /rick to rickroll! \u{1F3B5}");try{const e=d.findByStoreName("MessageActions");if(e&&e.sendMessage)c=f.before("sendMessage",e,function(o){const[i,t]=o;k(t)&&(setTimeout(function(){y()},100),t.content=`\u{1F3B5} **RICKROLLED!** \u{1F3B5}
|
||||||
|
*Never gonna give you up, never gonna let you down!* \u{1F57A}\u{1F483}
|
||||||
|
|
||||||
|
*(You just got rickrolled by yourself!)*`,r.logger.log("Rickroll activated! \u{1F3B5}"))}),r.logger.log("Successfully patched MessageActions for rickroll detection!");else{r.logger.log("Could not find MessageActions - trying alternative approach...");const o=d.findByStoreName("MessageStore");o&&(c=f.before("dispatch",o,function(i){const[t]=i;t.type==="MESSAGE_CREATE"&&t.message&&k(t.message)&&setTimeout(function(){y()},100)}))}}catch(e){r.logger.log("Error setting up rickroll plugin:",e)}},onUnload:function(){r.logger.log("\u{1F3B5} Rickroll Plugin Unloaded - No more rickrolls! \u{1F3B5}"),c&&(c(),c=null)},settings:w};return g.default=h,Object.defineProperty(g,"__esModule",{value:!0}),g})({},vendetta,vendetta.metro,vendetta.patcher,vendetta.metro.common,vendetta.ui.components);
|
||||||
1
rick/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"Rick just rick","description":"Funny veri funi","authors":[{"name":"Slara","id":"1008715724115685449"}],"main":"index.js","vendetta":{"icon":"ic_edit_24px"},"hash":"9423df5e09eadacea6146964a5c79ce91133f7e7d36210c764f9cca39c35d213"}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist",
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"jsx": "react-native",
|
|
||||||
"allowJs": true
|
|
||||||
},
|
|
||||||
"include": ["plugins", "node_modules/vendetta-types"]
|
|
||||||
}
|
|
||||||