255 lines
7.9 KiB
TypeScript
255 lines
7.9 KiB
TypeScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2024 Rose, Vendicated and contributors
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { DataStore } from "@api/index";
|
|
import { definePluginSettings } from "@api/Settings";
|
|
import { Flex } from "@components/Flex";
|
|
import { DeleteIcon } from "@components/Icons";
|
|
import { makeRange } from "@components/PluginSettings/components/SettingSliderComponent";
|
|
import { sleep } from "@utils/misc";
|
|
import { useForceUpdater } from "@utils/react";
|
|
import definePlugin, { OptionType } from "@utils/types";
|
|
import {
|
|
Button,
|
|
Forms,
|
|
React,
|
|
RelationshipStore,
|
|
SelectedChannelStore,
|
|
TextInput,
|
|
useState,
|
|
} from "@webpack/common";
|
|
import { Message } from "discord-types/general";
|
|
|
|
interface IMessageCreate {
|
|
type: "MESSAGE_CREATE";
|
|
optimistic: boolean;
|
|
isPushNotification: boolean;
|
|
channelId: string;
|
|
message: Message;
|
|
}
|
|
|
|
interface IRPCNotificationCreate {
|
|
type: "RPC_NOTIFICATION_CREATE";
|
|
channelId: string;
|
|
message: Message;
|
|
}
|
|
|
|
const TRIGGER_RULES_KEY = "[soundTrigger]-rules";
|
|
|
|
interface TriggerRule {
|
|
trigger: string;
|
|
sound: string;
|
|
}
|
|
let triggerRules = [
|
|
{
|
|
sound: "",
|
|
trigger: "",
|
|
},
|
|
] as TriggerRule[];
|
|
|
|
interface InputProps {
|
|
placeholder: string;
|
|
initalValue: string;
|
|
onChange(newValue: string): void;
|
|
}
|
|
|
|
function Input({ placeholder, initalValue, onChange }: InputProps) {
|
|
const [value, setValue] = useState(initalValue);
|
|
return (
|
|
<TextInput
|
|
placeholder={placeholder}
|
|
value={value}
|
|
onChange={setValue}
|
|
spellCheck={false}
|
|
onBlur={() => value !== initalValue && onChange(value)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function RuleInputter({ update }: { update: () => void; }) {
|
|
async function deleteRule(index: number) {
|
|
if (index === triggerRules.length - 1) return;
|
|
triggerRules.splice(index, 1);
|
|
|
|
await DataStore.set(TRIGGER_RULES_KEY, triggerRules);
|
|
update();
|
|
}
|
|
|
|
async function modifyRule(
|
|
newValue: string,
|
|
index: number,
|
|
key: keyof TriggerRule
|
|
) {
|
|
if (index === triggerRules.length - 1) {
|
|
triggerRules.push({
|
|
sound: "",
|
|
trigger: "",
|
|
});
|
|
}
|
|
triggerRules[index][key] = newValue;
|
|
|
|
if (triggerRules[index].trigger === "" && triggerRules[index].sound === "" && index !== triggerRules.length - 1) {
|
|
triggerRules.splice(index, 1);
|
|
}
|
|
|
|
await DataStore.set(TRIGGER_RULES_KEY, triggerRules);
|
|
update();
|
|
}
|
|
return (
|
|
<>
|
|
<Forms.FormDivider />
|
|
<Forms.FormTitle tag="h4">Rules</Forms.FormTitle>
|
|
<Flex flexDirection="column" style={{ gap: "0.5em" }}>
|
|
{triggerRules.map((rule, ind) => {
|
|
return (
|
|
<React.Fragment key={`${rule.trigger}-${ind}`}>
|
|
<Flex flexDirection="row" style={{ gap: "0.5em" }}>
|
|
<div
|
|
style={{
|
|
flexGrow: 1,
|
|
gap: "0.5em",
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr 1fr",
|
|
}}
|
|
>
|
|
<Input
|
|
placeholder="Trigger"
|
|
initalValue={rule.trigger}
|
|
onChange={e => modifyRule(e, ind, "trigger")}
|
|
/>
|
|
<TextInput
|
|
placeholder="Sound"
|
|
value={rule.sound}
|
|
onChange={e => modifyRule(e, ind, "sound")}
|
|
/>
|
|
</div>
|
|
<Button
|
|
size={Button.Sizes.ICON}
|
|
onClick={() => deleteRule(ind)}
|
|
style={{
|
|
background: "none",
|
|
color: "var(--status-danger)",
|
|
...(ind === triggerRules.length - 1
|
|
? {
|
|
visibility: "hidden",
|
|
pointerEvents: "none",
|
|
}
|
|
: {}),
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</Button>
|
|
</Flex>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</Flex>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const settings = definePluginSettings({
|
|
rules: {
|
|
type: OptionType.COMPONENT,
|
|
description: "List of triggers to react to",
|
|
component: () => {
|
|
const update = useForceUpdater();
|
|
return <RuleInputter update={update} />;
|
|
},
|
|
},
|
|
delay: {
|
|
description: "Delay between triggers",
|
|
type: OptionType.SLIDER,
|
|
markers: makeRange(0, 500, 50),
|
|
default: 250,
|
|
stickToMarkers: true,
|
|
},
|
|
volume: {
|
|
description: "Volume",
|
|
type: OptionType.SLIDER,
|
|
markers: makeRange(0, 1, 0.1),
|
|
default: 0.5,
|
|
stickToMarkers: false,
|
|
},
|
|
ignoreBlocked: {
|
|
description: "Ignore blocked users",
|
|
type: OptionType.BOOLEAN,
|
|
default: true,
|
|
},
|
|
});
|
|
|
|
export default definePlugin({
|
|
name: "Sound Trigger",
|
|
description: "Configure trigger phrases (powered by regex) to play sounds.",
|
|
authors: [
|
|
{
|
|
id: 172557961133162496n,
|
|
name: "Rose (rosasaur)",
|
|
},
|
|
],
|
|
settings,
|
|
async start() {
|
|
triggerRules = (await DataStore.get(TRIGGER_RULES_KEY)) ?? triggerRules;
|
|
},
|
|
flux: {
|
|
async RPC_NOTIFICATION_CREATE({ type, message }: IRPCNotificationCreate) {
|
|
if (type !== "RPC_NOTIFICATION_CREATE") return;
|
|
if (message.state === "SENDING") return;
|
|
if (
|
|
settings.store.ignoreBlocked &&
|
|
RelationshipStore.isBlocked(message.author?.id)
|
|
)
|
|
return;
|
|
if (!message.content) return;
|
|
|
|
await handleMessage(message);
|
|
},
|
|
async MESSAGE_CREATE({
|
|
optimistic,
|
|
type,
|
|
channelId,
|
|
message,
|
|
}: IMessageCreate) {
|
|
if (optimistic || type !== "MESSAGE_CREATE") return;
|
|
if (message.state === "SENDING") return;
|
|
if (
|
|
settings.store.ignoreBlocked &&
|
|
RelationshipStore.isBlocked(message.author?.id)
|
|
)
|
|
return;
|
|
if (!message.content) return;
|
|
if (channelId !== SelectedChannelStore.getChannelId()) return;
|
|
|
|
await handleMessage(message);
|
|
},
|
|
},
|
|
});
|
|
|
|
async function handleMessage(message: Message) {
|
|
let queue = [] as TriggerRule[];
|
|
|
|
for (const rule of triggerRules) {
|
|
if (!rule.trigger) continue;
|
|
const regex = new RegExp(rule.trigger, "g");
|
|
const count = (message.content.match(regex) ?? []).length;
|
|
queue = [...queue, ...Array(count).fill(rule)];
|
|
}
|
|
|
|
for (const item of queue) {
|
|
await playSound(item.sound);
|
|
await sleep(settings.store.delay);
|
|
}
|
|
}
|
|
|
|
function playSound(sound: string) {
|
|
return new Promise<void>(res => {
|
|
const audio = new Audio();
|
|
audio.src = sound;
|
|
audio.volume = settings.store.volume;
|
|
audio.addEventListener("ended", () => res());
|
|
audio.play();
|
|
});
|
|
}
|