mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-10 15:13:02 -04:00
Add Premid Plugin + RoleMembersViewer
This commit is contained in:
parent
d793f37b39
commit
f3f7696dca
6 changed files with 747 additions and 31 deletions
327
src/equicordplugins/premid/index.tsx
Normal file
327
src/equicordplugins/premid/index.tsx
Normal file
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* 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 { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
|
||||
|
||||
interface ActivityAssets {
|
||||
large_image: string;
|
||||
large_text?: string | null;
|
||||
small_image: string;
|
||||
small_text: string;
|
||||
}
|
||||
|
||||
type ActivityButton = {
|
||||
label: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface Activity {
|
||||
state: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
assets: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
name: string;
|
||||
application_id: string;
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: number;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
interface PremidActivity {
|
||||
state: string;
|
||||
details?: string;
|
||||
startTimestamp?: number;
|
||||
endTimestamp?: number;
|
||||
largeImageKey: string;
|
||||
largeImageText: string;
|
||||
smallImageKey: string;
|
||||
smallImageText: string;
|
||||
buttons?: ActivityButton[];
|
||||
name?: string;
|
||||
application_id: string;
|
||||
type: number;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
interface PresenceData {
|
||||
// Only relevant types - https://github.com/PreMiD/PreMiD/blob/main/%40types/PreMiD/PresenceData.d.ts
|
||||
clientId: string;
|
||||
presenceData: PremidActivity;
|
||||
}
|
||||
|
||||
const enum ActivityType {
|
||||
PLAYING = 0,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
COMPETING = 5
|
||||
}
|
||||
|
||||
const enum ActivityFlag {
|
||||
INSTANCE = 1 << 0
|
||||
}
|
||||
|
||||
interface PublicApp {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
statusType: ActivityType | undefined;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
const logger = new Logger("Vencord-PreMiD", "#8fd0ff");
|
||||
|
||||
const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application");
|
||||
|
||||
const apps: any = {};
|
||||
async function getApp(applicationId: string): Promise<PublicApp> {
|
||||
if (apps[applicationId]) return apps[applicationId];
|
||||
const socket: any = {};
|
||||
debugLog(`Looking up ${applicationId}`);
|
||||
await fetchApplicationsRPC(socket, applicationId);
|
||||
logger.debug(socket);
|
||||
debugLog(`Lookup finished for ${socket.application.name}`);
|
||||
const activityType = await determineStatusType(socket.application);
|
||||
debugLog(`Activity type for ${socket.application.name}: ${activityType}`);
|
||||
socket.application.statusType = settings.store.detectCategory ? activityType : ActivityType.PLAYING || ActivityType.PLAYING;
|
||||
apps[applicationId] = socket.application;
|
||||
return socket.application;
|
||||
}
|
||||
|
||||
const assetCache: Map<string, string> = new Map();
|
||||
// memoized because this method isnt cached
|
||||
async function getAppAsset(applicationId: string, key: string): Promise<string> {
|
||||
if (assetCache.has(applicationId + key)) {
|
||||
return assetCache.get(applicationId + key)!;
|
||||
}
|
||||
const result = (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0];
|
||||
assetCache.set(applicationId + key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function setActivity(activity: Activity | undefined) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity,
|
||||
socketId: "PreMiD",
|
||||
});
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enableSet: {
|
||||
description: "Should the plugin set presences?",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
onChange: (value: boolean) => {
|
||||
if (!value) preMid.clearActivity();
|
||||
},
|
||||
},
|
||||
showButtons: {
|
||||
description: "Show buttons",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
detectCategory: {
|
||||
description: "Set your Activity Type based on presence category",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
},
|
||||
hideViewChannel: {
|
||||
description: "YouTube: Hide view channel button",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const Native = VencordNative.pluginHelpers.PreMiD as PluginNative<typeof import("./native")>;
|
||||
|
||||
const preMid = definePlugin({
|
||||
name: "PreMiD",
|
||||
tags: ["presence", "premid", "rpc", "watching"],
|
||||
description: "A PreMiD app replacement. Supports watching/listening status. Requires extra setup (see settings)",
|
||||
authors: [Devs.Nyako],
|
||||
toolboxActions: {
|
||||
"Toggle presence sharing": () => {
|
||||
settings.store.enableSet = !settings.store.enableSet;
|
||||
showToast(`Presence sharing is now ${settings.store.enableSet ? "enabled" : "disabled"}`);
|
||||
preMid.clearActivity();
|
||||
},
|
||||
},
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
<Forms.FormTitle tag="h3">How to use this plugin</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
Install the <Link href="https://premid.app/downloads#ext-downloads">PreMiD browser extension</Link>. (recommended version: 2.5.2 OR 2.6.11+)
|
||||
</Forms.FormText>
|
||||
<Forms.FormText tag="h4">
|
||||
This will not work with anything that has differing behavior (such as PreWrap)
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
That's all you need, if you followed the instructions in this plugin's README you should be good. This plugin replicates their electron tray process so no need to use allat.
|
||||
</Forms.FormText>
|
||||
</>
|
||||
),
|
||||
|
||||
settings,
|
||||
logger,
|
||||
|
||||
start() {
|
||||
Native.init();
|
||||
},
|
||||
|
||||
stop() {
|
||||
this.clearActivity();
|
||||
Native.disconnect();
|
||||
},
|
||||
|
||||
clearActivity() {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: null,
|
||||
socketId: "PreMiD",
|
||||
});
|
||||
},
|
||||
|
||||
showToast,
|
||||
|
||||
async receiveActivity(data: PresenceData) {
|
||||
logger.debug("Received activity", data);
|
||||
if (!settings.store.enableSet) {
|
||||
this.clearActivity();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const id = data.clientId;
|
||||
if (!id) return;
|
||||
const appInfo = await getApp(id);
|
||||
const presence = { ...data.presenceData };
|
||||
if (appInfo.name === "PreMiD") return;
|
||||
logger.debug(`Setting activity of ${appInfo.name} "${presence.details}"`);
|
||||
|
||||
const { details, state, largeImageKey, smallImageKey, smallImageText } = presence;
|
||||
const activity: Activity = {
|
||||
application_id: id,
|
||||
name: appInfo.name,
|
||||
details: details ?? "",
|
||||
state: state ?? "",
|
||||
type: appInfo.statusType || ActivityType.PLAYING,
|
||||
flags: ActivityFlag.INSTANCE,
|
||||
assets: {
|
||||
large_image: await getAppAsset(id, largeImageKey ?? "oops"),
|
||||
small_image: await getAppAsset(id, smallImageKey ?? "oops"),
|
||||
small_text: smallImageText || "hello there :3",
|
||||
},
|
||||
buttons: presence.buttons?.map((b: { label: any; }) => b.label),
|
||||
metadata: {
|
||||
button_urls: presence.buttons?.map((b: { url: any; }) => b.url)
|
||||
},
|
||||
timestamps: {
|
||||
start: presence.startTimestamp,
|
||||
end: presence.endTimestamp
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (activity.type === ActivityType.PLAYING) {
|
||||
activity.assets = {
|
||||
large_image: await getAppAsset(id, largeImageKey ?? "guh"),
|
||||
large_text: "vc-premid",
|
||||
small_image: await getAppAsset(id, smallImageKey ?? "guhh"),
|
||||
small_text: smallImageText || "hello there :3",
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.store.showButtons && activity.buttons) {
|
||||
if (appInfo.name === "YouTube" && settings.store.hideViewChannel) {
|
||||
activity.buttons?.pop();
|
||||
if (activity.metadata && activity.metadata && activity.metadata.button_urls) {
|
||||
activity.metadata.button_urls = [activity.metadata.button_urls[0]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const k in activity) {
|
||||
if (k === "type") continue; // without type, the presence is considered invalid.
|
||||
const v = activity[k];
|
||||
if (!v || v.length === 0)
|
||||
delete activity[k];
|
||||
}
|
||||
|
||||
|
||||
setActivity(activity);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function determineStatusType(info: PublicApp): Promise<ActivityType | undefined> {
|
||||
let firstCharacter = info.name.charAt(0);
|
||||
if (firstCharacter.match(/[a-zA-Z]/)) {
|
||||
firstCharacter = firstCharacter;
|
||||
} else if (firstCharacter.match(/[0-9]/)) {
|
||||
firstCharacter = "0-9";
|
||||
} else {
|
||||
firstCharacter = "%23"; // #
|
||||
}
|
||||
|
||||
const res = await fetch(`https://raw.githubusercontent.com/PreMiD/Presences/main/websites/${firstCharacter}/${info.name}/metadata.json`);
|
||||
if (!res.ok) return ActivityType.PLAYING;
|
||||
|
||||
try {
|
||||
const metadata = await res.json();
|
||||
switch (metadata.category) {
|
||||
case "socials":
|
||||
if (metadata.tags.includes("video")) {
|
||||
return ActivityType.WATCHING;
|
||||
}
|
||||
break;
|
||||
case "anime":
|
||||
if (metadata.tags.some((tag: string) => ["video", "media", "streaming"].includes(tag))) {
|
||||
return ActivityType.WATCHING;
|
||||
}
|
||||
break;
|
||||
case "music":
|
||||
return ActivityType.LISTENING;
|
||||
case "videos":
|
||||
return ActivityType.WATCHING;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return ActivityType.PLAYING;
|
||||
}
|
||||
return ActivityType.PLAYING;
|
||||
}
|
||||
|
||||
function debugLog(msg: string) {
|
||||
if (IS_DEV) console.log(msg);
|
||||
}
|
||||
|
||||
function showToast(msg: string) {
|
||||
Toasts.show({
|
||||
message: msg,
|
||||
type: Toasts.Type.SUCCESS,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
duration: 5000,
|
||||
position: Toasts.Position.TOP
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default preMid;
|
116
src/equicordplugins/premid/native.ts
Normal file
116
src/equicordplugins/premid/native.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { BrowserWindow, dialog, WebContents } from "electron";
|
||||
import { createServer, Server as HttpServer } from "http";
|
||||
import { Server, Socket } from "socket.io";
|
||||
|
||||
let io: Server;
|
||||
let httpServer: HttpServer;
|
||||
let hasInit = false;
|
||||
let webFrame: WebContents;
|
||||
|
||||
export function init() {
|
||||
if (hasInit) return;
|
||||
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
const discordUrls = ["https://discord.com", "https://ptb.discord.com", "https://canary.discord.com"];
|
||||
|
||||
for (const win of windows) {
|
||||
const url = win.webContents.getURL();
|
||||
if (discordUrls.some(prefix => url.startsWith(prefix))) {
|
||||
webFrame = win.webContents;
|
||||
}
|
||||
}
|
||||
|
||||
httpServer = createServer();
|
||||
|
||||
io = new Server(httpServer, {
|
||||
serveClient: false,
|
||||
allowEIO3: true,
|
||||
cors: { origin: "*" }
|
||||
});
|
||||
httpServer.listen(3020, () => {
|
||||
console.log("[vc-premid] SocketIO starting on 3020");
|
||||
logRenderer("SocketIO starting on 3020");
|
||||
});
|
||||
httpServer.on("error", onIOError);
|
||||
io.on("connection", onConnect);
|
||||
hasInit = true;
|
||||
}
|
||||
|
||||
export function disconnect() {
|
||||
if (!hasInit) return;
|
||||
io.close();
|
||||
httpServer.close();
|
||||
hasInit = false;
|
||||
}
|
||||
|
||||
async function onConnect(sio: Socket) {
|
||||
try {
|
||||
logRenderer("[vc-premid] PreMiD socket connected!");
|
||||
// Get current user from plugin & send to extension
|
||||
const {
|
||||
username,
|
||||
globalName,
|
||||
id,
|
||||
avatar,
|
||||
discriminator,
|
||||
flags,
|
||||
premiumType
|
||||
} = JSON.parse(await webFrame.executeJavaScript("JSON.stringify(window.Vencord.Webpack.Common.UserStore.getCurrentUser());"));
|
||||
sio.emit("discordUser", { username, global_name: globalName, discriminator, id, avatar, bot: false, flags, premium_type: premiumType });
|
||||
|
||||
// Extension requests Premid version
|
||||
sio.on("getVersion", () => {
|
||||
logRenderer("Extension requested version");
|
||||
sio.emit("receiveVersion", "221");
|
||||
});
|
||||
|
||||
sio.on("setActivity", setActivity);
|
||||
sio.on("clearActivity", clearActivity);
|
||||
sio.on("selectLocalPresence", () => {
|
||||
logRenderer("Selecting local presence is not supported");
|
||||
dialog.showMessageBox({ message: "Selecting local presence is not supported right now!", title: "vc-premid: oops!" });
|
||||
});
|
||||
sio.once("disconnect", () => onIoDisconnect());
|
||||
} catch (e) {
|
||||
logError("Error in onConnect: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
function logRenderer(message: string) {
|
||||
if (webFrame) {
|
||||
webFrame.executeJavaScript(`window.Vencord.Plugins.plugins.PreMiD.logger.info('${message}')`);
|
||||
} else {
|
||||
// just in case, dont worry about it pls
|
||||
console.log(`[vc-premid (fallback)] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function logError(message: string, ...args: any[]) {
|
||||
console.error(`${message}`, args);
|
||||
}
|
||||
|
||||
function setActivity(activity: any) {
|
||||
// hopefully this works
|
||||
webFrame.executeJavaScript(`window.Vencord.Plugins.plugins.PreMiD.receiveActivity(${JSON.stringify(activity)})`).catch(console.error);
|
||||
}
|
||||
|
||||
function clearActivity() {
|
||||
webFrame.executeJavaScript("window.Vencord.Plugins.plugins.PreMiD.clearActivity()");
|
||||
}
|
||||
|
||||
function onIOError(e: { message: string; code: string; }) {
|
||||
if (e.message.includes("EADDRINUSE")) return; // dont care, probably 2+ clients open
|
||||
logError("SocketIO error", e);
|
||||
}
|
||||
|
||||
async function onIoDisconnect() {
|
||||
console.log("[vc-premid] SocketIO disconnected");
|
||||
logRenderer("SocketIO disconnected");
|
||||
clearActivity();
|
||||
}
|
151
src/equicordplugins/roleMembersViewer/index.tsx
Normal file
151
src/equicordplugins/roleMembersViewer/index.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import {
|
||||
FluxDispatcher,
|
||||
GuildMemberStore,
|
||||
GuildStore,
|
||||
Menu,
|
||||
SelectedChannelStore,
|
||||
SelectedGuildStore,
|
||||
UserProfileActions,
|
||||
UserStore
|
||||
} from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
function fetchMembersWithRole(guildId: string, roleId: string) {
|
||||
const guildMembers = GuildMemberStore.getMembers(guildId);
|
||||
let membersInRole = 0;
|
||||
guildMembers.forEach(member => {
|
||||
if (member.roles.includes(roleId)) {
|
||||
membersInRole++;
|
||||
}
|
||||
});
|
||||
if (Object.keys(guildMembers).length < membersInRole) {
|
||||
const chunk = 100;
|
||||
const requestCount = Math.ceil(membersInRole / chunk);
|
||||
for (let i = 0; i < requestCount; i++) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GUILD_MEMBERS_REQUEST",
|
||||
guildId,
|
||||
userIds: [],
|
||||
query: "",
|
||||
limit: chunk,
|
||||
withPresences: true,
|
||||
notifyOnLimit: true
|
||||
});
|
||||
}
|
||||
}
|
||||
const updatedGuildMembers = GuildMemberStore.getMembers(guildId);
|
||||
return Object.values(updatedGuildMembers)
|
||||
.filter(m => m.roles.includes(roleId))
|
||||
.map(m => ({
|
||||
...m,
|
||||
user: UserStore.getUser(m.userId)
|
||||
}))
|
||||
.sort((a, b) => a.user.username.localeCompare(b.user.username));
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "RoleMembersViewer",
|
||||
description: "Shows members with a role when right clicking roles in user profiles or role mentions in messages",
|
||||
authors: [EquicordDevs.okiso],
|
||||
|
||||
contextMenus: {
|
||||
"dev-context"(children, { id }: { id: string; }) {
|
||||
const guild = GuildStore.getGuild(SelectedGuildStore.getGuildId());
|
||||
if (!guild) return;
|
||||
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
const guildId = guild.id;
|
||||
const membersWithRole = fetchMembersWithRole(guildId, id);
|
||||
|
||||
const memberItems = membersWithRole.map(member => (
|
||||
<Menu.MenuItem
|
||||
key={member.userId}
|
||||
id={`role-member-${member.userId}`}
|
||||
label={member.user.username}
|
||||
action={() => {
|
||||
UserProfileActions.openUserProfileModal({
|
||||
userId: member.userId,
|
||||
guildId,
|
||||
channelId: SelectedChannelStore.getChannelId()
|
||||
});
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="role-members-viewer"
|
||||
label={`View Members (${role.name}) - ${membersWithRole.length}`}
|
||||
>
|
||||
<Menu.MenuGroup>{memberItems}</Menu.MenuGroup>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
},
|
||||
|
||||
"message"(children, { message }: { message: any; }) {
|
||||
const guild = GuildStore.getGuild(SelectedGuildStore.getGuildId());
|
||||
if (!guild) return;
|
||||
|
||||
const roleMentions = message.content.match(/<@&(\d+)>/g);
|
||||
if (!roleMentions?.length) return;
|
||||
|
||||
// Extract unique role IDs from the mentions.
|
||||
const roleIds = roleMentions.map(mention => mention.match(/<@&(\d+)>/)![1]);
|
||||
const uniqueRoleIds = [...new Set(roleIds)];
|
||||
|
||||
const guildId = guild.id;
|
||||
const roleMenuItems: JSX.Element[] = [];
|
||||
|
||||
for (const roleId of uniqueRoleIds as string[]) {
|
||||
const role = GuildStore.getRole(guildId, roleId);
|
||||
if (!role) continue;
|
||||
|
||||
const membersWithRole = fetchMembersWithRole(guildId, roleId);
|
||||
const memberItems = membersWithRole.map(member => (
|
||||
<Menu.MenuItem
|
||||
key={member.userId}
|
||||
id={`role-member-${member.userId}`}
|
||||
label={member.user?.username ?? "Unknown User"}
|
||||
action={() => {
|
||||
UserProfileActions.openUserProfileModal({
|
||||
userId: member.userId,
|
||||
guildId,
|
||||
channelId: SelectedChannelStore.getChannelId()
|
||||
});
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
roleMenuItems.push(
|
||||
<Menu.MenuItem
|
||||
id={`role-members-viewer-${roleId}`}
|
||||
label={`@${role.name} - ${membersWithRole.length}`}
|
||||
>
|
||||
<Menu.MenuGroup>{memberItems}</Menu.MenuGroup>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (roleMenuItems.length > 0) {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="role-members-viewer"
|
||||
label={`View Role Members (${roleMenuItems.length} roles)`}
|
||||
>
|
||||
<Menu.MenuGroup>{roleMenuItems}</Menu.MenuGroup>
|
||||
</Menu.MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1000,6 +1000,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "mochie",
|
||||
id: 1043599230247374869n,
|
||||
},
|
||||
okiso: {
|
||||
name: "okiso",
|
||||
id: 274178934143451137n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue