Plugin

ExportContacts

Export a list of friends to your clipboard. Adds a new button to the menu bar for the friends tab.

index.tsx
Download

Source

src/plugins/exportContacts/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./styles.css";
8
9import ErrorBoundary from "@components/ErrorBoundary";
10import { Devs } from "@utils/constants";
11import definePlugin from "@utils/types";
12import { Clipboard, Toasts } from "@webpack/common";
13
14interface User {
15 id: string;
16 username: string;
17 avatar: string;
18 discriminator: string;
19 publicFlags: number;
20 avatarDecorationData?: any;
21 globalName: string;
22}
23
24interface ContactsList {
25 id: string;
26 type: number;
27 nickname?: any;
28 user: User;
29 since: string;
30}
31
32// for type parameter, it takes in a number that determines the type of the contact
33// 1 is friends added
34// 2 is blocked users
35// 3 is incoming friend requests
36// 4 is outgoing friend requests
37function getUsernames(contacts: ContactsList[], type: number): string[] {
38 return contacts
39 // only select contacts that are the specified type
40 .filter(e => e.type === type)
41 // return the username, and discriminator if necessary
42 .map(e => e.user.discriminator === "0" ? e.user.username : e.user.username + "#" + e.user.discriminator);
43}
44
45export default definePlugin({
46 name: "ExportContacts",
47 description: "Export a list of friends to your clipboard. Adds a new button to the menu bar for the friends tab.",
48 authors: [Devs.FiveCord],
49 patches: [
50 {
51 find: "fetchRelationships(){",
52 replacement: {
53 match: /\.then\(e=>o\.default\.dispatch\({type:"LOAD_RELATIONSHIPS_SUCCESS",relationships:e\.body}\)/,
54 replace: ".then(e=>{o.default.dispatch({type:\"LOAD_RELATIONSHIPS_SUCCESS\",relationships:e.body}); $self.getContacts(e.body)}"
55 }
56 },
57 {
58 find: "[role=\"tab\"][aria-disabled=\"false\"]",
59 replacement: {
60 match: /this\.props;return\(/,
61 replace: "this.props;console.log(a.Children);return("
62 }
63 },
64 {
65 find: "[role=\"tab\"][aria-disabled=\"false\"]",
66 replacement: {
67 match: /(\w+)\.Children\.map\((\w+),\s*this\.renderChildren\)/,
68 replace: "[...$1.Children.map($2,this.renderChildren),$self.addExportButton()]"
69 }
70 }
71 ],
72
73 getContacts(contacts: ContactsList[]) {
74 this.contactList = {
75 friendsAdded: [...getUsernames(contacts, 1)],
76 blockedUsers: [...getUsernames(contacts, 2)],
77 incomingFriendRequests: [...getUsernames(contacts, 3)],
78 outgoingFriendRequests: [...getUsernames(contacts, 4)]
79 };
80 },
81
82 addExportButton() {
83 return <ErrorBoundary noop key=".2">
84 <button className="export-contacts-button" onClick={() => { this.copyContactToClipboard(); console.log("clicked"); }}>Export</button>
85 </ErrorBoundary>;
86 },
87
88 copyContactToClipboard() {
89 if (this.contactList) {
90 Clipboard.copy(JSON.stringify(this.contactList));
91 Toasts.show({
92 message: "Contacts copied to clipboard successfully.",
93 type: Toasts.Type.SUCCESS,
94 id: Toasts.genId(),
95 options: {
96 duration: 3000,
97 position: Toasts.Position.BOTTOM
98 }
99 });
100 return;
101 }
102 // reason why you need to click the all tab is because the data is extracted during
103 // the request itself when you fetch all your friends. this is done to avoid sending a
104 // manual request to discord, which may raise suspicion and might even get you terminated.
105 Toasts.show({
106 message: "Contact list is undefined. Click on the \"All\" tab before exporting.",
107 type: Toasts.Type.FAILURE,
108 id: Toasts.genId(),
109 options: {
110 duration: 3000,
111 position: Toasts.Position.BOTTOM
112 }
113 });
114 }
115});
116