diff --git a/src/plugins/iLoveSpam/index.ts b/src/plugins/iLoveSpam/index.ts index 8556de426..bb0b20539 100644 --- a/src/plugins/iLoveSpam/index.ts +++ b/src/plugins/iLoveSpam/index.ts @@ -22,7 +22,7 @@ import definePlugin from "@utils/types"; export default definePlugin({ name: "iLoveSpam", description: "Do not hide messages from 'likely spammers'", - authors: [Devs.botato, Devs.Animal], + authors: [Devs.botato, Devs.Nyako], patches: [ { find: "hasFlag:{writable", diff --git a/src/plugins/xsOverlay.desktop/README.md b/src/plugins/xsOverlay.desktop/README.md new file mode 100644 index 000000000..477e30bf3 --- /dev/null +++ b/src/plugins/xsOverlay.desktop/README.md @@ -0,0 +1,15 @@ +# XSOverlay Notifier + +Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR. + +## Preview + +![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40) + +![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2) + +## Usage +- Enable this plugin +- Set plugin settings as desired +- Open XSOverlay +- get ping spammed diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts new file mode 100644 index 000000000..5461696ec --- /dev/null +++ b/src/plugins/xsOverlay.desktop/index.ts @@ -0,0 +1,288 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { makeRange } from "@components/PluginSettings/components"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin, { OptionType, PluginNative } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; +import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; + +const enum ChannelTypes { + DM = 1, + GROUP_DM = 3 +} + +interface Message { + guild_id: string, + attachments: MessageAttachment[], + author: User, + channel_id: string, + components: any[], + content: string, + edited_timestamp: string, + embeds: Embed[], + sticker_items?: Sticker[], + flags: number, + id: string, + member: GuildMember, + mention_everyone: boolean, + mention_roles: string[], + mentions: Mention[], + nonce: string, + pinned: false, + referenced_message: any, + timestamp: string, + tts: boolean, + type: number; +} + +interface Mention { + avatar: string, + avatar_decoration_data: any, + discriminator: string, + global_name: string, + id: string, + public_flags: number, + username: string; +} + +interface Sticker { + t: "Sticker"; + description: string; + format_type: number; + guild_id: string; + id: string; + name: string; + tags: string; + type: number; +} + +interface Call { + channel_id: string, + guild_id: string, + message_id: string, + region: string, + ringing: string[]; +} + +const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled"); +const XSLog = new Logger("XSOverlay"); + +const settings = definePluginSettings({ + ignoreBots: { + type: OptionType.BOOLEAN, + description: "Ignore messages from bots", + default: false + }, + pingColor: { + type: OptionType.STRING, + description: "User mention color", + default: "#7289da" + }, + channelPingColor: { + type: OptionType.STRING, + description: "Channel mention color", + default: "#8a2be2" + }, + soundPath: { + type: OptionType.STRING, + description: "Notification sound (default/warning/error)", + default: "default" + }, + timeout: { + type: OptionType.NUMBER, + description: "Notif duration (secs)", + default: 1.0, + }, + opacity: { + type: OptionType.SLIDER, + description: "Notif opacity", + default: 1, + markers: makeRange(0, 1, 0.1) + }, + volume: { + type: OptionType.SLIDER, + description: "Volume", + default: 0.2, + markers: makeRange(0, 1, 0.1) + }, +}); + +const Native = VencordNative.pluginHelpers.XsOverlay as PluginNative; + +export default definePlugin({ + name: "XSOverlay", + description: "Forwards discord notifications to XSOverlay, for easy viewing in VR", + authors: [Devs.Nyako], + tags: ["vr", "notify"], + settings, + flux: { + CALL_UPDATE({ call }: { call: Call; }) { + if (call?.ringing?.includes(UserStore.getCurrentUser().id)) { + const channel = ChannelStore.getChannel(call.channel_id); + sendOtherNotif("Incoming call", `${channel.name} is calling you...`); + } + }, + MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) { + // Apparently without this try/catch, discord's socket connection dies if any part of this errors + try { + if (optimistic) return; + const channel = ChannelStore.getChannel(message.channel_id); + if (!shouldNotify(message, channel)) return; + + const pingColor = settings.store.pingColor.replaceAll("#", "").trim(); + const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim(); + let finalMsg = message.content; + let titleString = ""; + + if (channel.guild_id) { + const guild = GuildStore.getGuild(channel.guild_id); + titleString = `${message.author.username} (${guild.name}, #${channel.name})`; + } + + + switch (channel.type) { + case ChannelTypes.DM: + titleString = message.author.username.trim(); + break; + case ChannelTypes.GROUP_DM: + const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(", "); + titleString = `${message.author.username} (${channelName})`; + break; + } + + if (message.referenced_message) { + titleString += " (reply)"; + } + + if (message.embeds.length > 0) { + finalMsg += " [embed] "; + if (message.content === "") { + finalMsg = "sent message embed(s)"; + } + } + + if (message.sticker_items) { + finalMsg += " [sticker] "; + if (message.content === "") { + finalMsg = "sent a sticker"; + } + } + + const images = message.attachments.filter(e => + typeof e?.content_type === "string" + && e?.content_type.startsWith("image") + ); + + + images.forEach(img => { + finalMsg += ` [image: ${img.filename}] `; + }); + + message.attachments.filter(a => a && !a.content_type?.startsWith("image")).forEach(a => { + finalMsg += ` [attachment: ${a.filename}] `; + }); + + // make mentions readable + if (message.mentions.length > 0) { + finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `@${UserStore.getUser(id)?.username || "unknown-user"}`); + } + + if (message.mention_roles.length > 0) { + for (const roleId of message.mention_roles) { + const role = GuildStore.getGuild(channel.guild_id).roles[roleId]; + if (!role) continue; + const roleColor = role.colorString ?? `#${pingColor}`; + finalMsg = finalMsg.replace(`<@&${roleId}>`, `@${role.name}`); + } + } + + // make emotes and channel mentions readable + const emoteMatches = finalMsg.match(new RegExp("()", "g")); + const channelMatches = finalMsg.match(new RegExp("<(#\\d+)>", "g")); + + if (emoteMatches) { + for (const eMatch of emoteMatches) { + finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, "g"), `:${eMatch.split(":")[1]}:`); + } + } + + if (channelMatches) { + for (const cMatch of channelMatches) { + let channelId = cMatch.split("<#")[1]; + channelId = channelId.substring(0, channelId.length - 1); + finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, "g"), `#${ChannelStore.getChannel(channelId).name}`); + } + } + + sendMsgNotif(titleString, finalMsg, message); + } catch (err) { + XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`); + } + } + } +}); + +function sendMsgNotif(titleString: string, content: string, message: Message) { + fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => { + const msgData = { + messageType: 1, + index: 0, + timeout: settings.store.timeout, + height: calculateHeight(cleanMessage(content)), + opacity: settings.store.opacity, + volume: settings.store.volume, + audioPath: settings.store.soundPath, + title: titleString, + content: content, + useBase64Icon: true, + icon: result, + sourceApp: "Vencord" + }; + Native.sendToOverlay(msgData); + }); +} + +function sendOtherNotif(content: string, titleString: string) { + const msgData = { + messageType: 1, + index: 0, + timeout: settings.store.timeout, + height: calculateHeight(cleanMessage(content)), + opacity: settings.store.opacity, + volume: settings.store.volume, + audioPath: settings.store.soundPath, + title: titleString, + content: content, + useBase64Icon: false, + icon: null, + sourceApp: "Vencord" + }; + Native.sendToOverlay(msgData); +} + +function shouldNotify(message: Message, channel: Channel) { + const currentUser = UserStore.getCurrentUser(); + if (message.author.id === currentUser.id) return false; + if (message.author.bot && settings.store.ignoreBots) return false; + if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true; + + return message.mentions.some(m => m.id === currentUser.id); +} + +function calculateHeight(content: string) { + if (content.length <= 100) return 100; + if (content.length <= 200) return 150; + if (content.length <= 300) return 200; + return 250; +} + +function cleanMessage(content: string) { + return content.replace(new RegExp("<[^>]*>", "g"), ""); +} diff --git a/src/plugins/xsOverlay.desktop/native.ts b/src/plugins/xsOverlay.desktop/native.ts new file mode 100644 index 000000000..82809383a --- /dev/null +++ b/src/plugins/xsOverlay.desktop/native.ts @@ -0,0 +1,16 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { createSocket, Socket } from "dgram"; + +let xsoSocket: Socket; + +export function sendToOverlay(_, data: any) { + data.icon = Buffer.from(data.icon).toString("base64"); + const json = JSON.stringify(data); + xsoSocket ??= createSocket("udp4"); + xsoSocket.send(json, 42069, "127.0.0.1"); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 0ff7da72a..daa4a74dc 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -78,8 +78,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Samu", id: 702973430449832038n, }, - Animal: { - name: "Animal", + Nyako: { + name: "nyako", id: 118437263754395652n }, MaiKokain: {