Plugin

ImplicitRelationships

Shows your implicit relationships in the Friends tab.

Friends Servers
index.ts
Download

Source

src/plugins/implicitRelationships/index.ts
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { definePluginSettings } from "@api/Settings";
8import { Devs } from "@utils/constants";
9import { Logger } from "@utils/Logger";
10import definePlugin, { OptionType } from "@utils/types";
11import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserAffinitiesStore, UserStore } from "@webpack/common";
12
13const settings = definePluginSettings(
14 {
15 sortByAffinity: {
16 type: OptionType.BOOLEAN,
17 default: true,
18 description: "Whether to sort implicit relationships by their affinity to you.",
19 restartNeeded: true
20 },
21 }
22);
23
24export default definePlugin({
25 name: "ImplicitRelationships",
26 description: "Shows your implicit relationships in the Friends tab.",
27 tags: ["Friends", "Servers"],
28 authors: [Devs.Dolfies],
29 settings,
30
31 patches: [
32 // Counts header
33 {
34 find: "#{intl::FRIENDS_ALL_HEADER}",
35 replacement: {
36 match: /toString\(\)\}\);case (\i\.\i)\.PENDING/,
37 replace: 'toString()});case $1.IMPLICIT:return "Implicit — "+arguments[1];case $1.BLOCKED'
38 },
39 },
40 // No friends page
41 {
42 find: "FriendsEmptyState: Invalid empty state",
43 replacement: {
44 match: /case (\i\.\i)\.ONLINE:(?=return (\i)\.SECTION_ONLINE)/,
45 replace: "case $1.ONLINE:case $1.IMPLICIT:"
46 },
47 },
48 // Sections header
49 {
50 find: "#{intl::FRIENDS_SECTION_ONLINE}),className:",
51 replacement: {
52 match: /,{id:(\i\.\i)\.PENDING,show:.+?className:(\i\.\i)(?=\},\{id:)/,
53 replace: (rest, relationShipTypes, className) => `,{id:${relationShipTypes}.IMPLICIT,show:true,className:${className},content:"Implicit"}${rest}`
54 }
55 },
56 // Sections content
57 {
58 find: '"FriendsStore"',
59 replacement: {
60 match: /(?<=case (\i\.\i)\.SUGGESTIONS:return \d+===(\i)\.type)/,
61 replace: ";case $1.IMPLICIT:return $2.type===5"
62 },
63 },
64 // Piggyback relationship fetch
65 {
66 find: &#039;"FriendsStore&#039;,
67 replacement: {
68 match: /(\i\.\i)\.fetchRelationships\(\)/,
69 // This relationship fetch is actually completely useless, but whatevs
70 replace: "$1.fetchRelationships(),$self.fetchImplicitRelationships()"
71 },
72 },
73 // Modify sort -- thanks megu for the patch (from sortFriendRequests)
74 {
75 find: "getRelationshipCounts(){",
76 replacement: {
77 predicate: () => settings.store.sortByAffinity,
78 match: /\}\)\.sortBy\((.+?)\)\.value\(\)/,
79 replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()"
80 }
81 },
82
83 // Add support for the nonce parameter to Discord's shitcode
84 {
85 find: ".REQUEST_GUILD_MEMBERS,",
86 replacement: {
87 match: /\.REQUEST_GUILD_MEMBERS,{/,
88 replace: "$&nonce:arguments[1]?.nonce,"
89 }
90 },
91 {
92 find: "GUILD_MEMBERS_REQUEST:",
93 replacement: {
94 match: /presences:!!(\i)\.presences/,
95 replace: "$&,nonce:$1.nonce"
96 },
97 },
98 {
99 find: ".not_found",
100 replacement: {
101 match: /notFound:(\i)\.not_found/,
102 replace: "$&,nonce:$1.nonce"
103 },
104 }
105 ],
106
107 wrapSort(comparator: Function, row: any) {
108 return row.type === 5
109 ? (UserAffinitiesStore.getUserAffinity(row.user.id)?.communicationRank ?? 0)
110 : comparator(row);
111 },
112
113 async fetchImplicitRelationships() {
114 // Implicit relationships are defined as users that you:
115 // 1. Have an affinity for
116 // 2. Do not have a relationship with
117 const userAffinities: Record<string, any>[] = UserAffinitiesStore.getUserAffinities();
118 const relationships = RelationshipStore.getMutableRelationships();
119 const nonFriendAffinities = userAffinities.filter(a => !RelationshipStore.getRelationshipType(a.otherUserId));
120 nonFriendAffinities.forEach(a => {
121 relationships.set(a.otherUserId, 5);
122 });
123 RelationshipStore.emitChange();
124
125 const toRequest = nonFriendAffinities.filter(a => !UserStore.getUser(a.otherUserId));
126 const allGuildIds = Object.keys(GuildStore.getGuilds());
127 const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
128 let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
129
130 // OP 8 Request Guild Members allows 100 user IDs at a time
131 // Note: As we are using OP 8 here, implicit relationships who we do not share a guild
132 // with will not be fetched; so, if they're not otherwise cached, they will not be shown
133 // This should not be a big deal as these should be rare
134 const callback = ({ chunks }) => {
135 try {
136 const chunkCount = chunks.filter(chunk => chunk.nonce === sentNonce).length;
137 if (chunkCount === 0) return;
138
139 count -= chunkCount;
140 RelationshipStore.emitChange();
141 if (count <= 0) {
142 FluxDispatcher.unsubscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
143 }
144 } catch (e) {
145 new Logger("ImplicitRelationships").error("Error in GUILD_MEMBERS_CHUNK_BATCH handler", e);
146 }
147 };
148
149 FluxDispatcher.subscribe("GUILD_MEMBERS_CHUNK_BATCH", callback);
150 for (let i = 0; i < toRequest.length; i += 100) {
151 FluxDispatcher.dispatch({
152 type: "GUILD_MEMBERS_REQUEST",
153 guildIds: allGuildIds,
154 userIds: toRequest.slice(i, i + 100),
155 presences: true,
156 nonce: sentNonce,
157 });
158 }
159 },
160
161 start() {
162 Constants.FriendsSections.IMPLICIT = "IMPLICIT";
163 }
164});
165