1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-10 09:56:24 +00:00

add basic plugin

This commit is contained in:
splatter 2024-05-06 19:43:20 +01:00
parent ce18000c4e
commit 8b508881bc
No known key found for this signature in database
GPG key ID: A069D884579D2915
3 changed files with 258 additions and 0 deletions

View file

@ -0,0 +1,223 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./styles.css";
import { ApplicationCommandInputType, ApplicationCommandType, sendBotMessage } from "@api/Commands";
import { Commands, DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { Switch } from "@components/Switch";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { useAwaiter } from "@utils/react";
import { wordsToKebab as kebab } from "@utils/text";
import definePlugin, { OptionType } from "@utils/types";
import {
Button,
showToast,
Text,
TextArea,
TextInput,
Toasts,
useCallback,
useEffect,
useState
} from "@webpack/common";
const snippetLogger = new Logger("ScriptSnippets");
const wordsToKebab = (words: string) => kebab(words.split(/[\s\-_]+/).map(s => s.replace(/[^\w]/g, "")));
const SNIPPET_KEY = "ScriptSnippets_snippets";
type Snippets = Snippet[];
interface Snippet {
trigger: ScriptTrigger;
key: string;
code: string;
name: string;
}
enum ScriptTrigger {
STARTUP = "startup",
SLASH_COMMAND = "command"
}
const SnippetSettings = ({ snippet, update, delete: del }: { snippet: Snippet, update: (newSnippet: Snippet) => void, delete: () => void }) => {
const [code, setCode] = useState(snippet.code);
const [name, setName] = useState(wordsToKebab(snippet.name));
const [trigger, setTrigger] = useState(snippet.trigger);
return <form onSubmit={event => {
event.preventDefault();
update({ ...snippet, code, name, trigger });
}}>
<div className="vc-scriptsnippets-settings-script">
<div>
<TextInput required value={name} onChange={setName} name="name" placeholder="My script"/>
<div className="vc-scriptsnippets-settings-startup"><Text>Run on startup</Text> <Switch checked={trigger === ScriptTrigger.STARTUP} onChange={() => setTrigger(prev => prev === ScriptTrigger.STARTUP ? ScriptTrigger.SLASH_COMMAND : ScriptTrigger.STARTUP)} /></div>
</div>
<div className="vc-scriptsnippets-settings-btn">
<Button type="submit" size={Button.Sizes.SMALL}>Save</Button>
<Button type="button" onClick={del} size={Button.Sizes.SMALL} color={Button.Colors.RED}>Delete</Button>
</div>
</div>
<TextArea required value={code} onChange={setCode} name="code" placeholder="console.log('Hello world!');" />
</form>;
};
const settings = definePluginSettings({
snippets: {
type: OptionType.COMPONENT,
description: "Add snippets to run on certain triggers",
component: () => {
const [loadedSnippets, err, pending] = useAwaiter(() => DataStore.get<Snippets>(SNIPPET_KEY));
const [realSnippets, setRealSnippets] = useState<Snippets>(loadedSnippets || []);
useEffect(() => {
if (!pending && !err) {
setRealSnippets(loadedSnippets || []);
}
}, [pending]);
const updateSnippet = useCallback((newSnippet: Snippet) => {
if (!newSnippet.code || !newSnippet.name) return showToast("Name and code are required", Toasts.Type.FAILURE);
const newSnippets = realSnippets.filter(snippet => snippet.key !== newSnippet.key);
newSnippets.push(newSnippet);
setRealSnippets(newSnippets);
DataStore.set(SNIPPET_KEY, newSnippets).then(() => {
Commands.unregisterCommand(wordsToKebab(newSnippet.name));
Commands.registerCommand(createCommand(newSnippet), "ScriptSnippets");
showToast("Your script has been updated", Toasts.Type.SUCCESS);
}).catch(err => {
showToast("There was an error updating your script: " + err, Toasts.Type.FAILURE);
});
}, [realSnippets]);
const deleteSnippet = useCallback((key: string) => {
const newSnippets = realSnippets.filter(snippet => snippet.key !== key);
setRealSnippets(newSnippets);
DataStore.set(SNIPPET_KEY, newSnippets).then(() => {
showToast("Your script has been deleted", Toasts.Type.SUCCESS);
}).catch(err => {
showToast("There was an error deleting your script: " + err, Toasts.Type.FAILURE);
});
}, [realSnippets]);
if (pending) return <Text>Loading snippets...</Text>;
if (err) return <Text>There was an error loading snippets: {err}</Text>;
return (
<>
<Text>Snippets you create will be registered as a slash command. Changes require a restart to take effect.</Text>
{realSnippets?.map(snippet => <SnippetSettings snippet={snippet} update={updateSnippet} delete={() => deleteSnippet(snippet.key)} />)}
<Button onClick={() => {
setRealSnippets(snippets => [...snippets, {
code: "",
trigger: ScriptTrigger.SLASH_COMMAND,
// TODO: get a new way to generate unique keys
key: Date.now().toString(),
name: ""
}]);
}}>Create a script</Button>
</>
);
}
}
});
const createCommand = (snippet: Snippet) => ({
name: wordsToKebab(snippet.name),
description: "Run this script",
type: ApplicationCommandType.CHAT_INPUT,
inputType: ApplicationCommandInputType.BUILT_IN,
isVencordCommand: true,
execute: async (_, ctx) => {
try {
snippetLogger.debug(`Running script: ${snippet.name}`, snippet.code);
await eval(snippet.code);
snippetLogger.debug(`Script executed successfully: ${snippet.name}`);
sendBotMessage(ctx.channel.id, {
content: "Script executed successfully"
});
} catch (e) {
snippetLogger.debug(`Error running script: ${e}`);
sendBotMessage(ctx.channel.id, {
content: `There was an error running your script:\n\n\`\`\`${e}\`\`\``
});
}
}
});
export default definePlugin({
name: "ScriptSnippets",
description: "Add snippets to run on certain triggers",
authors: [Devs.splatter],
dependencies: ["CommandsAPI"],
settings,
async start() {
const snippets = await DataStore.get<Snippets>(SNIPPET_KEY);
snippetLogger.info(`Loaded ${snippets?.length ?? 0} snippets`);
if (snippets) for (const snippet of snippets) {
switch (snippet.trigger) {
case ScriptTrigger.SLASH_COMMAND: {
Commands.registerCommand(createCommand(snippet), "ScriptSnippets");
break;
}
case ScriptTrigger.STARTUP: {
try {
eval(snippet.code);
} catch (e) {
snippetLogger.error(`Error running startup script: ${e}`);
}
break;
}
default: {
snippetLogger.error(`Invalid trigger: ${snippet.trigger}`);
break;
}
}
}
},
async stop() {
const snippets = await DataStore.get(SNIPPET_KEY);
const commands = snippets?.map(snippet => wordsToKebab(snippet.name));
if (commands) {
for (const command of commands) {
Commands.unregisterCommand(command);
}
}
}
});

View file

@ -0,0 +1,31 @@
.vc-scriptsnippets-settings-script {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 1em;
margin-top: .25em;
margin-bottom: .5em;
}
.vc-scriptsnippets-settings-script > div {
display: flex;
gap: 1em;
margin-top: 0.5em;
flex-direction: row;
}
.vc-scriptsnippets-settings-btn {
/* even though this is below the previous rule, it will still override it for some reason */
gap: .5em !important;
}
.vc-scriptsnippets-settings-startup {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 0.5em;
margin-top: 0.25em;
margin-bottom: 0.25em;
}

View file

@ -453,6 +453,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
GabiRP: {
name: "GabiRP",
id: 507955112027750401n
},
splatter: {
name: "splatter",
id: 728342296696979526n
}
} satisfies Record<string, Dev>);