diff --git a/src/plugins/relationshipNotifier/events.ts b/src/plugins/relationshipNotifier/events.ts new file mode 100644 index 00000000..1600484f --- /dev/null +++ b/src/plugins/relationshipNotifier/events.ts @@ -0,0 +1,40 @@ +/* + * 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 . +*/ + +import { FluxEvents } from "@webpack/types"; + +import { onChannelDelete, onGuildDelete, onRelationshipRemove } from "./functions"; +import { syncFriends, syncGroups, syncGuilds } from "./utils"; + +export const FluxHandlers: Partial void>>> = { + GUILD_CREATE: [syncGuilds], + GUILD_DELETE: [onGuildDelete], + CHANNEL_CREATE: [syncGroups], + CHANNEL_DELETE: [onChannelDelete], + RELATIONSHIP_ADD: [syncFriends], + RELATIONSHIP_UPDATE: [syncFriends], + RELATIONSHIP_REMOVE: [syncFriends, onRelationshipRemove] +}; + +export function forEachEvent(fn: (event: FluxEvents, handler: (data: any) => void) => void) { + for (const event in FluxHandlers) { + for (const cb of FluxHandlers[event]) { + fn(event as FluxEvents, cb); + } + } +} diff --git a/src/plugins/relationshipNotifier/functions.ts b/src/plugins/relationshipNotifier/functions.ts new file mode 100644 index 00000000..c9ec6e3a --- /dev/null +++ b/src/plugins/relationshipNotifier/functions.ts @@ -0,0 +1,87 @@ +/* + * 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 . +*/ + +import { UserUtils } from "@webpack/common"; + +import settings from "./settings"; +import { ChannelDelete, ChannelType, GuildDelete, RelationshipRemove, RelationshipType } from "./types"; +import { deleteGroup, deleteGuild, getGroup, getGuild, notify } from "./utils"; + +let manuallyRemovedFriend: string | undefined; +let manuallyRemovedGuild: string | undefined; +let manuallyRemovedGroup: string | undefined; + +export const removeFriend = (id: string) => manuallyRemovedFriend = id; +export const removeGuild = (id: string) => manuallyRemovedGuild = id; +export const removeGroup = (id: string) => manuallyRemovedGroup = id; + +export async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) { + if (manuallyRemovedFriend === id) { + manuallyRemovedFriend = undefined; + return; + } + + const user = await UserUtils.fetchUser(id) + .catch(() => null); + if (!user) return; + + switch (type) { + case RelationshipType.FRIEND: + if (settings.store.friends) + notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false)); + break; + case RelationshipType.FRIEND_REQUEST: + if (settings.store.friendRequestCancels) + notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false)); + break; + } +} + +export function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) { + if (!settings.store.servers) return; + if (unavailable) return; + + if (manuallyRemovedGuild === id) { + deleteGuild(id); + manuallyRemovedGuild = undefined; + return; + } + + const guild = getGuild(id); + if (guild) { + deleteGuild(id); + notify(`You were removed from the server ${guild.name}.`, guild.iconURL); + } +} + +export function onChannelDelete({ channel: { id, type } }: ChannelDelete) { + if (!settings.store.groups) return; + if (type !== ChannelType.GROUP_DM) return; + + if (manuallyRemovedGroup === id) { + deleteGroup(id); + manuallyRemovedGroup = undefined; + return; + } + + const group = getGroup(id); + if (group) { + deleteGroup(id); + notify(`You were removed from the group ${group.name}.`, group.iconURL); + } +} diff --git a/src/plugins/relationshipNotifier/index.ts b/src/plugins/relationshipNotifier/index.ts new file mode 100644 index 00000000..fb91ca37 --- /dev/null +++ b/src/plugins/relationshipNotifier/index.ts @@ -0,0 +1,70 @@ +/* + * 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 . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { FluxDispatcher } from "@webpack/common"; + +import { forEachEvent } from "./events"; +import { removeFriend, removeGroup, removeGuild } from "./functions"; +import settings from "./settings"; +import { syncAndRunChecks } from "./utils"; + +export default definePlugin({ + name: "RelationshipNotifier", + description: "Notifies you when a friend, group chat, or server removes you.", + authors: [Devs.nick], + settings, + + patches: [ + { + find: "removeRelationship:function(", + replacement: { + match: /(removeRelationship:function\((\i),\i,\i\){)/, + replace: "$1$self.removeFriend($2);" + } + }, + { + find: "leaveGuild:function(", + replacement: { + match: /(leaveGuild:function\((\i)\){)/, + replace: "$1$self.removeGuild($2);" + } + }, + { + find: "closePrivateChannel:function(", + replacement: { + match: /(closePrivateChannel:function\((\i)\){)/, + replace: "$1$self.removeGroup($2);" + } + } + ], + + async start() { + await syncAndRunChecks(); + forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb)); + }, + + stop() { + forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb)); + }, + + removeFriend, + removeGroup, + removeGuild +}); diff --git a/src/plugins/relationshipNotifier/settings.ts b/src/plugins/relationshipNotifier/settings.ts new file mode 100644 index 00000000..1ed36ea7 --- /dev/null +++ b/src/plugins/relationshipNotifier/settings.ts @@ -0,0 +1,53 @@ +/* + * 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 . +*/ + +import { definePluginSettings } from "@api/settings"; +import { OptionType } from "@utils/types"; + +export default definePluginSettings({ + notices: { + type: OptionType.BOOLEAN, + description: "Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).", + default: false + }, + offlineRemovals: { + type: OptionType.BOOLEAN, + description: "Notify you when starting discord if you were removed while offline.", + default: true + }, + friends: { + type: OptionType.BOOLEAN, + description: "Notify when a friend removes you", + default: true + }, + friendRequestCancels: { + type: OptionType.BOOLEAN, + description: "Notify when a friend request is cancelled", + default: true + }, + servers: { + type: OptionType.BOOLEAN, + description: "Notify when removed from a server", + default: true + }, + groups: { + type: OptionType.BOOLEAN, + description: "Notify when removed from a group chat", + default: true + } +}); diff --git a/src/plugins/relationshipNotifier/types.ts b/src/plugins/relationshipNotifier/types.ts new file mode 100644 index 00000000..d49413ab --- /dev/null +++ b/src/plugins/relationshipNotifier/types.ts @@ -0,0 +1,62 @@ +/* + * 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 . +*/ + +import { Channel } from "discord-types/general"; + +export interface ChannelDelete { + type: "CHANNEL_DELETE"; + channel: Channel; +} + +export interface GuildDelete { + type: "GUILD_DELETE"; + guild: { + id: string; + unavailable?: boolean; + }; +} + +export interface RelationshipRemove { + type: "RELATIONSHIP_REMOVE"; + relationship: { + id: string; + nickname: string; + type: number; + }; +} + +export interface SimpleGroupChannel { + id: string; + name: string; + iconURL?: string; +} + +export interface SimpleGuild { + id: string; + name: string; + iconURL?: string; +} + +export const enum ChannelType { + GROUP_DM = 3, +} + +export const enum RelationshipType { + FRIEND = 1, + FRIEND_REQUEST = 3, +} diff --git a/src/plugins/relationshipNotifier/utils.ts b/src/plugins/relationshipNotifier/utils.ts new file mode 100644 index 00000000..09a329a3 --- /dev/null +++ b/src/plugins/relationshipNotifier/utils.ts @@ -0,0 +1,149 @@ +/* + * 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 . +*/ + +import { DataStore, Notices } from "@api/index"; +import { showNotification } from "@api/Notifications"; +import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common"; + +import settings from "./settings"; +import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types"; + +const guilds = new Map(); +const groups = new Map(); +const friends = { + friends: [] as string[], + requests: [] as string[] +}; + +export async function syncAndRunChecks() { + const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([ + "relationship-notifier-guilds", + "relationship-notifier-groups", + "relationship-notifier-friends" + ]) as [Map | undefined, Map | undefined, Record<"friends" | "requests", string[]> | undefined]; + + await Promise.all([syncGuilds(), syncGroups(), syncFriends()]); + + if (settings.store.offlineRemovals) { + if (settings.store.groups && oldGroups?.size) { + for (const [id, group] of oldGroups) { + if (!groups.has(id)) + notify(`You are no longer in the group ${group.name}.`, group.iconURL); + } + } + + if (settings.store.servers && oldGuilds?.size) { + for (const [id, guild] of oldGuilds) { + if (!guilds.has(id)) + notify(`You are no longer in the server ${guild.name}.`, guild.iconURL); + } + } + + if (settings.store.friends && oldFriends?.friends.length) { + for (const id of oldFriends.friends) { + if (friends.friends.includes(id)) continue; + + const user = await UserUtils.fetchUser(id).catch(() => void 0); + if (user) + notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false)); + } + } + + if (settings.store.friendRequestCancels && oldFriends?.requests?.length) { + for (const id of oldFriends.requests) { + if (friends.requests.includes(id)) continue; + + const user = await UserUtils.fetchUser(id).catch(() => void 0); + if (user) + notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false)); + } + } + } +} + +export function notify(text: string, icon?: string) { + if (settings.store.notices) + Notices.showNotice(text, "OK", () => Notices.popNotice()); + + showNotification({ + title: "Relationship Notifier", + body: text, + icon + }); +} + +export function getGuild(id: string) { + return guilds.get(id); +} + +export function deleteGuild(id: string) { + guilds.delete(id); + syncGuilds(); +} + +export async function syncGuilds() { + for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) { + guilds.set(id, { + id, + name, + iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png` + }); + } + await DataStore.set("relationship-notifier-guilds", guilds); +} + +export function getGroup(id: string) { + return groups.get(id); +} + +export function deleteGroup(id: string) { + groups.delete(id); + syncGroups(); +} + +export async function syncGroups() { + for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) { + if (type === ChannelType.GROUP_DM) + groups.set(id, { + id, + name: name || rawRecipients.map(r => r.username).join(", "), + iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png` + }); + } + + await DataStore.set("relationship-notifier-groups", groups); +} + +export async function syncFriends() { + friends.friends = []; + friends.requests = []; + + const relationShips = RelationshipStore.getRelationships(); + for (const id in relationShips) { + switch (relationShips[id]) { + case RelationshipType.FRIEND: + friends.friends.push(id); + break; + case RelationshipType.FRIEND_REQUEST: + friends.requests.push(id); + break; + } + } + + await DataStore.set("relationship-notifier-friends", friends); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c6277f55..473674af 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -194,6 +194,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Captain", id: 347366054806159360n }, + nick: { + name: "nick", + id: 347884694408265729n + }, whqwert: { name: "whqwert", id: 586239091520176128n