Add Aliucord and NekosLife commands, improve settings UI

- Add Aliucord and NekosLife commands with category browsing - Refactor
settings UI for better grouping and clarity - Improve code formatting
and consistency across commands - Add serve.mjs script for local
development server - Update .gitignore for node_modules and dist -
Update plugin authors and manifest for new features
This commit is contained in:
kmmiio99o 2025-10-04 11:02:27 +02:00
parent ba0d3d7679
commit c35789b14e
19 changed files with 1820 additions and 1592 deletions

2
.gitignore vendored
View file

@ -6,3 +6,5 @@ Themer
RealThemer RealThemer
ChatMesser ChatMesser
allcommands allcommands
node_modules
dist

View file

@ -1,13 +1,12 @@
{ {
"authors": [ "authors": [
{
"name": "pylix",
"id": "492949202121261067"
},
{ {
"name": "kmmiio99o", "name": "kmmiio99o",
"id": "879393496627306587" "id": "879393496627306587"
} }
], ],
"main": "src/index.ts" "main": "index.js",
"vendetta": {
"icon": "ic_application_command_24px"
}
} }

View file

@ -9,11 +9,11 @@
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^24.1.0", "@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-node-resolve": "^15.0.2",
"@swc/core": "^1.3.51",
"@types/react": "^18.0.37", "@types/react": "^18.0.37",
"@types/react-native": "^0.70.13", "@types/react-native": "^0.70.13",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0", "@typescript-eslint/parser": "^5.59.0",
"@swc/core": "^1.3.51",
"esbuild": "^0.17.17", "esbuild": "^0.17.17",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",

View file

@ -1,9 +1,9 @@
import { registerCommand } from "@vendetta/commands"; import { registerCommand } from "@vendetta/commands";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
import { import {
catFactCommand, catFactCommand,
dogFactCommand, dogFactCommand,
uselessFactCommand, uselessFactCommand,
} from "./src/commands/facts"; } from "./src/commands/facts";
import { pluginListCommand, themeListCommand } from "./src/commands/lists"; import { pluginListCommand, themeListCommand } from "./src/commands/lists";
import { petPetCommand } from "./src/commands/petpet"; import { petPetCommand } from "./src/commands/petpet";
@ -11,10 +11,10 @@ import { konoSelfCommand, konoSendCommand } from "./src/commands/konochan";
import { firstMessageCommand } from "./src/commands/firstmessage"; import { firstMessageCommand } from "./src/commands/firstmessage";
import { sysinfoCommand } from "./src/commands/sysinfo"; import { sysinfoCommand } from "./src/commands/sysinfo";
import { import {
spotifyTrackCommand, spotifyTrackCommand,
spotifyAlbumCommand, spotifyAlbumCommand,
spotifyArtistsCommand, spotifyArtistsCommand,
spotifyCoverCommand spotifyCoverCommand
} from "./src/commands/spotify"; } from "./src/commands/spotify";
import { garyCommand } from "./src/commands/gary"; import { garyCommand } from "./src/commands/gary";
import { lovefemboysCommand } from "./src/commands/lovefemboys"; import { lovefemboysCommand } from "./src/commands/lovefemboys";
@ -24,103 +24,103 @@ import settings from "./settings";
// Initialize storage with default values // Initialize storage with default values
if (!storage.factSettings) { if (!storage.factSettings) {
storage.factSettings = { storage.factSettings = {
sendAsReply: true, sendAsReply: true,
includeCitation: false, includeCitation: false,
}; };
} }
if (!storage.listSettings) { if (!storage.listSettings) {
storage.listSettings = { storage.listSettings = {
pluginListAlwaysDetailed: false, pluginListAlwaysDetailed: false,
themeListAlwaysDetailed: false, themeListAlwaysDetailed: false,
}; };
} }
if (!storage.garySettings) { if (!storage.garySettings) {
storage.garySettings = { storage.garySettings = {
imageSource: "gary", imageSource: "gary",
}; };
} }
if (!storage.enabledCommands) { if (!storage.enabledCommands) {
storage.enabledCommands = { storage.enabledCommands = {
catfact: true, catfact: true,
dogfact: true, dogfact: true,
useless: true, useless: true,
petpet: true, petpet: true,
pluginList: true, pluginList: true,
themeList: true, themeList: true,
konoself: true, konoself: true,
konosend: true, konosend: true,
firstmessage: true, firstmessage: true,
sysinfo: true, sysinfo: true,
spotifyTrack: true, spotifyTrack: true,
spotifyAlbum: true, spotifyAlbum: true,
spotifyArtists: true, spotifyArtists: true,
spotifyCover: true, spotifyCover: true,
gary: true, gary: true,
ip: true, // IP command - enabled by default ip: true, // IP command - enabled by default
lovefemboys: false, // Hidden command - disabled by default lovefemboys: false, // Hidden command - disabled by default
nekoslife: false, // Hidden command - disabled by default nekoslife: false, // Hidden command - disabled by default
}; };
} }
// Initialize hidden settings storage // Initialize hidden settings storage
if (!storage.hiddenSettings) { if (!storage.hiddenSettings) {
storage.hiddenSettings = { storage.hiddenSettings = {
enabled: false, enabled: false,
visible: false, visible: false,
konochanBypassNsfw: false, // NSFW bypass option konochanBypassNsfw: false, // NSFW bypass option
}; };
} }
// Map commands to their storage keys // Map commands to their storage keys
const commandMap = { const commandMap = {
catfact: catFactCommand, catfact: catFactCommand,
dogfact: dogFactCommand, dogfact: dogFactCommand,
useless: uselessFactCommand, useless: uselessFactCommand,
petpet: petPetCommand, petpet: petPetCommand,
pluginList: pluginListCommand, pluginList: pluginListCommand,
themeList: themeListCommand, themeList: themeListCommand,
konoself: konoSelfCommand, konoself: konoSelfCommand,
konosend: konoSendCommand, konosend: konoSendCommand,
firstmessage: firstMessageCommand, firstmessage: firstMessageCommand,
sysinfo: sysinfoCommand, sysinfo: sysinfoCommand,
spotifyTrack: spotifyTrackCommand, spotifyTrack: spotifyTrackCommand,
spotifyAlbum: spotifyAlbumCommand, spotifyAlbum: spotifyAlbumCommand,
spotifyArtists: spotifyArtistsCommand, spotifyArtists: spotifyArtistsCommand,
spotifyCover: spotifyCoverCommand, spotifyCover: spotifyCoverCommand,
gary: garyCommand, gary: garyCommand,
ip: ipCommand, // Add IP command ip: ipCommand, // Add IP command
lovefemboys: lovefemboysCommand, // Add hidden command to map lovefemboys: lovefemboysCommand, // Add hidden command to map
nekoslife: nekoslifeCommand, // Add NekosLife command nekoslife: nekoslifeCommand, // Add NekosLife command
}; };
// Store registered commands for cleanup // Store registered commands for cleanup
let commands: Array<() => void> = []; let commands: Array<() => void> = [];
export default { export default {
onLoad: () => { onLoad: () => {
console.log("[All-In-One Commands] Loading plugin..."); console.log("[All-In-One Commands] Loading plugin...");
// Register only enabled commands // Register only enabled commands
for (const [key, command] of Object.entries(commandMap)) { for (const [key, command] of Object.entries(commandMap)) {
if (storage.enabledCommands[key]) { if (storage.enabledCommands[key]) {
commands.push(registerCommand(command)); commands.push(registerCommand(command));
console.log(`[All-In-One Commands] Registered command: ${key}`); console.log(`[All-In-One Commands] Registered command: ${key}`);
} }
} }
console.log("[All-In-One Commands] Plugin loaded successfully!"); console.log("[All-In-One Commands] Plugin loaded successfully!");
}, },
onUnload: () => { onUnload: () => {
console.log("[All-In-One Commands] Unloading plugin..."); console.log("[All-In-One Commands] Unloading plugin...");
commands.forEach((unregister) => unregister()); commands.forEach((unregister) => unregister());
commands = []; commands = [];
console.log("[All-In-One Commands] Plugin unloaded successfully!"); console.log("[All-In-One Commands] Plugin unloaded successfully!");
}, },
settings, settings,
}; };

File diff suppressed because it is too large Load diff

View file

@ -6,90 +6,90 @@ const MessageActions = findByProps("sendMessage");
// Helper function to format fact response // Helper function to format fact response
const formatFactResponse = (fact: { text: string; source?: string }) => { const formatFactResponse = (fact: { text: string; source?: string }) => {
let response = fact.text; let response = fact.text;
if (storage.factSettings?.includeCitation && fact.source) { if (storage.factSettings?.includeCitation && fact.source) {
response += `\n\nSource: ${fact.source}`; response += `\n\nSource: ${fact.source}`;
} }
return response; return response;
}; };
export const catFactCommand = { export const catFactCommand = {
name: "catfact", name: "catfact",
displayName: "catfact", displayName: "catfact",
description: "Sends a random cat fact.", description: "Sends a random cat fact.",
displayDescription: "Sends a random cat fact.", displayDescription: "Sends a random cat fact.",
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const fact = await catFact(); const fact = await catFact();
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
ctx.channel.id, ctx.channel.id,
{ content: formatFactResponse(fact) }, { content: formatFactResponse(fact) },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[CatFact] Error:', error); console.error("[CatFact] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
}; };
export const dogFactCommand = { export const dogFactCommand = {
name: "dogfact", name: "dogfact",
displayName: "dogfact", displayName: "dogfact",
description: "Sends a dog fact.", description: "Sends a dog fact.",
displayDescription: "Sends a dog fact.", displayDescription: "Sends a dog fact.",
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const fact = await dogFact(); const fact = await dogFact();
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
ctx.channel.id, ctx.channel.id,
{ content: formatFactResponse(fact) }, { content: formatFactResponse(fact) },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[DogFact] Error:', error); console.error("[DogFact] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
}; };
export const uselessFactCommand = { export const uselessFactCommand = {
name: "useless", name: "useless",
displayName: "useless", displayName: "useless",
description: "Sends a useless fact.", description: "Sends a useless fact.",
displayDescription: "Sends a useless fact.", displayDescription: "Sends a useless fact.",
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const fact = await uselessFact(); const fact = await uselessFact();
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
ctx.channel.id, ctx.channel.id,
{ content: formatFactResponse(fact) }, { content: formatFactResponse(fact) },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[UselessFact] Error:', error); console.error("[UselessFact] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
}; };

View file

@ -6,130 +6,130 @@ const MessageActions = findByProps("sendMessage");
// Helper functions for fetching messages // Helper functions for fetching messages
const getFirstGuildMessage = async ( const getFirstGuildMessage = async (
guildId: string, guildId: string,
userId?: string, userId?: string,
channelId?: string, channelId?: string,
) => { ) => {
const userParam = userId ? `&author_id=${userId}` : ""; const userParam = userId ? `&author_id=${userId}` : "";
const channelParam = channelId ? `&channel_id=${channelId}` : ""; const channelParam = channelId ? `&channel_id=${channelId}` : "";
const minIdParam = userId ? "" : `&min_id=0`; const minIdParam = userId ? "" : "&min_id=0";
return ( return (
await APIUtils.get({ await APIUtils.get({
url: `/guilds/${guildId}/messages/search`, url: `/guilds/${guildId}/messages/search`,
query: `include_nsfw=true${userParam}${channelParam}${minIdParam}&sort_by=timestamp&sort_order=asc&offset=0`, query: `include_nsfw=true${userParam}${channelParam}${minIdParam}&sort_by=timestamp&sort_order=asc&offset=0`,
}) })
).body.messages[0][0]; ).body.messages[0][0];
}; };
const getFirstDMMessage = async (dmId: string, userId?: string) => { const getFirstDMMessage = async (dmId: string, userId?: string) => {
const userParam = userId ? `&author_id=${userId}` : ""; const userParam = userId ? `&author_id=${userId}` : "";
const minIdParam = userId ? "" : `&min_id=0`; const minIdParam = userId ? "" : "&min_id=0";
return ( return (
await APIUtils.get({ await APIUtils.get({
url: `/channels/${dmId}/messages/search`, url: `/channels/${dmId}/messages/search`,
query: `&sort_by=timestamp&sort_order=asc&offset=0${userParam}${minIdParam}`, query: `&sort_by=timestamp&sort_order=asc&offset=0${userParam}${minIdParam}`,
}) })
).body.messages[0][0]; ).body.messages[0][0];
}; };
export const firstMessageCommand = { export const firstMessageCommand = {
name: "firstmessage", name: "firstmessage",
displayName: "firstmessage", displayName: "firstmessage",
description: "Tired of scrolling to first message?", description: "Tired of scrolling to first message?",
displayDescription: "Tired of scrolling to first message?", displayDescription: "Tired of scrolling to first message?",
options: [ options: [
{ {
name: "user", name: "user",
displayName: "user", displayName: "user",
description: "Target user to get their first message in this server/dm", description: "Target user to get their first message in this server/dm",
displayDescription: "Target user to get their first message in this server/dm", displayDescription: "Target user to get their first message in this server/dm",
type: 6, // USER type type: 6, // USER type
required: false, required: false,
},
{
name: "channel",
displayName: "channel",
description: "Target channel to get first message of",
displayDescription: "Target channel to get first message of",
type: 7, // CHANNEL type
required: false,
},
{
name: "send",
displayName: "send",
description: "Whether to send the resulting url",
displayDescription: "Whether to send the resulting url",
type: 5, // BOOLEAN type
required: false,
},
],
execute: async (args: any, ctx: any) => {
try {
const options = new Map(args.map((option: any) => [option.name, option]));
const user = options.get("user")?.value;
const channel = options.get("channel")?.value;
const send = options.get("send")?.value;
const guildId = ctx.guild?.id;
const channelId = ctx.channel.id;
const isDM = ctx.channel.type === 1;
let result = "https://discord.com/channels/";
let message;
if (!user && !channel) {
if (isDM) {
message = await getFirstDMMessage(channelId);
result += `@me/${channelId}/${message.id}`;
} else {
message = await getFirstGuildMessage(guildId);
result += `${guildId}/${message.channel_id}/${message.id}`;
}
} else if (user) {
if (isDM) {
message = await getFirstDMMessage(channelId, user);
result += `@me/${channelId}/${message.id}`;
} else {
message = await getFirstGuildMessage(guildId, user);
result += `${guildId}/${message.channel_id}/${message.id}`;
}
} else if (channel) {
if (isDM) {
// Silent fail for invalid combination
return { type: 4 };
}
message = await getFirstGuildMessage(guildId, null, channel);
result += `${guildId}/${channel}/${message.id}`;
} else {
// both user and channel specified
if (isDM) {
// Silent fail for invalid combination
return { type: 4 };
}
message = await getFirstGuildMessage(guildId, user, channel);
result += `${guildId}/${channel}/${message.id}`;
}
if (send) {
const fixNonce = Date.now().toString();
MessageActions.sendMessage(
ctx.channel.id,
{ content: result },
void 0,
{ nonce: fixNonce }
);
return { type: 4 };
} else {
url.openDeeplink(result);
return { type: 4 };
}
} catch (error) {
console.error("[FirstMessage] Error:", error);
// Silent fail - no error message in chat
return { type: 4 };
}
}, },
{ applicationId: "-1",
name: "channel", inputType: 1,
displayName: "channel", type: 1,
description: "Target channel to get first message of",
displayDescription: "Target channel to get first message of",
type: 7, // CHANNEL type
required: false,
},
{
name: "send",
displayName: "send",
description: "Whether to send the resulting url",
displayDescription: "Whether to send the resulting url",
type: 5, // BOOLEAN type
required: false,
},
],
execute: async (args: any, ctx: any) => {
try {
const options = new Map(args.map((option: any) => [option.name, option]));
const user = options.get("user")?.value;
const channel = options.get("channel")?.value;
const send = options.get("send")?.value;
const guildId = ctx.guild?.id;
const channelId = ctx.channel.id;
const isDM = ctx.channel.type === 1;
let result = "https://discord.com/channels/";
let message;
if (!user && !channel) {
if (isDM) {
message = await getFirstDMMessage(channelId);
result += `@me/${channelId}/${message.id}`;
} else {
message = await getFirstGuildMessage(guildId);
result += `${guildId}/${message.channel_id}/${message.id}`;
}
} else if (user) {
if (isDM) {
message = await getFirstDMMessage(channelId, user);
result += `@me/${channelId}/${message.id}`;
} else {
message = await getFirstGuildMessage(guildId, user);
result += `${guildId}/${message.channel_id}/${message.id}`;
}
} else if (channel) {
if (isDM) {
// Silent fail for invalid combination
return { type: 4 };
}
message = await getFirstGuildMessage(guildId, null, channel);
result += `${guildId}/${channel}/${message.id}`;
} else {
// both user and channel specified
if (isDM) {
// Silent fail for invalid combination
return { type: 4 };
}
message = await getFirstGuildMessage(guildId, user, channel);
result += `${guildId}/${channel}/${message.id}`;
}
if (send) {
const fixNonce = Date.now().toString();
MessageActions.sendMessage(
ctx.channel.id,
{ content: result },
void 0,
{ nonce: fixNonce }
);
return { type: 4 };
} else {
url.openDeeplink(result);
return { type: 4 };
}
} catch (error) {
console.error("[FirstMessage] Error:", error);
// Silent fail - no error message in chat
return { type: 4 };
}
},
applicationId: "-1",
inputType: 1,
type: 1,
}; };

View file

@ -5,42 +5,42 @@ import { getGaryUrl } from "../utils/api";
const MessageActions = findByProps("sendMessage"); const MessageActions = findByProps("sendMessage");
export const garyCommand = { export const garyCommand = {
name: "gary", name: "gary",
displayName: "gary", displayName: "gary",
description: "Send a random Gary image to the channel.", description: "Send a random Gary image to the channel.",
displayDescription: "Send a random Gary image to the channel.", displayDescription: "Send a random Gary image to the channel.",
options: [], options: [],
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
// Ensure garySettings exists and get the source // Ensure garySettings exists and get the source
if (!storage.garySettings) { if (!storage.garySettings) {
storage.garySettings = { imageSource: "gary" }; storage.garySettings = { imageSource: "gary" };
} }
const source = storage.garySettings.imageSource || "gary"; const source = storage.garySettings.imageSource || "gary";
console.log(`[Gary Command] Using image source: ${source}`); // Debug log console.log(`[Gary Command] Using image source: ${source}`); // Debug log
const imageUrl = await getGaryUrl(source); const imageUrl = await getGaryUrl(source);
if (!imageUrl) { if (!imageUrl) {
console.log("[Gary Command] No image URL received"); console.log("[Gary Command] No image URL received");
// Silent fail - no error message // Silent fail - no error message
return { type: 4 }; return { type: 4 };
} }
console.log(`[Gary Command] Sending image: ${imageUrl}`); // Debug log console.log(`[Gary Command] Sending image: ${imageUrl}`); // Debug log
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content: imageUrl }, void 0, { MessageActions.sendMessage(ctx.channel.id, { content: imageUrl }, void 0, {
nonce: fixNonce, nonce: fixNonce,
}); });
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[Gary] Error:', error); console.error("[Gary] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -26,131 +26,131 @@ interface IpWhoisResponse {
} }
export const ipCommand = { export const ipCommand = {
name: "ip", name: "ip",
displayName: "ip", displayName: "ip",
description: "IP address and domain lookup", description: "IP address and domain lookup",
displayDescription: "IP address and domain lookup", displayDescription: "IP address and domain lookup",
options: [ options: [
{ {
name: "query", name: "query",
displayName: "query", displayName: "query",
description: "Enter IPv4/IPv6 address or domain name", description: "Enter IPv4/IPv6 address or domain name",
displayDescription: "Enter IPv4/IPv6 address or domain name", displayDescription: "Enter IPv4/IPv6 address or domain name",
type: 3, // String type: 3, // String
required: true, required: true,
}, },
{ {
name: "send", name: "send",
displayName: "send", displayName: "send",
description: "Send to chat", description: "Send to chat",
displayDescription: "Send to chat", displayDescription: "Send to chat",
type: 5, // Boolean type: 5, // Boolean
required: false, required: false,
}, },
{ {
name: "ephemeral", name: "ephemeral",
displayName: "ephemeral", displayName: "ephemeral",
description: "Send as ephemeral message (only you can see)", description: "Send as ephemeral message (only you can see)",
displayDescription: "Send as ephemeral message (only you can see)", displayDescription: "Send as ephemeral message (only you can see)",
type: 5, // Boolean type: 5, // Boolean
required: false, required: false,
}
],
execute: async (args: any, ctx: any) => {
try {
const query = args.find((arg: any) => arg.name === "query")?.value;
const shouldSend = args.find((arg: any) => arg.name === "send")?.value || false;
const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
if (!query) {
const errorMsg = "❌ Query parameter is required!";
if (isEphemeral) {
return {
type: 4,
data: {
content: errorMsg,
flags: 64,
},
};
} }
showToast(errorMsg, getAssetIDByName("CircleXIcon")); ],
return { type: 4 }; execute: async (args: any, ctx: any) => {
} try {
const query = args.find((arg: any) => arg.name === "query")?.value;
const shouldSend = args.find((arg: any) => arg.name === "send")?.value || false;
const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
// Show loading toast if (!query) {
if (!isEphemeral && !shouldSend) { const errorMsg = "❌ Query parameter is required!";
showToast(`Looking up IP information for ${query}...`, getAssetIDByName("DownloadIcon")); if (isEphemeral) {
} return {
type: 4,
data: {
content: errorMsg,
flags: 64,
},
};
}
showToast(errorMsg, getAssetIDByName("CircleXIcon"));
return { type: 4 };
}
// Build API URL with all requested fields // Show loading toast
const url = `http://ipwhois.app/json/${encodeURIComponent(query)}?objects=` + if (!isEphemeral && !shouldSend) {
`success,ip,type,as,org,isp,continent,country,country_capital,city,region,` + showToast(`Looking up IP information for ${query}...`, getAssetIDByName("DownloadIcon"));
`country_phone,latitude,longitude,timezone,currency,completed_requests`; }
const response = await fetch(url); // Build API URL with all requested fields
const url = `http://ipwhois.app/json/${encodeURIComponent(query)}?objects=` +
"success,ip,type,as,org,isp,continent,country,country_capital,city,region," +
"country_phone,latitude,longitude,timezone,currency,completed_requests";
const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
const data: IpWhoisResponse = await response.json(); const data: IpWhoisResponse = await response.json();
if (!data.success) { if (!data.success) {
const errorMsg = `❌ Failed to lookup IP information for: ${query}`; const errorMsg = `❌ Failed to lookup IP information for: ${query}`;
if (isEphemeral) { if (isEphemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content: errorMsg, content: errorMsg,
flags: 64, flags: 64,
}, },
}; };
}
showToast(errorMsg, getAssetIDByName("CircleXIcon"));
return { type: 4 };
}
// Format the response as code block
const formattedData = JSON.stringify(data, null, 4);
const content = `\`\`\`json\n${formattedData}\n\`\`\``;
if (isEphemeral) {
return {
type: 4,
data: {
content,
flags: 64,
},
};
} else if (shouldSend) {
const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content }, void 0, { nonce: fixNonce });
return { type: 4 };
} else {
// Send as bot message (ephemeral-like)
messageUtil.sendBotMessage(ctx.channel.id, content);
return { type: 4 };
}
} catch (error) {
console.error("[IP] Command error:", error);
const errorMessage = `❌ An error occurred while looking up IP information: ${error.message || "Unknown error"}`;
const isEphemeral = args?.find?.((arg: any) => arg.name === "ephemeral")?.value ?? false;
if (isEphemeral) {
return {
type: 4,
data: {
content: errorMessage,
flags: 64,
},
};
}
showToast(errorMessage, getAssetIDByName("CircleXIcon"));
return { type: 4 };
} }
showToast(errorMsg, getAssetIDByName("CircleXIcon")); },
return { type: 4 }; applicationId: "-1",
} inputType: 1,
type: 1,
// Format the response as code block
const formattedData = JSON.stringify(data, null, 4);
const content = `\`\`\`json\n${formattedData}\n\`\`\``;
if (isEphemeral) {
return {
type: 4,
data: {
content,
flags: 64,
},
};
} else if (shouldSend) {
const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content }, void 0, { nonce: fixNonce });
return { type: 4 };
} else {
// Send as bot message (ephemeral-like)
messageUtil.sendBotMessage(ctx.channel.id, content);
return { type: 4 };
}
} catch (error) {
console.error('[IP] Command error:', error);
const errorMessage = `❌ An error occurred while looking up IP information: ${error.message || 'Unknown error'}`;
const isEphemeral = args?.find?.((arg: any) => arg.name === "ephemeral")?.value ?? false;
if (isEphemeral) {
return {
type: 4,
data: {
content: errorMessage,
flags: 64,
},
};
}
showToast(errorMessage, getAssetIDByName("CircleXIcon"));
return { type: 4 };
}
},
applicationId: "-1",
inputType: 1,
type: 1,
}; };

View file

@ -11,128 +11,128 @@ const messageUtil = findByProps("sendBotMessage", "sendMessage", "receiveMessage
* @returns The URL of the fetched image or null if none are found. * @returns The URL of the fetched image or null if none are found.
*/ */
const fetchImage = async (isNSFW: boolean): Promise<string | null> => { const fetchImage = async (isNSFW: boolean): Promise<string | null> => {
const baseURL = "https://konachan.com/post.json"; const baseURL = "https://konachan.com/post.json";
const tag = isNSFW ? "rating:explicit" : "rating:safe"; const tag = isNSFW ? "rating:explicit" : "rating:safe";
const randomPage = Math.floor(Math.random() * 100); const randomPage = Math.floor(Math.random() * 100);
try { try {
const response = await fetch(`${baseURL}?tags=${tag}&limit=1&page=${randomPage}`); const response = await fetch(`${baseURL}?tags=${tag}&limit=1&page=${randomPage}`);
const data = await response.json(); const data = await response.json();
if (!Array.isArray(data) || data.length === 0) { if (!Array.isArray(data) || data.length === 0) {
return null; return null;
}
return data[0].file_url;
} catch (error) {
console.error("[KonoChan Randomizer] Error fetching image:", error);
return null;
} }
return data[0].file_url;
} catch (error) {
console.error("[KonoChan Randomizer] Error fetching image:", error);
return null;
}
}; };
const showNSFWWarning = () => { const showNSFWWarning = () => {
return alerts.showConfirmationAlert({ return alerts.showConfirmationAlert({
title: "NSFW Content Warning", title: "NSFW Content Warning",
content: "NSFW content can only be sent in NSFW channels!", content: "NSFW content can only be sent in NSFW channels!",
confirmText: "Okay", confirmText: "Okay",
cancelText: null, cancelText: null,
confirmColor: "brand", confirmColor: "brand",
isDismissable: true, isDismissable: true,
}); });
}; };
// Common options for both commands // Common options for both commands
const nsfwOption = { const nsfwOption = {
name: "nsfw", name: "nsfw",
description: "Include NSFW content?", description: "Include NSFW content?",
type: 5, type: 5,
required: false, required: false,
displayName: "nsfw", displayName: "nsfw",
displayDescription: "Include NSFW content?", displayDescription: "Include NSFW content?",
}; };
export const konoSelfCommand = { export const konoSelfCommand = {
name: "konoself", name: "konoself",
displayName: "konoself", displayName: "konoself",
description: "Fetch a random image from KonoChan for yourself.", description: "Fetch a random image from KonoChan for yourself.",
displayDescription: "Fetch a random image from KonoChan for yourself.", displayDescription: "Fetch a random image from KonoChan for yourself.",
options: [nsfwOption], options: [nsfwOption],
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const options = new Map(args.map((option: any) => [option.name, option])); const options = new Map(args.map((option: any) => [option.name, option]));
const isNSFW = options.get("nsfw")?.value || false; const isNSFW = options.get("nsfw")?.value || false;
// Check if channel is NSFW for NSFW content (unless bypass is enabled) // Check if channel is NSFW for NSFW content (unless bypass is enabled)
const bypassEnabled = storage.hiddenSettings?.konochanBypassNsfw || false; const bypassEnabled = storage.hiddenSettings?.konochanBypassNsfw || false;
console.log(`[KonoSelf] NSFW: ${isNSFW}, Channel NSFW: ${ctx.channel.nsfw}, Bypass: ${bypassEnabled}`); console.log(`[KonoSelf] NSFW: ${isNSFW}, Channel NSFW: ${ctx.channel.nsfw}, Bypass: ${bypassEnabled}`);
if (isNSFW && !ctx.channel.nsfw && !bypassEnabled) { if (isNSFW && !ctx.channel.nsfw && !bypassEnabled) {
showNSFWWarning(); showNSFWWarning();
return { type: 4 }; return { type: 4 };
} }
const imageUrl = await fetchImage(isNSFW); const imageUrl = await fetchImage(isNSFW);
if (!imageUrl) { if (!imageUrl) {
// Silent fail - no error message // Silent fail - no error message
return { type: 4 }; return { type: 4 };
} }
messageUtil.sendBotMessage( messageUtil.sendBotMessage(
ctx.channel.id, ctx.channel.id,
`Here's your random image: ${imageUrl}`, `Here's your random image: ${imageUrl}`,
); );
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[KonoSelf] Error:', error); console.error("[KonoSelf] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };
export const konoSendCommand = { export const konoSendCommand = {
name: "konosend", name: "konosend",
displayName: "konosend", displayName: "konosend",
description: "Fetch a random image from KonoChan and send it to the channel.", description: "Fetch a random image from KonoChan and send it to the channel.",
displayDescription: "Fetch a random image from KonoChan and send it to the channel.", displayDescription: "Fetch a random image from KonoChan and send it to the channel.",
options: [nsfwOption], options: [nsfwOption],
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const options = new Map(args.map((option: any) => [option.name, option])); const options = new Map(args.map((option: any) => [option.name, option]));
const isNSFW = options.get("nsfw")?.value || false; const isNSFW = options.get("nsfw")?.value || false;
// Check if channel is NSFW for NSFW content (unless bypass is enabled) // Check if channel is NSFW for NSFW content (unless bypass is enabled)
const bypassEnabled = storage.hiddenSettings?.konochanBypassNsfw || false; const bypassEnabled = storage.hiddenSettings?.konochanBypassNsfw || false;
console.log(`[KonoSend] NSFW: ${isNSFW}, Channel NSFW: ${ctx.channel.nsfw}, Bypass: ${bypassEnabled}`); console.log(`[KonoSend] NSFW: ${isNSFW}, Channel NSFW: ${ctx.channel.nsfw}, Bypass: ${bypassEnabled}`);
if (isNSFW && !ctx.channel.nsfw && !bypassEnabled) { if (isNSFW && !ctx.channel.nsfw && !bypassEnabled) {
showNSFWWarning(); showNSFWWarning();
return { type: 4 }; return { type: 4 };
} }
const imageUrl = await fetchImage(isNSFW); const imageUrl = await fetchImage(isNSFW);
if (!imageUrl) { if (!imageUrl) {
// Silent fail - no error message // Silent fail - no error message
return { type: 4 }; return { type: 4 };
} }
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content: imageUrl }, void 0, { MessageActions.sendMessage(ctx.channel.id, { content: imageUrl }, void 0, {
nonce: fixNonce, nonce: fixNonce,
}); });
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[KonoSend] Error:', error); console.error("[KonoSend] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -38,16 +38,16 @@ const STATUS = {
NOT_SELECTED: "❌" NOT_SELECTED: "❌"
}; };
const MessageActions = findByProps('sendMessage', 'receiveMessage'); const MessageActions = findByProps("sendMessage", "receiveMessage");
const Clyde = findByProps('sendBotMessage'); const Clyde = findByProps("sendBotMessage");
const maxMessageLength = (() => { const maxMessageLength = (() => {
try { try {
return findByStoreName('UserStore') return findByStoreName("UserStore")
.getCurrentUser() .getCurrentUser()
?.premiumType === 2 ?.premiumType === 2
? 4000 ? 4000
: 2000; : 2000;
} catch (e) { } catch (e) {
return 2000; // fallback return 2000; // fallback
} }
@ -75,8 +75,8 @@ const addonAuthors = (authors: any) => {
if (authors.length === 0) return "Unknown"; if (authors.length === 0) return "Unknown";
return authors return authors
.filter(author => author && (typeof author === 'string' || (typeof author === 'object' && author.name))) .filter(author => author && (typeof author === "string" || (typeof author === "object" && author.name)))
.map(author => typeof author === 'string' ? author : (author.name || "Unknown")) .map(author => typeof author === "string" ? author : (author.name || "Unknown"))
.join(JOINERS.SEMICOL) || "Unknown"; .join(JOINERS.SEMICOL) || "Unknown";
}; };
@ -94,7 +94,7 @@ const sendList = async (channelID: string, list: string[]) => {
}, void 0, { nonce: fixNonce }); }, void 0, { nonce: fixNonce });
}; };
const baseListHeader = (type: 'Plugin' | 'Theme', length: number) => [ const baseListHeader = (type: "Plugin" | "Theme", length: number) => [
`**My ${type} List | ${length} ${type}s**`, `**My ${type} List | ${length} ${type}s**`,
EMPTY EMPTY
]; ];
@ -105,7 +105,7 @@ export async function themeList(args: any[], ctx: any) {
const alwaysDetailed = storage.themeListAlwaysDetailed ?? false; const alwaysDetailed = storage.themeListAlwaysDetailed ?? false;
// Ensure themes is valid // Ensure themes is valid
if (!themes || typeof themes !== 'object') { if (!themes || typeof themes !== "object") {
const channelID: string = ctx.channel.id; const channelID: string = ctx.channel.id;
Clyde.sendBotMessage(channelID, "No themes found or themes not loaded yet."); Clyde.sendBotMessage(channelID, "No themes found or themes not loaded yet.");
return { type: 4 }; return { type: 4 };
@ -115,11 +115,11 @@ export async function themeList(args: any[], ctx: any) {
const channelID: string = ctx.channel.id; const channelID: string = ctx.channel.id;
const themeList = baseListHeader('Theme', Object.keys(themes).length); const themeList = baseListHeader("Theme", Object.keys(themes).length);
if (objectValues.length) { if (objectValues.length) {
for (const theme of objectValues) { for (const theme of objectValues) {
if (!theme || typeof theme !== 'object') continue; if (!theme || typeof theme !== "object") continue;
const { selected, data, id } = theme; const { selected, data, id } = theme;
@ -161,7 +161,7 @@ export async function themeList(args: any[], ctx: any) {
await sendList(channelID, themeList); await sendList(channelID, themeList);
} }
} catch (error) { } catch (error) {
console.error('[ThemeList] Error:', error); console.error("[ThemeList] Error:", error);
return { type: 4 }; return { type: 4 };
} }
} }
@ -174,15 +174,15 @@ export async function pluginList(args: any[], ctx: any) {
const channelID: string = ctx.channel.id; const channelID: string = ctx.channel.id;
// Ensure plugins is valid // Ensure plugins is valid
if (!plugins || typeof plugins !== 'object') { if (!plugins || typeof plugins !== "object") {
Clyde.sendBotMessage(channelID, "No plugins found or plugins not loaded yet."); Clyde.sendBotMessage(channelID, "No plugins found or plugins not loaded yet.");
return { type: 4 }; return { type: 4 };
} }
const pluginList = baseListHeader('Plugin', Object.keys(plugins).length); const pluginList = baseListHeader("Plugin", Object.keys(plugins).length);
for (const plugin of Object.values(plugins)) { for (const plugin of Object.values(plugins)) {
if (!plugin || typeof plugin !== 'object') continue; if (!plugin || typeof plugin !== "object") continue;
const { enabled, manifest, id } = plugin; const { enabled, manifest, id } = plugin;
@ -222,50 +222,50 @@ export async function pluginList(args: any[], ctx: any) {
await sendList(channelID, pluginList); await sendList(channelID, pluginList);
} }
} catch (error) { } catch (error) {
console.error('[PluginList] Error:', error); console.error("[PluginList] Error:", error);
return { type: 4 }; return { type: 4 };
} }
} }
// Export commands for your existing structure // Export commands for your existing structure
export const pluginListCommand = { export const pluginListCommand = {
name: "plugin-list", name: "plugin-list",
displayName: "plugin list", displayName: "plugin list",
description: "Send your plugin list to the current channel", description: "Send your plugin list to the current channel",
displayDescription: "Send your plugin list to the current channel", displayDescription: "Send your plugin list to the current channel",
options: [ options: [
{ {
name: "detailed", name: "detailed",
description: "Whether to send a list with detailed information.", description: "Whether to send a list with detailed information.",
type: 5, type: 5,
required: false, required: false,
displayName: "detailed", displayName: "detailed",
displayDescription: "Whether to send a list with detailed information.", displayDescription: "Whether to send a list with detailed information.",
}, },
], ],
execute: pluginList, execute: pluginList,
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };
export const themeListCommand = { export const themeListCommand = {
name: "theme-list", name: "theme-list",
displayName: "theme list", displayName: "theme list",
description: "Send your theme list to the current channel", description: "Send your theme list to the current channel",
displayDescription: "Send your theme list to the current channel", displayDescription: "Send your theme list to the current channel",
options: [ options: [
{ {
name: "detailed", name: "detailed",
description: "Whether to send a list with detailed information.", description: "Whether to send a list with detailed information.",
type: 5, type: 5,
required: false, required: false,
displayName: "detailed", displayName: "detailed",
displayDescription: "Whether to send a list with detailed information.", displayDescription: "Whether to send a list with detailed information.",
}, },
], ],
execute: themeList, execute: themeList,
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -1,274 +1,173 @@
import { findByProps } from "@vendetta/metro"; import { findByProps } from "@vendetta/metro";
import { showToast } from "@vendetta/ui/toasts"; import { storage } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { alerts } from "@vendetta/ui"; import { alerts } from "@vendetta/ui";
const MessageActions = findByProps("sendMessage"); const MessageActions = findByProps("sendMessage", "receiveMessage");
const Channels = findByProps("getLastSelectedChannelId");
const BotMessage = findByProps("createBotMessage");
const Avatars = findByProps("BOT_AVATARS");
interface RedditPost { function sendReply(channelID, content, embed) {
kind: string; const channel = channelID ?? Channels?.getChannelId?.();
data: { const msg = BotMessage.createBotMessage({
title: string; channelId: channel,
url: string; content: "",
is_video: boolean; embeds: embed,
over_18: boolean; });
thumbnail: string; msg.author.username = "Astolfo";
preview?: { msg.author.avatar = "Astolfo";
images: Array<{ Avatars.BOT_AVATARS.Astolfo =
source: { "https://i.pinimg.com/736x/50/77/1f/50771f45b1c015cfbb8b0853ba7b8521.jpg";
url: string; if (typeof content === "string") {
width: number; msg.content = content;
height: number; } else {
}; Object.assign(msg, content);
resolutions: Array<{
url: string;
width: number;
height: number;
}>;
}>;
};
secure_media?: {
oembed?: {
thumbnail_url: string;
type: string;
};
reddit_video?: {
fallback_url: string;
width: number;
height: number;
};
};
media?: {
reddit_video?: {
fallback_url: string;
width: number;
height: number;
};
};
media_embed?: {
content: string;
};
url_overridden_by_dest?: string;
domain: string;
post_hint?: string;
selftext: string;
};
}
interface RedditResponse {
kind: string;
data: {
children: RedditPost[];
after: string;
dist: number;
};
}
function isValidMediaUrl(url: string): boolean {
if (!url) return false;
const lowerUrl = url.toLowerCase();
// Check for image/video/gif extensions
const mediaExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.mp4', '.webm', '.mov'];
if (mediaExtensions.some(ext => lowerUrl.includes(ext))) {
return true;
}
// Known media hosting domains (including video/gif hosts)
const mediaHosts = [
'i.redd.it',
'i.imgur.com',
'preview.redd.it',
'v.redd.it',
'redgifs.com',
'gfycat.com',
'imgur.com'
];
return mediaHosts.some(host => lowerUrl.includes(host));
}
function extractMediaUrl(post: RedditPost): string | null {
const postData = post.data;
// Try Reddit video first
if (postData.secure_media?.reddit_video?.fallback_url) {
return postData.secure_media.reddit_video.fallback_url;
}
if (postData.media?.reddit_video?.fallback_url) {
return postData.media.reddit_video.fallback_url;
}
// Try the main URL
const mainUrl = postData.url;
if (isValidMediaUrl(mainUrl)) {
return mainUrl;
}
// Try preview images
if (postData.preview?.images?.[0]) {
const previewImage = postData.preview.images[0];
// Try source image first (highest quality)
if (previewImage.source?.url) {
const sourceUrl = previewImage.source.url.replace(/&amp;/g, '&');
if (isValidMediaUrl(sourceUrl)) {
return sourceUrl;
}
}
// Try highest resolution preview
if (previewImage.resolutions?.length > 0) {
const highestRes = previewImage.resolutions[previewImage.resolutions.length - 1];
if (highestRes?.url) {
const resUrl = highestRes.url.replace(/&amp;/g, '&');
if (isValidMediaUrl(resUrl)) {
return resUrl;
}
}
}
}
// Try secure_media oembed thumbnail
if (postData.secure_media?.oembed?.thumbnail_url) {
const thumbUrl = postData.secure_media.oembed.thumbnail_url;
if (isValidMediaUrl(thumbUrl)) {
return thumbUrl;
}
}
// Try regular thumbnail as last resort
if (postData.thumbnail &&
postData.thumbnail !== 'self' &&
postData.thumbnail !== 'default' &&
postData.thumbnail !== 'nsfw' &&
postData.thumbnail.startsWith('http')) {
if (isValidMediaUrl(postData.thumbnail)) {
return postData.thumbnail;
}
}
return null;
}
async function getFemboyMedia(): Promise<string | null> {
try {
console.log(`[LoveFemboys] Fetching random media`);
const response = await fetch('https://reddit.com/r/femboys.json?limit=100&raw_json=1');
if (!response.ok) {
console.log('[LoveFemboys] Failed to fetch from Reddit API:', response.status, response.statusText);
return null;
}
const data: RedditResponse = await response.json();
console.log(`[LoveFemboys] Fetched ${data.data.children.length} posts`);
const validPosts = [];
for (const post of data.data.children) {
const postData = post.data;
// Skip text-only posts
if (postData.selftext && !postData.url) {
continue;
}
// Try to extract media URL
const mediaUrl = extractMediaUrl(post);
if (mediaUrl) {
console.log(`[LoveFemboys] Found valid media: ${mediaUrl}`);
validPosts.push(mediaUrl);
}
}
console.log(`[LoveFemboys] Found ${validPosts.length} valid media posts`);
if (validPosts.length === 0) {
console.log('[LoveFemboys] No suitable media found');
return null;
}
// Get random media
const randomUrl = validPosts[Math.floor(Math.random() * validPosts.length)];
console.log(`[LoveFemboys] Selected: ${randomUrl}`);
return randomUrl;
} catch (error) {
console.error('[LoveFemboys] Error fetching media:', error);
return null;
} }
MessageActions.receiveMessage(channel, msg);
} }
export const lovefemboysCommand = { export const lovefemboysCommand = {
name: "lovefemboys", name: "lovefemboys",
displayName: "lovefemboys", displayName: "lovefemboys",
description: "Get random femboy content from r/femboys", description: "Get an image of a femboy",
displayDescription: "Get random femboy content from r/femboys", displayDescription: "Get an image of a femboy",
options: [ options: [
{ {
name: "ephemeral", name: "nsfw",
displayName: "ephemeral", displayName: "nsfw",
description: "Send as ephemeral message (only you can see)", description: "Get the result from r/femboys instead of r/femboy (NSFW)",
displayDescription: "Send as ephemeral message (only you can see)", displayDescription:
type: 5, // Boolean "Get the result from r/femboys instead of r/femboy (NSFW)",
required: false, required: false,
} type: 5,
},
{
name: "sort",
displayName: "sort",
description: "Changes the way reddit sorts.",
displayDescription: "Changes the way reddit sorts",
required: false,
type: 3,
},
{
name: "silent",
displayName: "silent",
description: "Makes it so only you can see the message.",
displayDescription: "Makes it so only you can see the message.",
required: false,
type: 5,
},
], ],
execute: async (args: any, ctx: any) => {
try {
const isEphemeral = args?.find?.((arg: any) => arg.name === "ephemeral")?.value ?? false;
console.log(`[LoveFemboys] Command executed with ephemeral: ${isEphemeral}`);
// Show NSFW warning popup before proceeding
const shouldProceed = await new Promise<boolean>((resolve) => {
alerts.showConfirmationAlert({
title: "⚠️ NSFW Content Warning",
content: "This command will send potentially NSFW (Not Safe For Work) content from r/femboys. Are you sure you want to continue?",
confirmText: "Yes, Continue",
onConfirm: () => resolve(true),
cancelText: "Cancel",
onCancel: () => resolve(false),
});
});
if (!shouldProceed) {
console.log(`[LoveFemboys] User cancelled the command`);
return { type: 4 }; // Just dismiss the command
}
const mediaUrl = await getFemboyMedia();
if (!mediaUrl) {
showToast("❌ Failed to fetch femboy content", getAssetIDByName("CircleXIcon"));
return { type: 4 };
}
if (isEphemeral) {
console.log(`[LoveFemboys] Sending ephemeral response`);
return {
type: 4,
data: {
content: mediaUrl,
flags: 64, // Ephemeral flag
},
};
} else {
console.log(`[LoveFemboys] Sending public message`);
const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content: mediaUrl }, void 0, {nonce: fixNonce});
return { type: 4 };
}
} catch (error) {
console.error('[LoveFemboys] Command error:', error);
showToast("❌ An error occurred while fetching content", getAssetIDByName("CircleXIcon"));
return { type: 4 };
}
},
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: async (args, ctx) => {
try {
// Some code taken from enmity gotfemboys plugin by spinfal & was modified with the help of meqativ
let nsfw = args.find((arg) => arg.name === "nsfw")?.value;
let sort = args.find((arg) => arg.name === "sort")?.value;
let silent = args.find((arg) => arg.name === "silent")?.value;
if (typeof sort === "undefined") sort = storage.sortdefs;
if (
!["best", "hot", "new", "rising", "top", "controversial"].includes(sort)
) {
sendReply(
ctx.channel.id,
"Incorrect sorting type. Valid options are\n`best`, `hot`, `new`, `rising`, `top`, `controversial`.",
[],
);
return;
}
// Show NSFW warning popup only when nsfw option is selected
if (nsfw) {
const shouldProceed = await new Promise((resolve) => {
alerts.showConfirmationAlert({
title: "⚠️ NSFW Content Warning",
content:
"This command will send NSFW (Not Safe For Work) content from r/femboys. Are you sure you want to continue?",
confirmText: "Yes, Continue",
onConfirm: () => resolve(true),
cancelText: "Cancel",
onCancel: () => resolve(false),
});
});
if (!shouldProceed) {
return { type: 4 };
}
}
let response = await fetch(
`https://www.reddit.com/r/femboy/${sort}.json?limit=100`,
).then((res) => res.json());
if (!ctx.channel.nsfw_ && nsfw && storage.nsfwwarn && !(silent ?? true)) {
sendReply(
ctx.channel.id,
"This channel is not marked as NSFW\n(You can disable this check in plugin settings)",
[],
);
return;
}
if (nsfw) {
response = await fetch(
`https://www.reddit.com/r/femboys/${sort}.json?limit=100`,
).then((res) => res.json());
}
response =
response.data?.children?.[
Math.floor(Math.random() * response.data?.children?.length)
]?.data;
let author = await fetch(
`https://www.reddit.com/u/${response?.author}/about.json`,
).then((res) => res.json());
if (silent ?? true) {
sendReply(ctx.channel.id, "", [
{
type: "rich",
title: response?.title,
url: `https://reddit.com${response?.permalink}`,
author: {
name: `u/${response?.author} • r/${response?.subreddit}`,
proxy_icon_url: author?.data.icon_img.split("?")[0],
icon_url: author?.data.icon_img.split("?")[0],
},
image: {
proxy_url:
response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ??
response?.url.replace(/.gifv$/g, ".gif"),
url:
response?.url_overridden_by_dest?.replace(/.gifv$/g, ".gif") ??
response?.url?.replace(/.gifv$/g, ".gif"),
width: response?.preview?.images?.[0]?.source?.width,
height: response?.preview?.images?.[0]?.source?.height,
},
color: "0xf4b8e4",
},
]);
} else {
const fixNonce = Date.now().toString();
MessageActions.sendMessage(
ctx.channel.id,
{
content: response?.url_overridden_by_dest ?? response?.url,
},
void 0,
{ nonce: fixNonce },
);
}
} catch (err) {
console.error("[LoveFemboys] Error:", err);
sendReply(
ctx.channel.id,
"ERROR !!!!!!!!!!!! 😭😭😭 Check debug logs!! 🥺🥺🥺",
[],
);
}
},
}; };

View file

@ -11,203 +11,203 @@ interface NekosLifeResult {
// Valid SFW categories for validation // Valid SFW categories for validation
const validSfwCategories = [ const validSfwCategories = [
"avatar", "classic", "cuddle", "fox_girl", "gecg", "holo", "avatar", "classic", "cuddle", "fox_girl", "gecg", "holo",
"kemonomimi", "kiss", "neko", "ngif", "smug", "spank", "kemonomimi", "kiss", "neko", "ngif", "smug", "spank",
"tickle", "waifu", "wallpaper", "woof" "tickle", "waifu", "wallpaper", "woof"
]; ];
async function fetchNekosLifeImages(category: string, count: number): Promise<string[]> { async function fetchNekosLifeImages(category: string, count: number): Promise<string[]> {
const urls: string[] = []; const urls: string[] = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
try { try {
// Add delay between requests to avoid rate limiting // Add delay between requests to avoid rate limiting
if (i > 0) { if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
} }
const response = await fetch(`https://nekos.life/api/v2/img/${category}`); const response = await fetch(`https://nekos.life/api/v2/img/${category}`);
if (!response.ok) { if (!response.ok) {
console.error(`[NekosLife] API request failed: ${response.status}`); console.error(`[NekosLife] API request failed: ${response.status}`);
continue; continue;
} }
const data: NekosLifeResult = await response.json(); const data: NekosLifeResult = await response.json();
if (data.url) { if (data.url) {
urls.push(data.url); urls.push(data.url);
} }
} catch (error) { } catch (error) {
console.error(`[NekosLife] Error fetching image ${i + 1}:`, error); console.error(`[NekosLife] Error fetching image ${i + 1}:`, error);
}
} }
}
return urls; return urls;
} }
function isValidSfwCategory(category: string): boolean { function isValidSfwCategory(category: string): boolean {
if (!category || typeof category !== 'string') return false; if (!category || typeof category !== "string") return false;
return validSfwCategories.includes(category.toLowerCase().trim()); return validSfwCategories.includes(category.toLowerCase().trim());
} }
export const nekoslifeCommand = { export const nekoslifeCommand = {
name: "nekoslife", name: "nekoslife",
displayName: "nekoslife", displayName: "nekoslife",
description: "Get SFW images/gifs from nekos.life", description: "Get SFW images/gifs from nekos.life",
displayDescription: "Get SFW images/gifs from nekos.life", displayDescription: "Get SFW images/gifs from nekos.life",
options: [ options: [
{ {
name: "category", name: "category",
displayName: "category", displayName: "category",
description: "Category name (e.g. neko, waifu, cuddle, etc.)", description: "Category name (e.g. neko, waifu, cuddle, etc.)",
displayDescription: "Category name (e.g. neko, waifu, cuddle, etc.)", displayDescription: "Category name (e.g. neko, waifu, cuddle, etc.)",
type: 3, // String - free text input type: 3, // String - free text input
required: true, required: true,
}, },
{ {
name: "limit", name: "limit",
displayName: "limit", displayName: "limit",
description: "Number of images (1-5, default: 1)", description: "Number of images (1-5, default: 1)",
displayDescription: "Number of images (1-5, default: 1)", displayDescription: "Number of images (1-5, default: 1)",
type: 4, // Integer type: 4, // Integer
required: false, required: false,
}, },
{ {
name: "send", name: "send",
displayName: "send", displayName: "send",
description: "Send to chat", description: "Send to chat",
displayDescription: "Send to chat", displayDescription: "Send to chat",
type: 5, // Boolean type: 5, // Boolean
required: false, required: false,
}, },
{ {
name: "ephemeral", name: "ephemeral",
displayName: "ephemeral", displayName: "ephemeral",
description: "Send as ephemeral message (only you can see)", description: "Send as ephemeral message (only you can see)",
displayDescription: "Send as ephemeral message (only you can see)", displayDescription: "Send as ephemeral message (only you can see)",
type: 5, // Boolean type: 5, // Boolean
required: false, required: false,
} }
], ],
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
console.log('[NekosLife] Command executed with args:', args); console.log("[NekosLife] Command executed with args:", args);
// Parse arguments // Parse arguments
const categoryInput = args.find((arg: any) => arg.name === "category")?.value; const categoryInput = args.find((arg: any) => arg.name === "category")?.value;
const limitInput = args.find((arg: any) => arg.name === "limit")?.value; const limitInput = args.find((arg: any) => arg.name === "limit")?.value;
const shouldSend = args.find((arg: any) => arg.name === "send")?.value || false; const shouldSend = args.find((arg: any) => arg.name === "send")?.value || false;
const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false; const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
console.log('[NekosLife] Parsed values:', { categoryInput, limitInput, shouldSend, isEphemeral }); console.log("[NekosLife] Parsed values:", { categoryInput, limitInput, shouldSend, isEphemeral });
if (!categoryInput || typeof categoryInput !== 'string') { if (!categoryInput || typeof categoryInput !== "string") {
const errorMsg = "❌ Category is required! Examples: neko, waifu, cuddle, kiss"; const errorMsg = "❌ Category is required! Examples: neko, waifu, cuddle, kiss";
console.error('[NekosLife] No category provided'); console.error("[NekosLife] No category provided");
if (isEphemeral) { if (isEphemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content: errorMsg, content: errorMsg,
flags: 64, flags: 64,
}, },
}; };
} }
showToast(errorMsg, getAssetIDByName("CircleXIcon")); showToast(errorMsg, getAssetIDByName("CircleXIcon"));
return { type: 4 }; return { type: 4 };
} }
// Clean and validate category // Clean and validate category
const category = categoryInput.toLowerCase().trim(); const category = categoryInput.toLowerCase().trim();
if (!isValidSfwCategory(category)) { if (!isValidSfwCategory(category)) {
const errorMsg = `❌ Invalid SFW category "${categoryInput}". Valid: neko, waifu, cuddle, kiss, holo, etc.`; const errorMsg = `❌ Invalid SFW category "${categoryInput}". Valid: neko, waifu, cuddle, kiss, holo, etc.`;
if (isEphemeral) { if (isEphemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content: errorMsg, content: errorMsg,
flags: 64, flags: 64,
}, },
}; };
} }
showToast(errorMsg, getAssetIDByName("CircleXIcon")); showToast(errorMsg, getAssetIDByName("CircleXIcon"));
return { type: 4 }; return { type: 4 };
} }
// Parse and validate limit // Parse and validate limit
let limit = 1; let limit = 1;
if (limitInput !== undefined && limitInput !== null) { if (limitInput !== undefined && limitInput !== null) {
limit = parseInt(String(limitInput)) || 1; limit = parseInt(String(limitInput)) || 1;
limit = Math.max(1, Math.min(5, limit)); // Clamp between 1-5 limit = Math.max(1, Math.min(5, limit)); // Clamp between 1-5
} }
console.log('[NekosLife] Processing SFW request:', { category, limit, shouldSend, isEphemeral }); console.log("[NekosLife] Processing SFW request:", { category, limit, shouldSend, isEphemeral });
// Show loading toast // Show loading toast
if (!isEphemeral) { if (!isEphemeral) {
showToast(`Fetching ${limit} SFW image(s) from nekos.life...`, getAssetIDByName("DownloadIcon")); showToast(`Fetching ${limit} SFW image(s) from nekos.life...`, getAssetIDByName("DownloadIcon"));
} }
const urls = await fetchNekosLifeImages(category, limit); const urls = await fetchNekosLifeImages(category, limit);
if (urls.length === 0) { if (urls.length === 0) {
const errorMsg = "❌ Failed to fetch images from nekos.life. Try again later!"; const errorMsg = "❌ Failed to fetch images from nekos.life. Try again later!";
if (isEphemeral) { if (isEphemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content: errorMsg, content: errorMsg,
flags: 64, flags: 64,
}, },
}; };
} }
showToast(errorMsg, getAssetIDByName("CircleXIcon")); showToast(errorMsg, getAssetIDByName("CircleXIcon"));
return { type: 4 }; return { type: 4 };
} }
const content = urls.join("\n"); const content = urls.join("\n");
if (isEphemeral) { if (isEphemeral) {
console.log('[NekosLife] Sending ephemeral response'); console.log("[NekosLife] Sending ephemeral response");
return { return {
type: 4, type: 4,
data: { data: {
content, content,
flags: 64, flags: 64,
}, },
}; };
} else if (shouldSend) { } else if (shouldSend) {
console.log('[NekosLife] Sending to chat'); console.log("[NekosLife] Sending to chat");
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage(ctx.channel.id, { content }, void 0, { nonce: fixNonce }); MessageActions.sendMessage(ctx.channel.id, { content }, void 0, { nonce: fixNonce });
return { type: 4 }; return { type: 4 };
} else { } else {
console.log('[NekosLife] Sending as bot message'); console.log("[NekosLife] Sending as bot message");
messageUtil.sendBotMessage(ctx.channel.id, content); messageUtil.sendBotMessage(ctx.channel.id, content);
return { type: 4 }; return { type: 4 };
} }
} catch (error) { } catch (error) {
console.error('[NekosLife] Command error:', error); console.error("[NekosLife] Command error:", error);
const errorMessage = "❌ An error occurred while fetching images."; const errorMessage = "❌ An error occurred while fetching images.";
const isEphemeral = args?.find?.((arg: any) => arg.name === "ephemeral")?.value ?? false; const isEphemeral = args?.find?.((arg: any) => arg.name === "ephemeral")?.value ?? false;
if (isEphemeral) { if (isEphemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content: errorMessage, content: errorMessage,
flags: 64, flags: 64,
}, },
}; };
} }
showToast(errorMessage, getAssetIDByName("CircleXIcon")); showToast(errorMessage, getAssetIDByName("CircleXIcon"));
return { type: 4 }; return { type: 4 };
} }
}, },
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -5,41 +5,41 @@ const UserStore = findByStoreName("UserStore");
const MessageActions = findByProps("sendMessage"); const MessageActions = findByProps("sendMessage");
export const petPetCommand = { export const petPetCommand = {
name: "petpet", name: "petpet",
displayName: "petpet", displayName: "petpet",
description: "PetPet someone", description: "PetPet someone",
displayDescription: "PetPet someone", displayDescription: "PetPet someone",
options: [ options: [
{ {
name: "user", name: "user",
description: "The user(or their id) to be patted", description: "The user(or their id) to be patted",
type: 6, type: 6,
required: true, required: true,
displayName: "user", displayName: "user",
displayDescription: "The user(or their id) to be patted", displayDescription: "The user(or their id) to be patted",
}, },
], ],
execute: async (args: any, ctx: any) => { execute: async (args: any, ctx: any) => {
try { try {
const user = await UserStore.getUser(args[0].value); const user = await UserStore.getUser(args[0].value);
const image = user.getAvatarURL(512); const image = user.getAvatarURL(512);
const data = await getPetPetData(image); const data = await getPetPetData(image);
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
ctx.channel.id, ctx.channel.id,
{ content: data.url }, { content: data.url },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
} catch (error) { } catch (error) {
console.error('[PetPet] Error:', error); console.error("[PetPet] Error:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}, },
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -1,115 +1,115 @@
import { findByProps, findByStoreName } from "@vendetta/metro"; import { findByProps, findByStoreName } from "@vendetta/metro";
import { storage } from "@vendetta/plugin"; import { storage } from "@vendetta/plugin";
const MessageActions = findByProps('sendMessage', 'receiveMessage'); const MessageActions = findByProps("sendMessage", "receiveMessage");
const SpotifyStore = findByStoreName('SpotifyStore'); const SpotifyStore = findByStoreName("SpotifyStore");
// Individual command functions // Individual command functions
async function spotifyTrackExecute(args: any[], ctx: any) { async function spotifyTrackExecute(args: any[], ctx: any) {
if (!storage.enabledCommands?.spotifyTrack) return { type: 4 }; if (!storage.enabledCommands?.spotifyTrack) return { type: 4 };
return handleSpotifyCommand(ctx, (track: any) => return handleSpotifyCommand(ctx, (track: any) =>
`https://open.spotify.com/track/${track.id}` `https://open.spotify.com/track/${track.id}`
); );
} }
async function spotifyAlbumExecute(args: any[], ctx: any) { async function spotifyAlbumExecute(args: any[], ctx: any) {
if (!storage.enabledCommands?.spotifyAlbum) return { type: 4 }; if (!storage.enabledCommands?.spotifyAlbum) return { type: 4 };
return handleSpotifyCommand(ctx, (track: any) => return handleSpotifyCommand(ctx, (track: any) =>
`https://open.spotify.com/album/${track.album.id}` `https://open.spotify.com/album/${track.album.id}`
); );
} }
async function spotifyArtistsExecute(args: any[], ctx: any) { async function spotifyArtistsExecute(args: any[], ctx: any) {
if (!storage.enabledCommands?.spotifyArtists) return { type: 4 }; if (!storage.enabledCommands?.spotifyArtists) return { type: 4 };
return handleSpotifyCommand(ctx, (track: any) => return handleSpotifyCommand(ctx, (track: any) =>
track.artists.map((artist: any) => track.artists.map((artist: any) =>
`[${artist.name}](${artist.external_urls.spotify})` `[${artist.name}](${artist.external_urls.spotify})`
).join('\n') ).join("\n")
); );
} }
async function spotifyCoverExecute(args: any[], ctx: any) { async function spotifyCoverExecute(args: any[], ctx: any) {
if (!storage.enabledCommands?.spotifyCover) return { type: 4 }; if (!storage.enabledCommands?.spotifyCover) return { type: 4 };
return handleSpotifyCommand(ctx, (track: any) => return handleSpotifyCommand(ctx, (track: any) =>
track.album.image.url track.album.image.url
); );
} }
// Common handler for all Spotify commands // Common handler for all Spotify commands
async function handleSpotifyCommand(ctx: any, contentGenerator: (track: any) => string) { async function handleSpotifyCommand(ctx: any, contentGenerator: (track: any) => string) {
try { try {
const channelId = ctx.channel.id; const channelId = ctx.channel.id;
const track = SpotifyStore?.getTrack?.(); const track = SpotifyStore?.getTrack?.();
if (!track) { if (!track) {
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
channelId, channelId,
{ content: 'You are not listening to any track.' }, { content: "You are not listening to any track." },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
}
const content = contentGenerator(track);
const fixNonce = Date.now().toString();
MessageActions.sendMessage(
channelId,
{ content },
void 0,
{ nonce: fixNonce }
);
return { type: 4 };
} catch (error) {
console.error("[Spotify Command] Error:", error);
return { type: 4 };
} }
const content = contentGenerator(track);
const fixNonce = Date.now().toString();
MessageActions.sendMessage(
channelId,
{ content },
void 0,
{ nonce: fixNonce }
);
return { type: 4 };
} catch (error) {
console.error('[Spotify Command] Error:', error);
return { type: 4 };
}
} }
// Export individual command objects // Export individual command objects
export const spotifyTrackCommand = { export const spotifyTrackCommand = {
name: 'spotify track', name: "spotify track",
displayName: 'spotify track', displayName: "spotify track",
description: 'Sends your current Spotify track', description: "Sends your current Spotify track",
displayDescription: 'Sends your current Spotify track', displayDescription: "Sends your current Spotify track",
applicationId: '-1', applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: spotifyTrackExecute execute: spotifyTrackExecute
}; };
export const spotifyAlbumCommand = { export const spotifyAlbumCommand = {
name: 'spotify album', name: "spotify album",
displayName: 'spotify album', displayName: "spotify album",
description: "Sends your current track's album", description: "Sends your current track's album",
displayDescription: "Sends your current track's album", displayDescription: "Sends your current track's album",
applicationId: '-1', applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: spotifyAlbumExecute execute: spotifyAlbumExecute
}; };
export const spotifyArtistsCommand = { export const spotifyArtistsCommand = {
name: 'spotify artists', name: "spotify artists",
displayName: 'spotify artists', displayName: "spotify artists",
description: "Sends your current track's artists", description: "Sends your current track's artists",
displayDescription: "Sends your current track's artists", displayDescription: "Sends your current track's artists",
applicationId: '-1', applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: spotifyArtistsExecute execute: spotifyArtistsExecute
}; };
export const spotifyCoverCommand = { export const spotifyCoverCommand = {
name: 'spotify cover', name: "spotify cover",
displayName: 'spotify cover', displayName: "spotify cover",
description: "Sends your current track's cover", description: "Sends your current track's cover",
displayDescription: "Sends your current track's cover", displayDescription: "Sends your current track's cover",
applicationId: '-1', applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
execute: spotifyCoverExecute execute: spotifyCoverExecute
}; };

View file

@ -8,207 +8,207 @@ const messageUtil = findByProps("sendBotMessage", "sendMessage", "receiveMessage
// Device info helper // Device info helper
function getDeviceInfo() { function getDeviceInfo() {
let { height, width, scale } = ReactNative.Dimensions.get("screen"); let { height, width, scale } = ReactNative.Dimensions.get("screen");
height *= scale; height *= scale;
width *= scale; width *= scale;
return { width, height }; return { width, height };
} }
// Get current UTC date and time // Get current UTC date and time
function getCurrentUTCDateTime() { function getCurrentUTCDateTime() {
try { try {
const now = new Date(); const now = new Date();
const utc = new Date(now.getTime() + (now.getTimezoneOffset() * 60000)); const utc = new Date(now.getTime() + (now.getTimezoneOffset() * 60000));
const year = utc.getFullYear(); const year = utc.getFullYear();
const month = String(utc.getMonth() + 1).padStart(2, '0'); const month = String(utc.getMonth() + 1).padStart(2, "0");
const day = String(utc.getDate()).padStart(2, '0'); const day = String(utc.getDate()).padStart(2, "0");
const hours = String(utc.getHours()).padStart(2, '0'); const hours = String(utc.getHours()).padStart(2, "0");
const minutes = String(utc.getMinutes()).padStart(2, '0'); const minutes = String(utc.getMinutes()).padStart(2, "0");
const seconds = String(utc.getSeconds()).padStart(2, '0'); const seconds = String(utc.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (e) { } catch (e) {
return "Unknown"; return "Unknown";
} }
} }
// Hardware info helper // Hardware info helper
function getHardwareInfo() { function getHardwareInfo() {
try { try {
const hwProps = findByProps("memory"); const hwProps = findByProps("memory");
if (!hwProps) return { cpuCoreCount: "N/A", cpuPerc: "N/A", memUsage: "N/A" }; if (!hwProps) return { cpuCoreCount: "N/A", cpuPerc: "N/A", memUsage: "N/A" };
const { cpuCoreCount, cpuPercentage, memory } = hwProps; const { cpuCoreCount, cpuPercentage, memory } = hwProps;
const cpuPerc = cpuPercentage ? cpuPercentage.toFixed(2) + "%" : "N/A"; const cpuPerc = cpuPercentage ? cpuPercentage.toFixed(2) + "%" : "N/A";
const memUsage = memory ? parseFloat((memory / 1000).toPrecision(3)) + " MB" : "N/A"; const memUsage = memory ? parseFloat((memory / 1000).toPrecision(3)) + " MB" : "N/A";
return { cpuCoreCount, cpuPerc, memUsage }; return { cpuCoreCount, cpuPerc, memUsage };
} catch (e) { } catch (e) {
console.warn("Hardware info unavailable:", e); console.warn("Hardware info unavailable:", e);
return { cpuCoreCount: "N/A", cpuPerc: "N/A", memUsage: "N/A" }; return { cpuCoreCount: "N/A", cpuPerc: "N/A", memUsage: "N/A" };
} }
} }
// Discord info helper // Discord info helper
function getDiscordInfo() { function getDiscordInfo() {
try { try {
const { NativeModules } = ReactNative; const { NativeModules } = ReactNative;
const discordInfo = NativeModules.InfoDictionaryManager ?? NativeModules.RTNClientInfoManager; const discordInfo = NativeModules.InfoDictionaryManager ?? NativeModules.RTNClientInfoManager;
return discordInfo || {}; return discordInfo || {};
} catch (e) { } catch (e) {
console.warn("Discord info unavailable:", e); console.warn("Discord info unavailable:", e);
return {}; return {};
} }
} }
// Generate system info // Generate system info
function generateSystemInfo() { function generateSystemInfo() {
try { try {
const { cpuCoreCount, cpuPerc, memUsage } = getHardwareInfo(); const { cpuCoreCount, cpuPerc, memUsage } = getHardwareInfo();
const discordInfo = getDiscordInfo(); const discordInfo = getDiscordInfo();
const { vendetta, discord, react, hermes, os, device } = getDebugInfo(); const { vendetta, discord, react, hermes, os, device } = getDebugInfo();
const utcDateTime = getCurrentUTCDateTime(); const utcDateTime = getCurrentUTCDateTime();
const { version: HermesRelease, bytecodeVersion: HermesBytecode } = hermes || {}; const { version: HermesRelease, bytecodeVersion: HermesBytecode } = hermes || {};
const { version: ReactVersion, nativeVersion: RNVersion } = react || {}; const { version: ReactVersion, nativeVersion: RNVersion } = react || {};
const { name: osName, version: osVersion, sdk: osSdk } = os || {}; const { name: osName, version: osVersion, sdk: osSdk } = os || {};
const { const {
manufacturer: deviceManufacturer, manufacturer: deviceManufacturer,
brand: deviceBrand, brand: deviceBrand,
model: deviceModel, model: deviceModel,
codename: deviceCodename, codename: deviceCodename,
} = device || {}; } = device || {};
const { version: vendettaVersion } = vendetta || {}; const { version: vendettaVersion } = vendetta || {};
const { version: discordVersion, build: discordBuild } = discord || {}; const { version: discordVersion, build: discordBuild } = discord || {};
const deviceName = osName == "iOS" ? deviceCodename : `${deviceBrand} ${deviceModel}`; const deviceName = osName == "iOS" ? deviceCodename : `${deviceBrand} ${deviceModel}`;
const { width, height } = getDeviceInfo(); const { width, height } = getDeviceInfo();
let output = { const output = {
Device: { Device: {
Device: deviceName || "Unknown", Device: deviceName || "Unknown",
Model: deviceModel || "Unknown", Model: deviceModel || "Unknown",
Manufacturer: deviceManufacturer || "Unknown", Manufacturer: deviceManufacturer || "Unknown",
Brand: deviceBrand || "Unknown", Brand: deviceBrand || "Unknown",
Display: `${width}x${height}`, Display: `${width}x${height}`,
}, },
Hardware: { Hardware: {
"CPU Cores": cpuCoreCount || "N/A", "CPU Cores": cpuCoreCount || "N/A",
"CPU Usage": cpuPerc, "CPU Usage": cpuPerc,
"Memory Usage": memUsage, "Memory Usage": memUsage,
}, },
Software: { Software: {
OS: osName || "Unknown", OS: osName || "Unknown",
Version: osVersion || "Unknown", Version: osVersion || "Unknown",
}, },
Discord: { Discord: {
Version: discordVersion || "Unknown", Version: discordVersion || "Unknown",
Build: discordBuild || "Unknown", Build: discordBuild || "Unknown",
Vendetta: vendettaVersion || "Unknown", Vendetta: vendettaVersion || "Unknown",
}, },
React: { React: {
Version: ReactVersion || "Unknown", Version: ReactVersion || "Unknown",
"Hermes Bytecode": HermesBytecode || "Unknown", "Hermes Bytecode": HermesBytecode || "Unknown",
Hermes: HermesRelease || "Unknown", Hermes: HermesRelease || "Unknown",
Native: RNVersion || "Unknown", Native: RNVersion || "Unknown",
}, },
Time: { Time: {
"UTC": utcDateTime, "UTC": utcDateTime,
}, },
}; };
// Add additional mod versions if available // Add additional mod versions if available
if ((window as any).enmity) { if ((window as any).enmity) {
output.Discord.Enmity = (window as any).enmity.version; output.Discord.Enmity = (window as any).enmity.version;
}
if ((window as any).aliucord) {
output.Discord.Aliucord = (window as any).aliucord.version;
}
if (osSdk) {
output.Software["SDK Version"] = osSdk;
}
return output;
} catch (e) {
console.error("Error generating system info:", e);
throw e;
} }
if ((window as any).aliucord) {
output.Discord.Aliucord = (window as any).aliucord.version;
}
if (osSdk) {
output.Software["SDK Version"] = osSdk;
}
return output;
} catch (e) {
console.error("Error generating system info:", e);
throw e;
}
} }
// Initialize storage defaults // Initialize storage defaults
const categories = ["device", "hardware", "software", "discord", "react", "time", "ephemeral"]; const categories = ["device", "hardware", "software", "discord", "react", "time", "ephemeral"];
for (const cat of categories) { for (const cat of categories) {
if (storage[cat] === undefined) storage[cat] = true; if (storage[cat] === undefined) storage[cat] = true;
} }
// Command execution function // Command execution function
function executeSysinfoCommand(args: any[], ctx: any) { function executeSysinfoCommand(args: any[], ctx: any) {
try { try {
let output = ["__System Information__\n"]; const output = ["__System Information__\n"];
const data = generateSystemInfo(); const data = generateSystemInfo();
Object.keys(data).forEach((option) => { Object.keys(data).forEach((option) => {
const permit = storage[option.toLowerCase()]; const permit = storage[option.toLowerCase()];
const slasharg = args.find((i) => i.name == option.toLowerCase()); const slasharg = args.find((i) => i.name == option.toLowerCase());
const slashval = slasharg?.value ?? permit; const slashval = slasharg?.value ?? permit;
if ((slashval || permit) && !(permit && !slashval)) { if ((slashval || permit) && !(permit && !slashval)) {
output.push(`[**${option}**]`); output.push(`[**${option}**]`);
Object.keys(data[option as keyof typeof data]).forEach((subOption) => { Object.keys(data[option as keyof typeof data]).forEach((subOption) => {
output.push(`> ${subOption}: ${(data[option as keyof typeof data] as any)[subOption]}`); output.push(`> ${subOption}: ${(data[option as keyof typeof data] as any)[subOption]}`);
});
}
}); });
}
});
const epermit = storage["ephemeral"]; const epermit = storage["ephemeral"];
const ephemeral = args.find((i) => i.name == "ephemeral")?.value ?? epermit; const ephemeral = args.find((i) => i.name == "ephemeral")?.value ?? epermit;
if ((ephemeral || epermit) && !(epermit && !ephemeral)) { if ((ephemeral || epermit) && !(epermit && !ephemeral)) {
messageUtil.sendBotMessage(ctx.channel.id, output.join("\n")); messageUtil.sendBotMessage(ctx.channel.id, output.join("\n"));
return { type: 4 }; return { type: 4 };
} else { } else {
const fixNonce = Date.now().toString(); const fixNonce = Date.now().toString();
MessageActions.sendMessage( MessageActions.sendMessage(
ctx.channel.id, ctx.channel.id,
{ content: output.join("\n") }, { content: output.join("\n") },
void 0, void 0,
{ nonce: fixNonce } { nonce: fixNonce }
); );
return { type: 4 }; return { type: 4 };
}
} catch (e) {
console.error("[Sysinfo] Error:", e);
// Silent fail - no chat message
return { type: 4 };
} }
} catch (e) {
console.error("[Sysinfo] Error:", e);
// Silent fail - no chat message
return { type: 4 };
}
} }
export const sysinfoCommand = { export const sysinfoCommand = {
name: "sysinfo", name: "sysinfo",
displayName: "sysinfo", displayName: "sysinfo",
description: "Display system information", description: "Display system information",
displayDescription: "Display system information", displayDescription: "Display system information",
options: [ options: [
{ {
name: "ephemeral", name: "ephemeral",
displayName: "ephemeral", displayName: "ephemeral",
description: "Keep sysinfo ephemeral (default: true)", description: "Keep sysinfo ephemeral (default: true)",
displayDescription: "Keep sysinfo ephemeral (default: true)", displayDescription: "Keep sysinfo ephemeral (default: true)",
type: 5, type: 5,
required: false, required: false,
}, },
...categories.slice(0, -1).map((cat) => ({ ...categories.slice(0, -1).map((cat) => ({
name: cat, name: cat,
displayName: cat, displayName: cat,
description: `Display the ${cat} section. Set default in settings.`, description: `Display the ${cat} section. Set default in settings.`,
displayDescription: `Display the ${cat} section. Set default in settings.`, displayDescription: `Display the ${cat} section. Set default in settings.`,
type: 5, type: 5,
required: false, required: false,
})), })),
], ],
execute: executeSysinfoCommand, execute: executeSysinfoCommand,
applicationId: "-1", applicationId: "-1",
inputType: 1, inputType: 1,
type: 1, type: 1,
}; };

View file

@ -2,64 +2,64 @@
// Facts APIs // Facts APIs
export const uselessFact = async () => { export const uselessFact = async () => {
const response = await fetch("https://uselessfacts.jsph.pl/api/v2/facts/random"); const response = await fetch("https://uselessfacts.jsph.pl/api/v2/facts/random");
const resp = await response.json(); const resp = await response.json();
return { return {
text: resp["text"], text: resp["text"],
source: resp["source"], source: resp["source"],
language: resp["language"], language: resp["language"],
}; };
}; };
export const dogFact = async () => { export const dogFact = async () => {
const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1"); const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1");
const resp = await response.json(); const resp = await response.json();
return { return {
text: resp["data"]["0"]["attributes"]["body"], text: resp["data"]["0"]["attributes"]["body"],
}; };
}; };
export const catFact = async () => { export const catFact = async () => {
const response = await fetch("https://catfact.ninja/fact"); const response = await fetch("https://catfact.ninja/fact");
const resp = await response.json(); const resp = await response.json();
return { return {
text: resp["fact"], text: resp["fact"],
length: resp["length"], length: resp["length"],
}; };
}; };
// Image APIs // Image APIs
export const getPetPetData = async (image: string) => { export const getPetPetData = async (image: string) => {
const data = await fetch( const data = await fetch(
`https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}` `https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`
); );
const body = await data.json(); const body = await data.json();
return body; return body;
}; };
// Gary API function // Gary API function
export const getGaryUrl = async (source: string = "gary"): Promise<string | null> => { export const getGaryUrl = async (source = "gary"): Promise<string | null> => {
try { try {
switch (source) { switch (source) {
case "gary": case "gary":
const response = await fetch("https://api.garythe.cat/gary"); const response = await fetch("https://api.garythe.cat/gary");
const json = await response.json(); const json = await response.json();
return json.url; return json.url;
case "goober": case "goober":
const gooberResponse = await fetch("https://api.garythe.cat/goober"); const gooberResponse = await fetch("https://api.garythe.cat/goober");
const gooberJson = await gooberResponse.json(); const gooberJson = await gooberResponse.json();
return gooberJson.url; return gooberJson.url;
case "catapi": case "catapi":
const catResponse = await fetch("https://api.thecatapi.com/v1/images/search"); const catResponse = await fetch("https://api.thecatapi.com/v1/images/search");
const catJson = await catResponse.json(); const catJson = await catResponse.json();
return catJson[0].url; return catJson[0].url;
case "minker": case "minker":
return "https://minky.materii.dev/"; return "https://minky.materii.dev/";
default: default:
throw new Error("Invalid Gary image source value"); throw new Error("Invalid Gary image source value");
}
} catch (error) {
console.error(`[Gary API] Error fetching image from ${source}:`, error);
return null;
} }
} catch (error) {
console.error(`[Gary API] Error fetching image from ${source}:`, error);
return null;
}
}; };

View file

@ -4,65 +4,65 @@ const MessageActions = findByProps("sendMessage");
// Channel type validation // Channel type validation
const isValidChannel = (channelType: number) => { const isValidChannel = (channelType: number) => {
// Allow: Text (0), DM (1), Voice (2), Group DM (3), News (5) // Allow: Text (0), DM (1), Voice (2), Group DM (3), News (5)
// Block: Threads (10,11,12), Forum (15), Stage (13), Category (4) // Block: Threads (10,11,12), Forum (15), Stage (13), Category (4)
const unsupportedTypes = [4, 10, 11, 12, 13, 15]; const unsupportedTypes = [4, 10, 11, 12, 13, 15];
return !unsupportedTypes.includes(channelType); return !unsupportedTypes.includes(channelType);
}; };
export const sendMessage = ( export const sendMessage = (
channelId: string, channelId: string,
content: string, content: string,
replyToId?: string, replyToId?: string,
storage?: any, storage?: any,
ephemeral?: boolean, ephemeral?: boolean,
) => { ) => {
// If no content, just acknowledge the command // If no content, just acknowledge the command
if (!content) return { type: 4 }; if (!content) return { type: 4 };
// For ephemeral messages, return special format // For ephemeral messages, return special format
if (ephemeral) { if (ephemeral) {
return { return {
type: 4, type: 4,
data: { data: {
content, content,
flags: 64, flags: 64,
}, },
}; };
} }
try { try {
// For normal messages, send through MessageActions // For normal messages, send through MessageActions
const message = { const message = {
content, content,
...(replyToId && storage?.factSettings?.sendAsReply ...(replyToId && storage?.factSettings?.sendAsReply
? { messageReference: { messageId: replyToId } } ? { messageReference: { messageId: replyToId } }
: {}), : {}),
}; };
// Use consistent, collision-resistant nonce generation // Use consistent, collision-resistant nonce generation
const nonce = (BigInt(Date.now()) * BigInt(4194304) + BigInt(Math.floor(Math.random() * 4194304))).toString(); const nonce = (BigInt(Date.now()) * BigInt(4194304) + BigInt(Math.floor(Math.random() * 4194304))).toString();
MessageActions.sendMessage(channelId, message, void 0, { nonce }); MessageActions.sendMessage(channelId, message, void 0, { nonce });
return { type: 4 }; // Acknowledge the command return { type: 4 }; // Acknowledge the command
} catch (error) { } catch (error) {
console.error('[Commands] Message send failed:', error); console.error("[Commands] Message send failed:", error);
// Silent fail - no error message in chat // Silent fail - no error message in chat
return { type: 4 }; return { type: 4 };
} }
}; };
// Helper function to validate channel types in commands // Helper function to validate channel types in commands
export const validateChannelForCommand = (ctx: any) => { export const validateChannelForCommand = (ctx: any) => {
if (!isValidChannel(ctx.channel.type)) { if (!isValidChannel(ctx.channel.type)) {
return { return {
type: 4, type: 4,
data: { data: {
content: "This command cannot be used in threads, forum channels, or voice channels.", content: "This command cannot be used in threads, forum channels, or voice channels.",
flags: 64 // Ephemeral flags: 64 // Ephemeral
} }
}; };
} }
return null; return null;
}; };

81
serve.mjs Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env node
import { exec } from "child_process";
import { networkInterfaces } from "os";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get local IP address
function getLocalIP() {
const nets = networkInterfaces();
const results = {};
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// Skip over non-IPv4 and internal addresses
if (net.family === "IPv4" && !net.internal) {
if (!results[name]) {
results[name] = [];
}
results[name].push(net.address);
}
}
}
// Return the first available IP
const interfaces = Object.keys(results);
if (interfaces.length > 0) {
return results[interfaces[0]][0];
}
return "localhost";
}
const distPath = path.join(__dirname, "dist");
const port = process.env.PORT || 3000;
const host = "0.0.0.0"; // Bind to all interfaces
const localIP = getLocalIP();
console.log("🚀 Starting server...");
console.log(`📁 Serving files from: ${distPath}`);
console.log(`🌐 Server will be available at:`);
console.log(` - Local: http://localhost:${port}`);
console.log(` - Network: http://${localIP}:${port}`);
console.log(
` - Public: http://0.0.0.0:${port} (accessible from any device on your network)`,
);
console.log("\n💡 Press Ctrl+C to stop the server\n");
// Use the serve package to start the server
const serveCommand = `npx serve -s "${distPath}" -l ${port}`;
const serverProcess = exec(serveCommand, (error, stdout, stderr) => {
if (error) {
console.error(`❌ Error starting server: ${error.message}`);
process.exit(1);
}
});
// Forward output from serve command
serverProcess.stdout.on("data", (data) => {
console.log(data.toString());
});
serverProcess.stderr.on("data", (data) => {
console.error(data.toString());
});
// Handle graceful shutdown
process.on("SIGINT", () => {
console.log("\n🛑 Shutting down server...");
serverProcess.kill("SIGTERM");
process.exit(0);
});
process.on("SIGTERM", () => {
console.log("\n🛑 Shutting down server...");
serverProcess.kill("SIGTERM");
process.exit(0);
});