diff --git a/src/plugins/implicitRelationships/README.md b/src/plugins/implicitRelationships/README.md new file mode 100644 index 000000000..a76e298f7 --- /dev/null +++ b/src/plugins/implicitRelationships/README.md @@ -0,0 +1,7 @@ +# ImplicitRelationships + +Shows your implicit relationships in the Friends tab. + +Implicit relationships on Discord are people with whom you've frecently interacted and share a mutual server; even though Discord thinks you should be friends with them, you haven't added them as friends. + +![](https://camo.githubusercontent.com/6927161ee0c933f7ef6d61f243cca3e6ea4c8db9d1becd8cbf73c45e1bd0d127/68747470733a2f2f692e646f6c66692e65732f7055447859464662674d2e706e673f6b65793d736e3950343936416c32444c7072) diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts new file mode 100644 index 000000000..9ae9fb512 --- /dev/null +++ b/src/plugins/implicitRelationships/index.ts @@ -0,0 +1,182 @@ +/* + * 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 { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByProps, findStoreLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common"; +import { Settings } from "Vencord"; + +const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore"); + +interface UserAffinity { + user_id: string; + affinity: number; +} + +export default definePlugin({ + name: "ImplicitRelationships", + description: "Shows your implicit relationships in the Friends tab.", + authors: [Devs.Dolfies], + patches: [ + // Counts header + { + find: ".FRIENDS_ALL_HEADER", + replacement: { + match: /toString\(\)\}\);case (\i\.\i)\.BLOCKED/, + replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED' + }, + }, + // No friends page + { + find: "FriendsEmptyState: Invalid empty state", + replacement: { + match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/, + replace: "case $1.ONLINE:case $1.IMPLICIT:" + }, + }, + // Sections header + { + find: ".FRIENDS_SECTION_ONLINE", + replacement: { + match: /(\(0,\i\.jsx\)\(\i\.TabBar\.Item,\{id:\i\.\i)\.BLOCKED,className:([^\s]+?)\.item,children:\i\.\i\.Messages\.BLOCKED\}\)/, + replace: "$1.IMPLICIT,className:$2.item,children:\"Implicit\"}),$&" + }, + }, + // Sections content + { + find: '"FriendsStore"', + replacement: { + match: /(?<=case (\i\.\i)\.BLOCKED:return (\i)\.type===\i\.\i\.BLOCKED)/, + replace: ";case $1.IMPLICIT:return $2.type===5" + }, + }, + // Piggyback relationship fetch + { + find: ".fetchRelationships()", + replacement: { + match: /(\i\.\i)\.fetchRelationships\(\)/, + // This relationship fetch is actually completely useless, but whatevs + replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()" + }, + }, + // Modify sort -- thanks megu for the patch (from sortFriendRequests) + { + find: "getRelationshipCounts(){", + replacement: { + predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity, + match: /\.sortBy\(\i=>\i\.comparator\)/, + replace: "$&.sortBy((row) => $self.sortList(row))" + } + }, + + // Add support for the nonce parameter to Discord's shitcode + { + find: ".REQUEST_GUILD_MEMBERS", + replacement: { + match: /\.send\(8,{/, + replace: "$&nonce:arguments[1].nonce," + } + }, + { + find: "GUILD_MEMBERS_REQUEST:", + replacement: { + match: /presences:!!(\i)\.presences/, + replace: "$&,nonce:$1.nonce" + }, + }, + { + find: ".not_found", + replacement: { + match: /notFound:(\i)\.not_found/, + replace: "$&,nonce:$1.nonce" + }, + } + ], + settings: definePluginSettings( + { + sortByAffinity: { + type: OptionType.BOOLEAN, + default: true, + description: "Whether to sort implicit relationships by their affinity to you.", + restartNeeded: true + }, + } + ), + + sortList(row: any) { + return row.type === 5 + ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0 + : row.comparator; + }, + + async fetchImplicitRelationships() { + // Implicit relationships are defined as users that you: + // 1. Have an affinity for + // 2. Do not have a relationship with // TODO: Check how this works with pending/blocked relationships + // 3. Have a mutual guild with + const userAffinities: Set = UserAffinitiesStore.getUserAffinitiesUserIds(); + const nonFriendAffinities = Array.from(userAffinities).filter( + id => !RelationshipStore.getRelationshipType(id) + ); + + // I would love to just check user cache here (falling back to the gateway of course) + // However, users in user cache may just be there because they share a DM or group DM with you + // So there's no guarantee that a user being in user cache means they have a mutual with you + // To get around this, we request users we have DMs with, and ignore them below if we don't get them back + const dmUserIds = new Set( + Object.values(ChannelStore.getSortedPrivateChannels()).flatMap(c => c.recipients) + ); + const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id) || dmUserIds.has(id)); + const allGuildIds = Object.keys(GuildStore.getGuilds()); + const sentNonce = SnowflakeUtils.fromTimestamp(Date.now()); + let count = allGuildIds.length * Math.ceil(toRequest.length / 100); + + // OP 8 Request Guild Members allows 100 user IDs at a time + const ignore = new Set(toRequest); + const relationships = RelationshipStore.getRelationships(); + const callback = ({ nonce, members }) => { + if (nonce !== sentNonce) return; + members.forEach(member => { + ignore.delete(member.user.id); + }); + + nonFriendAffinities.map(id => UserStore.getUser(id)).filter(user => user && !ignore.has(user.id)).forEach(user => relationships[user.id] = 5); + RelationshipStore.emitChange(); + if (--count === 0) { + FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK", callback); + } + }; + + FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK", callback); + for (let i = 0; i < toRequest.length; i += 100) { + FluxDispatcher.dispatch({ + type: "GUILD_MEMBERS_REQUEST", + guildIds: allGuildIds, + userIds: toRequest.slice(i, i + 100), + nonce: sentNonce, + }); + } + }, + + start() { + const { FriendsSections } = findByProps("FriendsSections"); + FriendsSections.IMPLICIT = "IMPLICIT"; + } +}); diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index c40a18140..32579a803 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -42,7 +42,7 @@ export default definePlugin({ find: "getRelationshipCounts(){", replacement: { match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: ".sortBy((row) => $self.sortList(row))" + replace: "$&.sortBy((row) => $self.sortList(row))" } }, { find: ".Messages.FRIEND_REQUEST_CANCEL",