diff --git a/plugins/MoreAlts/AccountActions.js b/plugins/MoreAlts/AccountActions.js new file mode 100644 index 0000000..94ef42d --- /dev/null +++ b/plugins/MoreAlts/AccountActions.js @@ -0,0 +1,814 @@ +import { showConfirmationAlert } from "@vendetta/ui/alerts"; +import { showToast } from "@vendetta/ui/toasts"; +import { clipboard } from "@vendetta/metro/common"; +import { findByProps, findByStoreName } from "@vendetta/metro"; +import { addLog } from "./Logger.js"; +import { simpleHash } from "./PasswordUtils.js"; + +const UserStore = findByStoreName("UserStore"); +const TokenManager = findByProps("getToken"); + +const exportAccounts = async (storage, showPasswordDialog) => { + const checkPassword = (callback) => { + if (storage.settings?.exportPasswordHash) { + showPasswordDialog({ + type: 'export', + title: 'Enter Export Password', + message: 'Enter your password to export accounts', + callback: callback + }); + } else { + callback(); + } + }; + + checkPassword(() => { + showConfirmationAlert({ + title: "⚠️ Export Accounts - Security Warning", + content: "Exporting accounts is UNSAFE and may lead to account takeover if shared with others. The export contains sensitive authentication tokens that give full access to your accounts. Only proceed if you understand the risks and will keep the data secure.", + confirmText: "I Understand - Export", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: async () => { + try { + addLog('info', 'Starting account export'); + const accounts = storage.accounts || {}; + const accountData = Object.values(accounts).map(account => ({ + username: account.username, + discriminator: account.discriminator, + avatar: account.avatar, + id: account.id, + token: account.token, + addedAt: account.addedAt || Date.now() + })); + + const exportData = { + accounts: accountData, + exportPasswordHash: storage.settings?.exportPasswordHash || null, + exportedAt: Date.now(), + version: "2.0" + }; + + clipboard.setString(JSON.stringify(exportData, null, 2)); + addLog('info', 'Accounts exported successfully', { count: accountData.length }); + showToast(`Exported ${accountData.length} accounts to clipboard`, 0); + } catch (e) { + addLog('error', 'Failed to export accounts', { error: e.message }); + console.error("Export error:", e); + showToast("Failed to export accounts", 1); + } + } + }); + }); +}; + +const importAccounts = async (storage, showPasswordDialog, importText) => { + const checkPassword = (callback) => { + if (storage.settings?.exportPasswordHash) { + showPasswordDialog({ + type: 'import', + title: 'Enter Import Password', + message: 'Enter your password to import accounts', + callback: callback + }); + } else { + callback(); + } + }; + + checkPassword(() => { + showConfirmationAlert({ + title: "⚠️ Import Accounts - Security Warning", + content: "Importing accounts is UNSAFE and may compromise your security if the data comes from untrusted sources. Only import data you exported yourself or from sources you completely trust. Malicious imports could lead to account takeover.", + confirmText: "I Understand - Import", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: async () => { + try { + addLog('info', 'Starting account import'); + let dataToImport = importText.trim(); + if (!dataToImport) { + try { + dataToImport = await clipboard.getString(); + } catch (e) { + addLog('error', 'Failed to get clipboard data', { error: e.message }); + showToast("No data in clipboard or input field", 1); + return; + } + } + + if (!dataToImport) { + addLog('warn', 'No data to import'); + showToast("No data to import", 1); + return; + } + + const importData = JSON.parse(dataToImport); + let accountsArray; + + if (importData.accounts && Array.isArray(importData.accounts)) { + if (importData.exportPasswordHash) { + if (!storage.settings?.exportPasswordHash || storage.settings.exportPasswordHash !== importData.exportPasswordHash) { + addLog('error', 'Import password mismatch'); + showToast("Import password mismatch or not set locally", 1); + return; + } + } + accountsArray = importData.accounts; + + if (importData.exportPasswordHash && !storage.settings?.exportPasswordHash) { + storage.settings.exportPasswordHash = importData.exportPasswordHash; + addLog('info', 'Import password hash set from import data'); + } + } else if (Array.isArray(importData)) { + accountsArray = importData; + addLog('debug', 'Using legacy import format'); + } else { + addLog('error', 'Invalid import format'); + showToast("Invalid import format", 1); + return; + } + + let importedCount = 0; + let skippedCount = 0; + + accountsArray.forEach(accountData => { + if (accountData.id && accountData.token && accountData.username) { + if (!storage.accounts[accountData.id]) { + storage.accounts[accountData.id] = { + id: accountData.id, + username: accountData.username, + discriminator: accountData.discriminator || "0", + avatar: accountData.avatar || null, + displayName: accountData.displayName || accountData.username, + token: accountData.token, + addedAt: accountData.addedAt || Date.now() + }; + + if (!storage.accountOrder.includes(accountData.id)) { + storage.accountOrder.push(accountData.id); + } + importedCount++; + } else { + skippedCount++; + } + } + }); + + addLog('info', 'Import completed', { imported: importedCount, skipped: skippedCount }); + showToast(`Imported ${importedCount} accounts, skipped ${skippedCount} duplicates`, 0); + } catch (e) { + addLog('error', 'Import failed', { error: e.message }); + console.error("Import error:", e); + showToast("Failed to import - invalid format or password mismatch", 1); + } + } + }); + }); +}; + +const setExportPassword = (storage, newPassword, confirmPassword, setNewPassword, setConfirmPassword) => { + if (!newPassword.trim()) { + showToast("Please enter a password", 1); + return; + } + + if (newPassword !== confirmPassword) { + showToast("Passwords don't match", 1); + return; + } + + const passwordHash = simpleHash(newPassword); + storage.settings.exportPasswordHash = passwordHash; + setNewPassword(""); + setConfirmPassword(""); + addLog('info', 'Export password set successfully'); + showToast("Export password set successfully", 0); +}; + +const removeExportPassword = (storage, showPasswordDialog) => { + const checkPassword = (callback) => { + showPasswordDialog({ + type: 'remove', + title: 'Enter Password to Remove', + message: 'Enter your current password to remove protection', + callback: callback + }); + }; + + checkPassword(() => { + showConfirmationAlert({ + title: "Remove Export Password", + content: "Are you sure you want to remove the export password? This will make exports/imports less secure.", + confirmText: "Remove Password", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: () => { + delete storage.settings.exportPasswordHash; + addLog('info', 'Export password removed'); + showToast("Export password removed", 0); + } + }); + }); +}; + +const addAccountWithToken = async (storage, newToken, setNewToken, setIsAdding) => { + if (!storage.settings.enableUnsafeFeatures) return; + + setIsAdding(true); + addLog('info', 'Starting token account addition'); + + try { + let token = newToken.trim(); + if (!token) { + token = TokenManager.getToken(); + addLog('debug', 'Using current account token'); + } + + if (!token.startsWith("Bot ") && !token.match(/^[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}$/)) { + addLog('error', 'Invalid token format provided'); + showToast("Invalid token format", 1); + setIsAdding(false); + return; + } + + addLog('debug', 'Fetching user info from Discord API'); + const response = await fetch("https://discord.com/api/v9/users/@me", { + headers: { "Authorization": token, "Content-Type": "application/json" } + }); + + if (!response.ok) { + addLog('error', 'Discord API request failed', { status: response.status, statusText: response.statusText }); + showToast("Invalid or expired token", 1); + setIsAdding(false); + return; + } + + const user = await response.json(); + addLog('info', 'Successfully retrieved user info', { username: user.username, id: user.id }); + + const userInfo = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + displayName: user.global_name || user.username + }; + + if (storage.accounts[userInfo.id]) { + addLog('warn', 'Account already exists', { username: userInfo.username }); + showToast(`Account ${userInfo.username} already saved`, 1); + setIsAdding(false); + return; + } + + const newAccount = { ...userInfo, token: token, addedAt: Date.now() }; + + storage.accounts[userInfo.id] = newAccount; + if (!storage.accountOrder.includes(userInfo.id)) { + storage.accountOrder.push(userInfo.id); + } + + setNewToken(""); + addLog('info', 'Account added successfully via token', { username: userInfo.username }); + showToast(`Account ${userInfo.username} added!`, 0); + } catch (e) { + addLog('error', 'Failed to add account via token', { error: e.message, stack: e.stack }); + console.error("Add account error:", e); + showToast("Failed to add account", 1); + } + setIsAdding(false); +}; + +const forceLogout = async () => { + showConfirmationAlert({ + title: "Force Logout", + content: "This will logout your current session by using an invalid token. Your saved accounts will remain intact. Continue?", + confirmText: "Force Logout", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: async () => { + try { + addLog('info', 'Starting force logout'); + const fakeToken = "MTExMTExMTExMTExMTExMTEx.GxxxXx.xXxXxXxXxXxXxXxXxXxXxXxXxXxXx"; + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(fakeToken); + addLog('info', 'Force logout completed successfully'); + showToast("Force logout successful - you can now login to a different account", 0); + } catch (e) { + addLog('warn', 'Force logout completed with error', { error: e.message }); + console.error("Force logout error:", e); + showToast("Force logout completed", 0); + } + } + }); +}; + +const addAccountWithCredentials = async (storage, email, password, setEmail, setPassword, setShowAddDialog, setIsAddingDynamic) => { + if (!email.trim() || !password.trim()) { + showToast("Please enter both email and password", 1); + return; + } + + setIsAddingDynamic(true); + addLog('info', 'Starting credential-based account addition', { email: email.trim() }); + + try { + const response = await fetch("https://discord.com/api/v9/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + body: JSON.stringify({ + login: email.trim(), + password: password.trim() + }) + }); + + const loginData = await response.json(); + + if (!response.ok || !loginData.token) { + addLog('error', 'Discord login failed', { + status: response.status, + message: loginData.message, + errors: loginData.errors, + captchaRequired: !!loginData.captcha_key + }); + + let errorMessage = "Login failed"; + + if (loginData.captcha_key) { + errorMessage = "Captcha required - please login through Discord first"; + } else if (loginData.message) { + const msg = loginData.message.toLowerCase(); + if (msg.includes('invalid') && (msg.includes('email') || msg.includes('phone') || msg.includes('login'))) { + errorMessage = "Invalid email or username"; + } else if (msg.includes('password')) { + errorMessage = "Invalid password"; + } else if (msg.includes('account')) { + errorMessage = "Account issue - check your credentials"; + } else if (msg.includes('rate limit') || msg.includes('too many')) { + errorMessage = "Too many attempts - please wait and try again"; + } else if (msg.includes('mfa') || msg.includes('2fa') || msg.includes('verification')) { + errorMessage = "2FA/MFA accounts not supported via credentials"; + } else { + errorMessage = "Login failed - check your credentials"; + } + } else if (loginData.errors) { + const errors = loginData.errors; + if (errors.login) { + errorMessage = "Invalid email or username"; + } else if (errors.password) { + errorMessage = "Invalid password"; + } else { + errorMessage = "Invalid credentials"; + } + } else if (response.status === 401) { + errorMessage = "Invalid credentials"; + } else if (response.status === 429) { + errorMessage = "Rate limited - please wait and try again"; + } else if (response.status >= 500) { + errorMessage = "Discord servers are having issues - try again later"; + } + + showToast(errorMessage, 1); + setIsAddingDynamic(false); + return; + } + + addLog('debug', 'Login successful, fetching user info'); + const userResponse = await fetch("https://discord.com/api/v9/users/@me", { + headers: { "Authorization": loginData.token } + }); + + if (!userResponse.ok) { + addLog('error', 'Failed to get user info after login', { status: userResponse.status }); + showToast("Login succeeded but failed to get user info - check logs", 1); + setIsAddingDynamic(false); + return; + } + + const user = await userResponse.json(); + + if (storage.accounts[user.id]) { + addLog('warn', 'Account already exists', { username: user.username }); + showToast(`Account ${user.username} already saved`, 1); + setIsAddingDynamic(false); + return; + } + + const newAccount = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + displayName: user.global_name || user.username, + token: loginData.token, + addedAt: Date.now() + }; + + storage.accounts[user.id] = newAccount; + if (!storage.accountOrder.includes(user.id)) { + storage.accountOrder.push(user.id); + } + + setEmail(""); + setPassword(""); + setShowAddDialog(false); + addLog('info', 'Account added successfully via credentials', { username: user.username }); + showToast(`Account ${user.username} added successfully!`, 0); + } catch (e) { + addLog('error', 'Credential login failed', { error: e.message, stack: e.stack }); + console.error("Login error:", e); + + let errorMessage = "Login failed - check logs for more info"; + if (e.message.includes('network') || e.message.includes('fetch')) { + errorMessage = "Network error - check your connection"; + } else if (e.message.includes('timeout')) { + errorMessage = "Request timed out - try again"; + } + + showToast(errorMessage, 1); + } + setIsAddingDynamic(false); +}; + +export { + exportAccounts, + importAccounts, + setExportPassword, + removeExportPassword, + addAccountWithToken, + addAccountWithCredentials, + forceLogout +};showConfirmationAlert({ + title: "⚠️ Import Accounts - Security Warning", + content: "Importing accounts is UNSAFE and may compromise your security if the data comes from untrusted sources. Only import data you exported yourself or from sources you completely trust. Malicious imports could lead to account takeover.", + confirmText: "I Understand - Import", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: async () => { + try { + addLog('info', 'Starting account import'); + let dataToImport = importText.trim(); + if (!dataToImport) { + try { + dataToImport = await clipboard.getString(); + } catch (e) { + addLog('error', 'Failed to get clipboard data', { error: e.message }); + showToast("No data in clipboard or input field", 1); + return; + } + } + + if (!dataToImport) { + addLog('warn', 'No data to import'); + showToast("No data to import", 1); + return; + } + + const importData = JSON.parse(dataToImport); + let accountsArray; + + if (importData.accounts && Array.isArray(importData.accounts)) { + if (importData.exportPasswordHash) { + if (!storage.settings?.exportPasswordHash || storage.settings.exportPasswordHash !== importData.exportPasswordHash) { + addLog('error', 'Import password mismatch'); + showToast("Import password mismatch or not set locally", 1); + return; + } + } + accountsArray = importData.accounts; + + if (importData.exportPasswordHash && !storage.settings?.exportPasswordHash) { + storage.settings.exportPasswordHash = importData.exportPasswordHash; + addLog('info', 'Import password hash set from import data'); + } + } else if (Array.isArray(importData)) { + accountsArray = importData; + addLog('debug', 'Using legacy import format'); + } else { + addLog('error', 'Invalid import format'); + showToast("Invalid import format", 1); + return; + } + + let importedCount = 0; + let skippedCount = 0; + + accountsArray.forEach(accountData => { + if (accountData.id && accountData.token && accountData.username) { + if (!storage.accounts[accountData.id]) { + storage.accounts[accountData.id] = { + id: accountData.id, + username: accountData.username, + discriminator: accountData.discriminator || "0", + avatar: accountData.avatar || null, + displayName: accountData.displayName || accountData.username, + token: accountData.token, + addedAt: accountData.addedAt || Date.now() + }; + + if (!storage.accountOrder.includes(accountData.id)) { + storage.accountOrder.push(accountData.id); + } + importedCount++; + } else { + skippedCount++; + } + } + }); + + addLog('info', 'Import completed', { imported: importedCount, skipped: skippedCount }); + showToast(`Imported ${importedCount} accounts, skipped ${skippedCount} duplicates`, 0); + } catch (e) { + addLog('error', 'Import failed', { error: e.message }); + console.error("Import error:", e); + showToast("Failed to import - invalid format or password mismatch", 1); + } + } + }); + }); +}; + +// Password management functions +const setExportPassword = (storage, newPassword, confirmPassword, setNewPassword, setConfirmPassword) => { + if (!newPassword.trim()) { + showToast("Please enter a password", 1); + return; + } + + if (newPassword !== confirmPassword) { + showToast("Passwords don't match", 1); + return; + } + + const passwordHash = simpleHash(newPassword); + storage.settings.exportPasswordHash = passwordHash; + setNewPassword(""); + setConfirmPassword(""); + addLog('info', 'Export password set successfully'); + showToast("Export password set successfully", 0); +}; + +const removeExportPassword = (storage, showPasswordDialog) => { + const checkPassword = (callback) => { + showPasswordDialog({ + type: 'remove', + title: 'Enter Password to Remove', + message: 'Enter your current password to remove protection', + callback: callback + }); + }; + + checkPassword(() => { + showConfirmationAlert({ + title: "Remove Export Password", + content: "Are you sure you want to remove the export password? This will make exports/imports less secure.", + confirmText: "Remove Password", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: () => { + delete storage.settings.exportPasswordHash; + addLog('info', 'Export password removed'); + showToast("Export password removed", 0); + } + }); + }); +}; + +// Account management functions +const addAccountWithToken = async (storage, newToken, setNewToken, setIsAdding) => { + if (!storage.settings.enableUnsafeFeatures) return; + + setIsAdding(true); + addLog('info', 'Starting token account addition'); + + try { + let token = newToken.trim(); + if (!token) { + token = TokenManager.getToken(); + addLog('debug', 'Using current account token'); + } + + if (!token.startsWith("Bot ") && !token.match(/^[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}$/)) { + addLog('error', 'Invalid token format provided'); + showToast("Invalid token format", 1); + setIsAdding(false); + return; + } + + addLog('debug', 'Fetching user info from Discord API'); + const response = await fetch("https://discord.com/api/v9/users/@me", { + headers: { "Authorization": token, "Content-Type": "application/json" } + }); + + if (!response.ok) { + addLog('error', 'Discord API request failed', { status: response.status, statusText: response.statusText }); + showToast("Invalid or expired token", 1); + setIsAdding(false); + return; + } + + const user = await response.json(); + addLog('info', 'Successfully retrieved user info', { username: user.username, id: user.id }); + + const userInfo = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + displayName: user.global_name || user.username + }; + + if (storage.accounts[userInfo.id]) { + addLog('warn', 'Account already exists', { username: userInfo.username }); + showToast(`Account ${userInfo.username} already saved`, 1); + setIsAdding(false); + return; + } + + const newAccount = { ...userInfo, token: token, addedAt: Date.now() }; + + storage.accounts[userInfo.id] = newAccount; + if (!storage.accountOrder.includes(userInfo.id)) { + storage.accountOrder.push(userInfo.id); + } + + setNewToken(""); + addLog('info', 'Account added successfully via token', { username: userInfo.username }); + showToast(`Account ${userInfo.username} added!`, 0); + } catch (e) { + addLog('error', 'Failed to add account via token', { error: e.message, stack: e.stack }); + console.error("Add account error:", e); + showToast("Failed to add account", 1); + } + setIsAdding(false); +}; + +const forceLogout = async () => { + showConfirmationAlert({ + title: "Force Logout", + content: "This will logout your current session by using an invalid token. Your saved accounts will remain intact. Continue?", + confirmText: "Force Logout", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: async () => { + try { + addLog('info', 'Starting force logout'); + const fakeToken = "MTExMTExMTExMTExMTExMTEx.GxxxXx.xXxXxXxXxXxXxXxXxXxXxXxXxXxXx"; + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(fakeToken); + addLog('info', 'Force logout completed successfully'); + showToast("Force logout successful - you can now login to a different account", 0); + } catch (e) { + addLog('warn', 'Force logout completed with error', { error: e.message }); + console.error("Force logout error:", e); + showToast("Force logout completed", 0); + } + } + }); +}; + +const addAccountWithCredentials = async (storage, email, password, setEmail, setPassword, setShowAddDialog, setIsAddingDynamic) => { + if (!email.trim() || !password.trim()) { + showToast("Please enter both email and password", 1); + return; + } + + setIsAddingDynamic(true); + addLog('info', 'Starting credential-based account addition', { email: email.trim() }); + + try { + const response = await fetch("https://discord.com/api/v9/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + body: JSON.stringify({ + login: email.trim(), + password: password.trim() + }) + }); + + const loginData = await response.json(); + + if (!response.ok || !loginData.token) { + addLog('error', 'Discord login failed', { + status: response.status, + message: loginData.message, + errors: loginData.errors, + captchaRequired: !!loginData.captcha_key + }); + + // Provide user-friendly error messages + let errorMessage = "Login failed"; + + if (loginData.captcha_key) { + errorMessage = "Captcha required - please login through Discord first"; + } else if (loginData.message) { + // Clean up common Discord API error messages + const msg = loginData.message.toLowerCase(); + if (msg.includes('invalid') && (msg.includes('email') || msg.includes('phone') || msg.includes('login'))) { + errorMessage = "Invalid email or username"; + } else if (msg.includes('password')) { + errorMessage = "Invalid password"; + } else if (msg.includes('account')) { + errorMessage = "Account issue - check your credentials"; + } else if (msg.includes('rate limit') || msg.includes('too many')) { + errorMessage = "Too many attempts - please wait and try again"; + } else if (msg.includes('mfa') || msg.includes('2fa') || msg.includes('verification')) { + errorMessage = "2FA/MFA accounts not supported via credentials"; + } else { + errorMessage = "Login failed - check your credentials"; + } + } else if (loginData.errors) { + // Handle field-specific errors + const errors = loginData.errors; + if (errors.login) { + errorMessage = "Invalid email or username"; + } else if (errors.password) { + errorMessage = "Invalid password"; + } else { + errorMessage = "Invalid credentials"; + } + } else if (response.status === 401) { + errorMessage = "Invalid credentials"; + } else if (response.status === 429) { + errorMessage = "Rate limited - please wait and try again"; + } else if (response.status >= 500) { + errorMessage = "Discord servers are having issues - try again later"; + } + + showToast(errorMessage, 1); + setIsAddingDynamic(false); + return; + } + + addLog('debug', 'Login successful, fetching user info'); + const userResponse = await fetch("https://discord.com/api/v9/users/@me", { + headers: { "Authorization": loginData.token } + }); + + if (!userResponse.ok) { + addLog('error', 'Failed to get user info after login', { status: userResponse.status }); + showToast("Login succeeded but failed to get user info - check logs", 1); + setIsAddingDynamic(false); + return; + } + + const user = await userResponse.json(); + + if (storage.accounts[user.id]) { + addLog('warn', 'Account already exists', { username: user.username }); + showToast(`Account ${user.username} already saved`, 1); + setIsAddingDynamic(false); + return; + } + + const newAccount = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + displayName: user.global_name || user.username, + token: loginData.token, + addedAt: Date.now() + }; + + storage.accounts[user.id] = newAccount; + if (!storage.accountOrder.includes(user.id)) { + storage.accountOrder.push(user.id); + } + + setEmail(""); + setPassword(""); + setShowAddDialog(false); + addLog('info', 'Account added successfully via credentials', { username: user.username }); + showToast(`Account ${user.username} added successfully!`, 0); + } catch (e) { + addLog('error', 'Credential login failed', { error: e.message, stack: e.stack }); + console.error("Login error:", e); + + // Network or other errors + let errorMessage = "Login failed - check logs for more info"; + if (e.message.includes('network') || e.message.includes('fetch')) { + errorMessage = "Network error - check your connection"; + } else if (e.message.includes('timeout')) { + errorMessage = "Request timed out - try again"; + } + + showToast(errorMessage, 1); + } + setIsAddingDynamic(false); +}; + +export { + exportAccounts, + importAccounts, + setExportPassword, + removeExportPassword, + addAccountWithToken, + addAccountWithCredentials, + forceLogout +}; \ No newline at end of file diff --git a/plugins/MoreAlts/Logger.js b/plugins/MoreAlts/Logger.js new file mode 100644 index 0000000..3fc65a6 --- /dev/null +++ b/plugins/MoreAlts/Logger.js @@ -0,0 +1,33 @@ +import { storage } from "@vendetta/plugin"; + +const pluginLogs = []; +const maxLogs = 1000; + +function addLog(type, message, data = null) { + const logEntry = { + timestamp: new Date().toISOString(), + type: type, + message: message, + data: data + }; + + pluginLogs.unshift(logEntry); + if (pluginLogs.length > maxLogs) { + pluginLogs.pop(); + } + + if (storage?.settings?.enableUnsafeFeatures) { + console.log(`[AccountSwitcher] ${type.toUpperCase()}: ${message}`, data || ''); + } +} + +function getLogs() { + return pluginLogs; +} + +function clearLogs() { + pluginLogs.length = 0; + addLog('info', 'Logs cleared'); +} + +export { addLog, getLogs, clearLogs }; \ No newline at end of file diff --git a/plugins/MoreAlts/PassPrompt.js b/plugins/MoreAlts/PassPrompt.js new file mode 100644 index 0000000..469c555 --- /dev/null +++ b/plugins/MoreAlts/PassPrompt.js @@ -0,0 +1,95 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { showToast } from "@vendetta/ui/toasts"; +import { simpleHash } from "./PasswordUtils.js"; +import { addLog } from "./Logger.js"; + +function PasswordPrompt({ dialogInfo, onCancel, onSuccess, storage }) { + const [passwordInput, setPasswordInput] = React.useState(""); + + const handlePasswordDialog = () => { + if (!passwordInput.trim()) { + showToast("Please enter the password", 1); + return; + } + + const inputHash = simpleHash(passwordInput); + if (inputHash === storage.settings.exportPasswordHash) { + addLog('info', 'Password verification successful', { type: dialogInfo.type }); + onSuccess(); + } else { + addLog('warn', 'Incorrect password attempt', { type: dialogInfo.type }); + showToast("Incorrect password", 1); + } + }; + + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "pwd-header", + style: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "back", + onPress: onCancel, + style: { marginRight: 16 } + }, React.createElement(ReactNative.Text, { + style: { color: '#7289da', fontSize: 16 } + }, "← Cancel")), + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, dialogInfo.title) + ]), + + React.createElement(ReactNative.View, { + key: "pwd-content", + style: { flex: 1, padding: 16, justifyContent: 'center' } + }, [ + React.createElement(ReactNative.Text, { + key: "message", + style: { color: '#b9bbbe', fontSize: 16, marginBottom: 20, textAlign: 'center' } + }, dialogInfo.message), + + React.createElement(ReactNative.TextInput, { + key: "password-input", + placeholder: "Enter password", + placeholderTextColor: '#72767d', + value: passwordInput, + onChangeText: setPasswordInput, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 16, + borderRadius: 8, + marginBottom: 20, + fontSize: 16 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "confirm-btn", + onPress: handlePasswordDialog, + style: { + backgroundColor: '#7289da', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Confirm")) + ]) + ]); +} + +export { PasswordPrompt }; \ No newline at end of file diff --git a/plugins/MoreAlts/PasswordUtils.js b/plugins/MoreAlts/PasswordUtils.js new file mode 100644 index 0000000..b15e431 --- /dev/null +++ b/plugins/MoreAlts/PasswordUtils.js @@ -0,0 +1,12 @@ +function simpleHash(str) { + let hash = 0; + if (str.length === 0) return hash.toString(); + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return Math.abs(hash).toString(36); +} + +export { simpleHash }; \ No newline at end of file diff --git a/plugins/MoreAlts/Settings.js b/plugins/MoreAlts/Settings.js new file mode 100644 index 0000000..0b5de8b --- /dev/null +++ b/plugins/MoreAlts/Settings.js @@ -0,0 +1,1646 @@ +import { React, ReactNative } from "@vendetta/metro/common"; +import { storage } from "@vendetta/plugin"; +import { useProxy } from "@vendetta/storage"; +import { showConfirmationAlert } from "@vendetta/ui/alerts"; +import { showToast } from "@vendetta/ui/toasts"; +import { clipboard } from "@vendetta/metro/common"; +import { findByProps, findByStoreName } from "@vendetta/metro"; + + +import { addLog, getLogs, clearLogs } from "./Logger.js"; +import { PasswordPrompt } from "./PassPrompt.js"; +import { + exportAccounts, + importAccounts, + setExportPassword, + removeExportPassword, + addAccountWithToken, + addAccountWithCredentials, + forceLogout +} from "./AccountActions.js"; + +const UserStore = findByStoreName("UserStore"); +const TokenManager = findByProps("getToken"); + + +if (!storage.accounts) storage.accounts = {}; +if (!storage.accountOrder) storage.accountOrder = []; +if (!storage.settings) { + storage.settings = { + enableCLI: true, + confirmBeforeDelete: true, + enableUnsafeFeatures: false + }; +} else { + if (storage.settings.enableUnsafeFeatures === undefined) { + storage.settings.enableUnsafeFeatures = false; + } + if (storage.settings.enableCLI === undefined) { + storage.settings.enableCLI = storage.settings.showAccountNames !== false; // Migrate from old setting + } + if (storage.settings.confirmBeforeDelete === undefined) { + storage.settings.confirmBeforeDelete = true; + } + if (storage.settings.showAccountNames !== undefined) { + delete storage.settings.showAccountNames; + } +} + +addLog('info', 'AccountSwitcher plugin initialized', { + accountsCount: Object.keys(storage.accounts).length, + cliEnabled: storage.settings.enableCLI, + unsafeFeaturesEnabled: storage.settings.enableUnsafeFeatures +}); + +function SettingsPage({ onBack }) { + useProxy(storage); + + const [newToken, setNewToken] = React.useState(""); + const [isAdding, setIsAdding] = React.useState(false); + const [importText, setImportText] = React.useState(""); + const [showPasswordDialog, setShowPasswordDialog] = React.useState(null); + const [newPassword, setNewPassword] = React.useState(""); + const [confirmPassword, setConfirmPassword] = React.useState(""); + + const updateSetting = (key, value) => { + const newSettings = { ...storage.settings, [key]: value }; + storage.settings = newSettings; + addLog('info', `Setting updated: ${key}`, { newValue: value }); + }; + + const copyLogs = () => { + const logs = getLogs(); + const logsText = logs.map(log => + `[${log.timestamp}] ${log.type.toUpperCase()}: ${log.message}${log.data ? ' | Data: ' + JSON.stringify(log.data) : ''}` + ).join('\n'); + + clipboard.setString(logsText); + addLog('info', 'Logs copied to clipboard', { count: logs.length }); + showToast(`Copied ${logs.length} log entries to clipboard`, 0); + }; + + const enableUnsafeFeatures = (value) => { + if (value) { + showConfirmationAlert({ + React.createElement(ReactNative.View, { + key: "enable-cli", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 8 + } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Enable CLI Interface"), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.enableCLI, + onValueChange: (v) => updateSetting("enableCLI", v), + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]), + + React.createElement(ReactNative.View, { + key: "add-to-sidebar", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 8 + } + }, [ + React.createElement(ReactNative.View, { + key: "text-container", + style: { flex: 1 } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Add to Settings Sidebar"), + React.createElement(ReactNative.Text, { + key: "desc", + style: { color: '#72767d', fontSize: 12, marginTop: 2 } + }, "Show Account Switcher in Revenge settings menu") + ]), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.addToSidebar, + onValueChange: (v) => { + updateSetting("addToSidebar", v); + if (v) { + showToast("Sidebar enabled - restart app to see changes", 0); + } else { + showToast("Sidebar disabled - restart app to remove", 0); + } + }, + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]), + + React.createElement(ReactNative.View, { + key: "confirm-delete", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8 + } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Confirm Before Delete"), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.confirmBeforeDelete, + onValueChange: (v) => updateSetting("confirmBeforeDelete", v), + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]) + ]) + ]) + ]); +} + +export default function AccountsManager(props) { + useProxy(storage); + + const [showSettings, setShowSettings] = React.useState(false); + const [switchingTo, setSwitchingTo] = React.useState(null); + const [showAddDialog, setShowAddDialog] = React.useState(false); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [isAddingDynamic, setIsAddingDynamic] = React.useState(false); + + const currentUserId = UserStore.getCurrentUser()?.id; + + const removeAccount = (accountId) => { + const account = storage.accounts[accountId]; + if (!account) return; + + const isCurrent = accountId === currentUserId; + const accountName = account.username; + addLog('info', 'Attempting to remove account', { username: accountName, isCurrent }); + + const deleteFromStorage = () => { + delete storage.accounts[accountId]; + storage.accountOrder = storage.accountOrder.filter(id => id !== accountId); + }; + + const removeFromSwitcherOnly = () => { + deleteFromStorage(); + addLog('info', 'Account removed from switcher only', { username: accountName }); + showToast(`Account ${accountName} removed from switcher`, 0); + }; + + const logoutAndDelete = async () => { + try { + addLog('info', 'Starting account removal and logout', { username: accountName }); + const currentToken = TokenManager.getToken(); + + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token); + addLog('debug', 'Switched to account for logout'); + + await findByProps("login", "logout").logout(); + addLog('debug', 'Account logged out from Discord'); + + setTimeout(async () => { + try { + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(currentToken); + addLog('debug', 'Switched back to original account'); + } catch (e) { + addLog('warn', 'Could not switch back to original account', { error: e.message }); + } + }, 100); + } catch (e) { + addLog('error', 'Failed to logout account from Discord', { error: e.message }); + showToast("Failed to logout, but removed from switcher", 1); + } + + deleteFromStorage(); + addLog('info', 'Account removed and logged out', { username: accountName }); + showToast(`Account ${accountName} removed and logged out`, 0); + }; + + if (storage.settings.confirmBeforeDelete) { + ReactNative.Alert.alert( + isCurrent ? "Remove Current Account" : "Remove Account", + isCurrent + ? "Do you want to remove the current account from the switcher? (To logout, use Force Logout in settings)" + : `What do you want to do with ${accountName}?`, + [ + { text: "Cancel", style: "cancel" }, + { text: "Delete from switcher only", onPress: removeFromSwitcherOnly }, + !isCurrent && { text: "Delete and logout", onPress: logoutAndDelete }, + ].filter(Boolean) + ); + } else { + if (isCurrent) { + removeFromSwitcherOnly(); + } else { + logoutAndDelete(); + } + } + }; + + const switchToAccount = async (accountId) => { + const account = storage.accounts[accountId]; + if (!account) return; + setSwitchingTo(accountId); + + addLog('info', 'Starting account switch', { username: account.username }); + + try { + showToast(`Switching to ${account.username}...`, 0); + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token); + addLog('info', 'Account switch successful', { username: account.username }); + showToast(`Switched to ${account.username}!`, 0); + } catch (e) { + addLog('error', 'Account switch failed', { username: account.username, error: e.message }); + console.error("Switch error:", e); + showToast(`Failed to switch: ${e.message}`, 1); + } + setSwitchingTo(null); + }; + + const copyToken = (accountId) => { + if (!storage.settings.enableUnsafeFeatures) return; + const account = storage.accounts[accountId]; + if (!account) return; + + clipboard.setString(account.token); + addLog('info', 'Token copied to clipboard', { username: account.username }); + showToast(`Token for ${account.username} copied`, 0); + }; + + const addCurrentAccount = async () => { + setIsAddingDynamic(true); + addLog('info', 'Adding current account'); + + try { + const token = TokenManager.getToken(); + const currentUser = UserStore.getCurrentUser(); + + if (!currentUser || storage.accounts[currentUser.id]) { + const message = storage.accounts[currentUser.id] ? "Current account already saved" : "Failed to get current user"; + addLog('warn', message, { userId: currentUser?.id }); + showToast(message, 1); + setIsAddingDynamic(false); + return; + } + + const newAccount = { + id: currentUser.id, + username: currentUser.username, + discriminator: currentUser.discriminator, + avatar: currentUser.avatar, + displayName: currentUser.globalName || currentUser.username, + token: token, + addedAt: Date.now() + }; + + storage.accounts[currentUser.id] = newAccount; + if (!storage.accountOrder.includes(currentUser.id)) { + storage.accountOrder.push(currentUser.id); + } + + setShowAddDialog(false); + addLog('info', 'Current account added successfully', { username: currentUser.username }); + showToast(`Current account ${currentUser.username} added!`, 0); + } catch (e) { + addLog('error', 'Failed to add current account', { error: e.message }); + console.error("Add current account error:", e); + showToast("Failed to add current account", 1); + } + setIsAddingDynamic(false); + }; + + const addAccountWithCredentialsHandler = async () => { + await addAccountWithCredentials(storage, email, password, setEmail, setPassword, setShowAddDialog, setIsAddingDynamic); + }; + + const formatDate = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + }; + + const orderedAccounts = storage.accountOrder + .filter(id => storage.accounts[id]) + .map(id => storage.accounts[id]); + + if (showSettings) { + return React.createElement(SettingsPage, { + onBack: () => setShowSettings(false) + }); + } + + if (showAddDialog) { + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "add-header", + style: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "back", + onPress: () => { + setShowAddDialog(false); + setEmail(""); + setPassword(""); + }, + style: { marginRight: 16 } + }, React.createElement(ReactNative.Text, { + style: { color: '#7289da', fontSize: 16 } + }, "← Back")), + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Add Account") + ]), + + React.createElement(ReactNative.ScrollView, { + key: "add-content", + style: { flex: 1 }, + contentContainerStyle: { padding: 16, paddingBottom: 100 } + }, [ + React.createElement(ReactNative.Text, { + key: "instruction", + style: { color: '#b9bbbe', fontSize: 16, marginBottom: 20, textAlign: 'center' } + }, "Enter your Discord account credentials or add your current account"), + + React.createElement(ReactNative.TextInput, { + key: "email-input", + placeholder: "Email address", + placeholderTextColor: '#72767d', + value: email, + onChangeText: setEmail, + keyboardType: "email-address", + autoCapitalize: "none", + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 16, + borderRadius: 8, + marginBottom: 12, + fontSize: 16 + } + }), + + React.createElement(ReactNative.TextInput, { + key: "password-input", + placeholder: "Password", + placeholderTextColor: '#72767d', + value: password, + onChangeText: setPassword, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 16, + borderRadius: 8, + marginBottom: 20, + fontSize: 16 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "login-btn", + onPress: addAccountWithCredentialsHandler, + disabled: isAddingDynamic, + style: { + backgroundColor: isAddingDynamic ? '#5c6bc0' : '#7289da', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + marginBottom: 12, + opacity: isAddingDynamic ? 0.6 : 1 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, isAddingDynamic ? "Adding Account..." : "Add Account with Email & Password")), + + React.createElement(ReactNative.View, { + key: "divider", + style: { + height: 1, + backgroundColor: '#40444b', + marginVertical: 16 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "add-current-btn", + onPress: addCurrentAccount, + disabled: isAddingDynamic, + style: { + backgroundColor: isAddingDynamic ? '#5c6bc0' : '#43b581', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + opacity: isAddingDynamic ? 0.6 : 1 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, isAddingDynamic ? "Adding..." : "Add Current Account")) + ]) + ]); + } + + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "header", + style: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 20, fontWeight: 'bold' } + }, "MoreAlts"), + React.createElement(ReactNative.TouchableOpacity, { + key: "settings-btn", + onPress: () => setShowSettings(true), + style: { + padding: 8, + borderRadius: 6, + backgroundColor: '#4f545c' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18 } + }, "⚙️")) + ]), + + React.createElement(ReactNative.View, { + key: "accounts-section", + style: { flex: 1, padding: 16 } + }, [ + React.createElement(ReactNative.Text, { + key: "section-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, `SAVED ACCOUNTS (${orderedAccounts.length})`), + + orderedAccounts.length === 0 + ? React.createElement(ReactNative.View, { + key: "empty", + style: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40 + } + }, [ + React.createElement(ReactNative.Text, { + key: "empty-text", + style: { color: '#72767d', fontSize: 16, marginBottom: 8 } + }, "No accounts saved yet"), + React.createElement(ReactNative.Text, { + key: "empty-hint", + style: { color: '#72767d', fontSize: 14, textAlign: 'center' } + }, "Access the switcher by pressing the logout button on the main settings page.") + ]) + : React.createElement(ReactNative.ScrollView, { + key: "accounts-scroll", + style: { flex: 1 }, + contentContainerStyle: { paddingBottom: 100 } + }, orderedAccounts.map((account, index) => { + const isCurrent = account.id === currentUserId; + const isSwitching = switchingTo === account.id; + + let avatarUrl = `https://cdn.discordapp.com/embed/avatars/1.png`; + if (account.avatar) { + avatarUrl = `https://cdn.discordapp.com/avatars/${account.id}/${account.avatar}.png?size=48`; + } + + return React.createElement(ReactNative.View, { + key: account.id, + style: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: isCurrent ? '#7289da20' : '#36393f', + borderWidth: isCurrent ? 2 : 0, + borderColor: '#7289da', + borderRadius: 12, + padding: 12, + marginBottom: 12 + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "avatar", + onPress: () => switchToAccount(account.id), + disabled: isCurrent || isSwitching, + style: { marginRight: 12 } + }, React.createElement(ReactNative.Image, { + source: { uri: avatarUrl }, + style: { + width: 48, + height: 48, + borderRadius: 24, + opacity: (isCurrent || isSwitching) ? 0.7 : 1 + } + })), + + React.createElement(ReactNative.View, { + key: "info", + style: { flex: 1 } + }, [ + React.createElement(ReactNative.Text, { + key: "name", + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, `${index + 1}. ${storage.settings.enableCLI ? `${account.username}${account.discriminator !== "0" ? `#${account.discriminator}` : ''}` : account.displayName}`), + React.createElement(ReactNative.Text, { + key: "status", + style: { + color: isCurrent ? '#43b581' : '#72767d', + fontSize: 12, + marginTop: 2 + } + }, isCurrent ? "✓ Current Account" : + isSwitching ? "Switching..." : + "Tap avatar to switch"), + React.createElement(ReactNative.Text, { + key: "date", + style: { + color: '#72767d', + fontSize: 11, + marginTop: 2 + } + }, `Added: ${formatDate(account.addedAt || Date.now())}`) + ]), + + React.createElement(ReactNative.View, { + key: "actions", + style: { flexDirection: 'column', alignItems: 'flex-end' } + }, [ + storage.settings.enableUnsafeFeatures && React.createElement(ReactNative.TouchableOpacity, { + key: "copy", + onPress: () => copyToken(account.id), + style: { + backgroundColor: '#4f545c', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + minWidth: 80, + alignItems: 'center', + marginBottom: 6 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 12, fontWeight: 'bold' } + }, "Copy Token")), + + React.createElement(ReactNative.TouchableOpacity, { + key: "remove", + onPress: () => removeAccount(account.id), + style: { + backgroundColor: '#f04747', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + minWidth: 80, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 12, fontWeight: 'bold' } + }, "Remove")) + ]) + ]); + })) + ]), + + React.createElement(ReactNative.View, { + key: "bottom-actions", + style: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: '#202225', + borderTopWidth: 1, + borderTopColor: '#40444b', + paddingVertical: 12, + paddingHorizontal: 16, + paddingBottom: 40 + } + }, React.createElement(ReactNative.TouchableOpacity, { + key: "add", + onPress: () => setShowAddDialog(true), + style: { + backgroundColor: '#7289da', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center' + } + }, [ + React.createElement(ReactNative.Text, { + key: "add-icon", + style: { color: 'white', fontSize: 20, marginRight: 8 } + }, "+"), + React.createElement(ReactNative.Text, { + key: "add-text", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Add Account") + ])) + ]); +} + +export { addLog };title: "⚠️ Enable Unsafe Features - Security Warning", + content: "Unsafe features include token copying, manual token adding, and detailed logging. These features can compromise your account security if misused. Tokens provide full access to accounts and should never be shared. Only enable if you understand the security implications.", + confirmText: "I Understand - Enable", + cancelText: "Cancel", + confirmColor: "brand", + onConfirm: () => updateSetting("enableUnsafeFeatures", true) + }); + } else { + updateSetting("enableUnsafeFeatures", false); + } + }; + + // Password dialog handlers + const handlePasswordCancel = () => { + setShowPasswordDialog(null); + }; + + const handlePasswordSuccess = () => { + const callback = showPasswordDialog.callback; + setShowPasswordDialog(null); + callback(); + }; + + if (showPasswordDialog) { + return React.createElement(PasswordPrompt, { + dialogInfo: showPasswordDialog, + onCancel: handlePasswordCancel, + onSuccess: handlePasswordSuccess, + storage: storage + }); + } + + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "header", + style: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "back", + onPress: onBack, + style: { marginRight: 16 } + }, React.createElement(ReactNative.Text, { + style: { color: '#7289da', fontSize: 16 } + }, "← Back to Accounts")), + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Settings") + ]), + + React.createElement(ReactNative.ScrollView, { + key: "content", + style: { flex: 1, padding: 16 }, + contentContainerStyle: { paddingBottom: 100 } + }, [ + React.createElement(ReactNative.View, { + key: "export-import", + style: { marginBottom: 24 } + }, [ + React.createElement(ReactNative.Text, { + key: "export-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, "BACKUP & RESTORE"), + + React.createElement(ReactNative.TouchableOpacity, { + key: "export-btn", + onPress: () => exportAccounts(storage, setShowPasswordDialog), + style: { + backgroundColor: '#7289da', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 12, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, "📤 Export Accounts to Clipboard")), + + React.createElement(ReactNative.TextInput, { + key: "import-input", + placeholder: "Paste exported account data here (or leave empty to use clipboard)...", + placeholderTextColor: '#72767d', + value: importText, + onChangeText: setImportText, + multiline: true, + numberOfLines: 4, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 12, + borderRadius: 8, + marginBottom: 12, + fontSize: 14, + textAlignVertical: 'top' + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "import-btn", + onPress: () => importAccounts(storage, setShowPasswordDialog, importText), + style: { + backgroundColor: '#43b581', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 12, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, "📥 Import Accounts")), + + React.createElement(ReactNative.TouchableOpacity, { + key: "force-logout-btn", + onPress: forceLogout, + style: { + backgroundColor: '#f04747', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, "🚪 Force Logout")) + ]), + + React.createElement(ReactNative.View, { + key: "export-password", + style: { marginBottom: 24 } + }, [ + React.createElement(ReactNative.Text, { + key: "password-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, "EXPORT PASSWORD PROTECTION"), + + storage.settings.exportPasswordHash + ? React.createElement(ReactNative.View, { key: "password-set" }, [ + React.createElement(ReactNative.Text, { + key: "status", + style: { color: '#43b581', fontSize: 16, marginBottom: 12 } + }, "✓ Export password is set (hashed)"), + React.createElement(ReactNative.TouchableOpacity, { + key: "remove-pwd", + onPress: () => removeExportPassword(storage, setShowPasswordDialog), + style: { + backgroundColor: '#f04747', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, "Remove Password")) + ]) + : React.createElement(ReactNative.View, { key: "password-unset" }, [ + React.createElement(ReactNative.Text, { + key: "desc", + style: { color: '#72767d', fontSize: 14, marginBottom: 12 } + }, "Set a password to protect your account exports and imports (will be hashed for security)"), + + React.createElement(ReactNative.TextInput, { + key: "new-password", + placeholder: "New password", + placeholderTextColor: '#72767d', + value: newPassword, + onChangeText: setNewPassword, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 12, + borderRadius: 8, + marginBottom: 8, + fontSize: 14 + } + }), + + React.createElement(ReactNative.TextInput, { + key: "confirm-password", + placeholder: "Confirm password", + placeholderTextColor: '#72767d', + value: confirmPassword, + onChangeText: setConfirmPassword, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 12, + borderRadius: 8, + marginBottom: 12, + fontSize: 14 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "set-password", + onPress: () => setExportPassword(storage, newPassword, confirmPassword, setNewPassword, setConfirmPassword), + style: { + backgroundColor: '#7289da', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 8, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, "Set Password")) + ]) + ]), + + React.createElement(ReactNative.View, { + key: "unsafe", + style: { marginBottom: 24 } + }, [ + React.createElement(ReactNative.Text, { + key: "unsafe-title", + style: { color: '#f04747', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, "UNSAFE FEATURES"), + + React.createElement(ReactNative.View, { + key: "enable-unsafe", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 8 + } + }, [ + React.createElement(ReactNative.View, { + key: "text-container", + style: { flex: 1 } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Enable Unsafe Features"), + React.createElement(ReactNative.Text, { + key: "desc", + style: { color: '#72767d', fontSize: 12, marginTop: 2 } + }, "Allows token operations, detailed logging, and troubleshooting") + ]), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.enableUnsafeFeatures, + onValueChange: enableUnsafeFeatures, + trackColor: { false: '#72767d', true: '#f04747' }, + thumbColor: 'white' + }) + ]), + + storage.settings.enableUnsafeFeatures && React.createElement(ReactNative.View, { + key: "unsafe-features", + style: { marginTop: 16 } + }, [ + React.createElement(ReactNative.Text, { + key: "logging-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 8 } + }, "TROUBLESHOOTING & LOGGING"), + + React.createElement(ReactNative.View, { + key: "log-actions", + style: { flexDirection: 'row', gap: 8, marginBottom: 16 } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "copy-logs", + onPress: copyLogs, + style: { + flex: 1, + backgroundColor: '#7289da', + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 6, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 14, fontWeight: 'bold' } + }, `Copy Logs (${getLogs().length})`)), + + React.createElement(ReactNative.TouchableOpacity, { + key: "clear-logs", + onPress: clearLogs, + style: { + flex: 1, + backgroundColor: '#f04747', + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 6, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 14, fontWeight: 'bold' } + }, "Clear Logs")) + ]), + + React.createElement(ReactNative.Text, { + key: "token-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 8 } + }, "ADD ACCOUNT VIA TOKEN"), + + React.createElement(ReactNative.TextInput, { + key: "token-input", + placeholder: "Paste account token here (leave empty to add current account)...", + placeholderTextColor: '#72767d', + value: newToken, + onChangeText: setNewToken, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 12, + borderRadius: 8, + marginBottom: 12, + fontSize: 14 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "add-btn", + onPress: () => addAccountWithToken(storage, newToken, setNewToken, setIsAdding), + disabled: isAdding, + style: { + backgroundColor: isAdding ? '#5c6bc0' : '#7289da', + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + opacity: isAdding ? 0.6 : 1 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, isAdding ? "Adding..." : (newToken.trim() ? "Add Account" : "Add Current Account"))) + ]) + ]), + + React.createElement(ReactNative.View, { + key: "general", + style: { marginBottom: 24 } + }, [ + React.createElement(ReactNative.Text, { + key: "general-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, "GENERAL"), + + React.createElement(ReactNative.View, { + key: "enable-cli", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 8 + } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Enable CLI Interface"), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.enableCLI, + onValueChange: (v) => updateSetting("enableCLI", v), + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]), + + React.createElement(ReactNative.View, { + key: "add-to-sidebar", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8, + marginBottom: 8 + } + }, [ + React.createElement(ReactNative.View, { + key: "text-container", + style: { flex: 1 } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Add to Settings Sidebar"), + React.createElement(ReactNative.Text, { + key: "desc", + style: { color: '#72767d', fontSize: 12, marginTop: 2 } + }, "Show Account Switcher in Revenge settings menu") + ]), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.addToSidebar, + onValueChange: (v) => { + updateSetting("addToSidebar", v); + if (v) { + showToast("Sidebar enabled - restart app to see changes", 0); + } else { + showToast("Sidebar disabled - restart app to remove", 0); + } + }, + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]), + + React.createElement(ReactNative.View, { + key: "confirm-delete", + style: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + backgroundColor: '#36393f', + paddingHorizontal: 16, + borderRadius: 8 + } + }, [ + React.createElement(ReactNative.Text, { + key: "label", + style: { color: 'white', fontSize: 16 } + }, "Confirm Before Delete"), + React.createElement(ReactNative.Switch, { + key: "toggle", + value: storage.settings.confirmBeforeDelete, + onValueChange: (v) => updateSetting("confirmBeforeDelete", v), + trackColor: { false: '#72767d', true: '#7289da' }, + thumbColor: 'white' + }) + ]) + ]) + ]) + ]); +} + +export default function AccountsManager(props) { + useProxy(storage); + + const [showSettings, setShowSettings] = React.useState(false); + const [switchingTo, setSwitchingTo] = React.useState(null); + const [showAddDialog, setShowAddDialog] = React.useState(false); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [isAddingDynamic, setIsAddingDynamic] = React.useState(false); + + const currentUserId = UserStore.getCurrentUser()?.id; + + const removeAccount = (accountId) => { + const account = storage.accounts[accountId]; + if (!account) return; + + const isCurrent = accountId === currentUserId; + const accountName = account.username; + addLog('info', 'Attempting to remove account', { username: accountName, isCurrent }); + + const deleteFromStorage = () => { + delete storage.accounts[accountId]; + storage.accountOrder = storage.accountOrder.filter(id => id !== accountId); + }; + + const removeFromSwitcherOnly = () => { + deleteFromStorage(); + addLog('info', 'Account removed from switcher only', { username: accountName }); + showToast(`Account ${accountName} removed from switcher`, 0); + }; + + const logoutAndDelete = async () => { + try { + addLog('info', 'Starting account removal and logout', { username: accountName }); + const currentToken = TokenManager.getToken(); + + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token); + addLog('debug', 'Switched to account for logout'); + + await findByProps("login", "logout").logout(); + addLog('debug', 'Account logged out from Discord'); + + setTimeout(async () => { + try { + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(currentToken); + addLog('debug', 'Switched back to original account'); + } catch (e) { + addLog('warn', 'Could not switch back to original account', { error: e.message }); + } + }, 100); + } catch (e) { + addLog('error', 'Failed to logout account from Discord', { error: e.message }); + showToast("Failed to logout, but removed from switcher", 1); + } + + deleteFromStorage(); + addLog('info', 'Account removed and logged out', { username: accountName }); + showToast(`Account ${accountName} removed and logged out`, 0); + }; + + if (storage.settings.confirmBeforeDelete) { + ReactNative.Alert.alert( + isCurrent ? "Remove Current Account" : "Remove Account", + isCurrent + ? "Do you want to remove the current account from the switcher? (To logout, use Force Logout in settings)" + : `What do you want to do with ${accountName}?`, + [ + { text: "Cancel", style: "cancel" }, + { text: "Delete from switcher only", onPress: removeFromSwitcherOnly }, + !isCurrent && { text: "Delete and logout", onPress: logoutAndDelete }, + ].filter(Boolean) + ); + } else { + if (isCurrent) { + removeFromSwitcherOnly(); + } else { + logoutAndDelete(); + } + } + }; + + const switchToAccount = async (accountId) => { + const account = storage.accounts[accountId]; + if (!account) return; + setSwitchingTo(accountId); + + addLog('info', 'Starting account switch', { username: account.username }); + + try { + showToast(`Switching to ${account.username}...`, 0); + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token); + addLog('info', 'Account switch successful', { username: account.username }); + showToast(`Switched to ${account.username}!`, 0); + } catch (e) { + addLog('error', 'Account switch failed', { username: account.username, error: e.message }); + console.error("Switch error:", e); + showToast(`Failed to switch: ${e.message}`, 1); + } + setSwitchingTo(null); + }; + + const copyToken = (accountId) => { + if (!storage.settings.enableUnsafeFeatures) return; + const account = storage.accounts[accountId]; + if (!account) return; + + clipboard.setString(account.token); + addLog('info', 'Token copied to clipboard', { username: account.username }); + showToast(`Token for ${account.username} copied`, 0); + }; + + const addCurrentAccount = async () => { + setIsAddingDynamic(true); + addLog('info', 'Adding current account'); + + try { + const token = TokenManager.getToken(); + const currentUser = UserStore.getCurrentUser(); + + if (!currentUser || storage.accounts[currentUser.id]) { + const message = storage.accounts[currentUser.id] ? "Current account already saved" : "Failed to get current user"; + addLog('warn', message, { userId: currentUser?.id }); + showToast(message, 1); + setIsAddingDynamic(false); + return; + } + + const newAccount = { + id: currentUser.id, + username: currentUser.username, + discriminator: currentUser.discriminator, + avatar: currentUser.avatar, + displayName: currentUser.globalName || currentUser.username, + token: token, + addedAt: Date.now() + }; + + storage.accounts[currentUser.id] = newAccount; + if (!storage.accountOrder.includes(currentUser.id)) { + storage.accountOrder.push(currentUser.id); + } + + setShowAddDialog(false); + addLog('info', 'Current account added successfully', { username: currentUser.username }); + showToast(`Current account ${currentUser.username} added!`, 0); + } catch (e) { + addLog('error', 'Failed to add current account', { error: e.message }); + console.error("Add current account error:", e); + showToast("Failed to add current account", 1); + } + setIsAddingDynamic(false); + }; + + const addAccountWithCredentialsHandler = async () => { + await addAccountWithCredentials(storage, email, password, setEmail, setPassword, setShowAddDialog, setIsAddingDynamic); + }; + + const formatDate = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); + }; + + const orderedAccounts = storage.accountOrder + .filter(id => storage.accounts[id]) + .map(id => storage.accounts[id]); + + if (showSettings) { + return React.createElement(SettingsPage, { + onBack: () => setShowSettings(false) + }); + } + + if (showAddDialog) { + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "add-header", + style: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "back", + onPress: () => { + setShowAddDialog(false); + setEmail(""); + setPassword(""); + }, + style: { marginRight: 16 } + }, React.createElement(ReactNative.Text, { + style: { color: '#7289da', fontSize: 16 } + }, "← Back")), + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Add Account") + ]), + + React.createElement(ReactNative.ScrollView, { + key: "add-content", + style: { flex: 1 }, + contentContainerStyle: { padding: 16, paddingBottom: 100 } + }, [ + React.createElement(ReactNative.Text, { + key: "instruction", + style: { color: '#b9bbbe', fontSize: 16, marginBottom: 20, textAlign: 'center' } + }, "Enter your Discord account credentials or add your current account"), + + React.createElement(ReactNative.TextInput, { + key: "email-input", + placeholder: "Email address", + placeholderTextColor: '#72767d', + value: email, + onChangeText: setEmail, + keyboardType: "email-address", + autoCapitalize: "none", + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 16, + borderRadius: 8, + marginBottom: 12, + fontSize: 16 + } + }), + + React.createElement(ReactNative.TextInput, { + key: "password-input", + placeholder: "Password", + placeholderTextColor: '#72767d', + value: password, + onChangeText: setPassword, + secureTextEntry: true, + style: { + backgroundColor: '#40444b', + color: 'white', + padding: 16, + borderRadius: 8, + marginBottom: 20, + fontSize: 16 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "login-btn", + onPress: addAccountWithCredentialsHandler, + disabled: isAddingDynamic, + style: { + backgroundColor: isAddingDynamic ? '#5c6bc0' : '#7289da', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + marginBottom: 12, + opacity: isAddingDynamic ? 0.6 : 1 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, isAddingDynamic ? "Adding Account..." : "Add Account with Email & Password")), + + React.createElement(ReactNative.View, { + key: "divider", + style: { + height: 1, + backgroundColor: '#40444b', + marginVertical: 16 + } + }), + + React.createElement(ReactNative.TouchableOpacity, { + key: "add-current-btn", + onPress: addCurrentAccount, + disabled: isAddingDynamic, + style: { + backgroundColor: isAddingDynamic ? '#5c6bc0' : '#43b581', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + opacity: isAddingDynamic ? 0.6 : 1 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, isAddingDynamic ? "Adding..." : "Add Current Account")) + ]) + ]); + } + + return React.createElement(ReactNative.View, { + style: { flex: 1, backgroundColor: '#2f3136' } + }, [ + React.createElement(ReactNative.View, { + key: "header", + style: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + backgroundColor: '#202225', + borderBottomWidth: 1, + borderBottomColor: '#40444b' + } + }, [ + React.createElement(ReactNative.Text, { + key: "title", + style: { color: 'white', fontSize: 20, fontWeight: 'bold' } + }, "Account Switcher"), + React.createElement(ReactNative.TouchableOpacity, { + key: "settings-btn", + onPress: () => setShowSettings(true), + style: { + padding: 8, + borderRadius: 6, + backgroundColor: '#4f545c' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 18 } + }, "⚙️")) + ]), + + React.createElement(ReactNative.View, { + key: "accounts-section", + style: { flex: 1, padding: 16 } + }, [ + React.createElement(ReactNative.Text, { + key: "section-title", + style: { color: '#b9bbbe', fontSize: 14, fontWeight: 'bold', marginBottom: 12 } + }, `SAVED ACCOUNTS (${orderedAccounts.length})`), + + orderedAccounts.length === 0 + ? React.createElement(ReactNative.View, { + key: "empty", + style: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40 + } + }, [ + React.createElement(ReactNative.Text, { + key: "empty-text", + style: { color: '#72767d', fontSize: 16, marginBottom: 8 } + }, "No accounts saved yet"), + React.createElement(ReactNative.Text, { + key: "empty-hint", + style: { color: '#72767d', fontSize: 14, textAlign: 'center' } + }, "Access the switcher by pressing the logout button on the main settings page.") + ]) + : React.createElement(ReactNative.ScrollView, { + key: "accounts-scroll", + style: { flex: 1 }, + contentContainerStyle: { paddingBottom: 100 } + }, orderedAccounts.map((account, index) => { + const isCurrent = account.id === currentUserId; + const isSwitching = switchingTo === account.id; + + let avatarUrl = `https://cdn.discordapp.com/embed/avatars/1.png`; + if (account.avatar) { + avatarUrl = `https://cdn.discordapp.com/avatars/${account.id}/${account.avatar}.png?size=48`; + } + + return React.createElement(ReactNative.View, { + key: account.id, + style: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: isCurrent ? '#7289da20' : '#36393f', + borderWidth: isCurrent ? 2 : 0, + borderColor: '#7289da', + borderRadius: 12, + padding: 12, + marginBottom: 12 + } + }, [ + React.createElement(ReactNative.TouchableOpacity, { + key: "avatar", + onPress: () => switchToAccount(account.id), + disabled: isCurrent || isSwitching, + style: { marginRight: 12 } + }, React.createElement(ReactNative.Image, { + source: { uri: avatarUrl }, + style: { + width: 48, + height: 48, + borderRadius: 24, + opacity: (isCurrent || isSwitching) ? 0.7 : 1 + } + })), + + React.createElement(ReactNative.View, { + key: "info", + style: { flex: 1 } + }, [ + React.createElement(ReactNative.Text, { + key: "name", + style: { color: 'white', fontSize: 16, fontWeight: 'bold' } + }, `${index + 1}. ${storage.settings.enableCLI ? `${account.username}${account.discriminator !== "0" ? `#${account.discriminator}` : ''}` : account.displayName}`), + React.createElement(ReactNative.Text, { + key: "status", + style: { + color: isCurrent ? '#43b581' : '#72767d', + fontSize: 12, + marginTop: 2 + } + }, isCurrent ? "✓ Current Account" : + isSwitching ? "Switching..." : + "Tap avatar to switch"), + React.createElement(ReactNative.Text, { + key: "date", + style: { + color: '#72767d', + fontSize: 11, + marginTop: 2 + } + }, `Added: ${formatDate(account.addedAt || Date.now())}`) + ]), + + React.createElement(ReactNative.View, { + key: "actions", + style: { flexDirection: 'column', alignItems: 'flex-end' } + }, [ + storage.settings.enableUnsafeFeatures && React.createElement(ReactNative.TouchableOpacity, { + key: "copy", + onPress: () => copyToken(account.id), + style: { + backgroundColor: '#4f545c', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + minWidth: 80, + alignItems: 'center', + marginBottom: 6 + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 12, fontWeight: 'bold' } + }, "Copy Token")), + + React.createElement(ReactNative.TouchableOpacity, { + key: "remove", + onPress: () => removeAccount(account.id), + style: { + backgroundColor: '#f04747', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + minWidth: 80, + alignItems: 'center' + } + }, React.createElement(ReactNative.Text, { + style: { color: 'white', fontSize: 12, fontWeight: 'bold' } + }, "Remove")) + ]) + ]); + })) + ]), + + React.createElement(ReactNative.View, { + key: "bottom-actions", + style: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: '#202225', + borderTopWidth: 1, + borderTopColor: '#40444b', + paddingVertical: 12, + paddingHorizontal: 16, + paddingBottom: 40 + } + }, React.createElement(ReactNative.TouchableOpacity, { + key: "add", + onPress: () => setShowAddDialog(true), + style: { + backgroundColor: '#7289da', + paddingVertical: 16, + paddingHorizontal: 20, + borderRadius: 8, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center' + } + }, [ + React.createElement(ReactNative.Text, { + key: "add-icon", + style: { color: 'white', fontSize: 20, marginRight: 8 } + }, "+"), + React.createElement(ReactNative.Text, { + key: "add-text", + style: { color: 'white', fontSize: 18, fontWeight: 'bold' } + }, "Add Account") + ])) + ]); +} + +export { addLog }; \ No newline at end of file diff --git a/plugins/MoreAlts/SidebarPatcher.js b/plugins/MoreAlts/SidebarPatcher.js new file mode 100644 index 0000000..ab9155a --- /dev/null +++ b/plugins/MoreAlts/SidebarPatcher.js @@ -0,0 +1,199 @@ +import { storage } from "@vendetta/plugin"; +import { React, NavigationNative } from "@vendetta/metro/common"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { after } from "@vendetta/patcher"; +import { Forms } from "@vendetta/ui/components"; +import { findInReactTree } from "@vendetta/utils"; +import { findByProps } from "@vendetta/metro"; +import { logger } from "@vendetta"; +import AccountSwitcherSettings from "../Settings"; + +const { FormSection, FormRow } = Forms; +const { TableRowIcon } = findByProps("TableRowIcon"); + +const bunny = window.bunny; + +const tabsNavigationRef = bunny?.metro?.findByPropsLazy("getRootNavigationRef"); +const settingConstants = bunny?.metro?.findByPropsLazy("SETTING_RENDERER_CONFIG"); +const SettingsOverviewScreen = bunny?.metro?.findByNameLazy("SettingsOverviewScreen", false); + +function Section({ tabs }) { + const navigation = NavigationNative.useNavigation(); + + return React.createElement(FormRow, { + label: tabs.title(), + leading: React.createElement(FormRow.Icon, { source: tabs.icon }), + trailing: React.createElement(React.Fragment, {}, [ + tabs.trailing ? tabs.trailing() : null, + React.createElement(FormRow.Arrow, { key: "arrow" }) + ]), + onPress: () => { + const Component = tabs.page; + navigation.navigate("VendettaCustomPage", { + title: tabs.title(), + render: () => React.createElement(Component) + }); + } + }); +} + +function patchPanelUI(tabs, patches) { + try { + patches.push( + after( + "default", + bunny?.metro?.findByNameLazy("UserSettingsOverviewWrapper", false), + (_, ret) => { + const UserSettingsOverview = findInReactTree( + ret.props.children, + n => n.type?.name === "UserSettingsOverview" + ); + + if (UserSettingsOverview) { + patches.push( + after( + "render", + UserSettingsOverview.type.prototype, + (_args, res) => { + const sections = findInReactTree( + res.props.children, + n => n?.children?.[1]?.type === FormSection + )?.children; + + if (sections) { + const index = sections.findIndex((c) => + ["BILLING_SETTINGS", "PREMIUM_SETTINGS"].includes(c?.props?.label) + ); + + sections.splice( + -~index || 4, + 0, + React.createElement(Section, { key: tabs.key, tabs }) + ); + } + } + ) + ); + } + }, + true + ) + ); + } catch (error) { + logger.info("Panel UI patch failed graciously 💔", error); + } +} + +function patchTabsUI(tabs, patches) { + if (!settingConstants || !SettingsOverviewScreen || !tabsNavigationRef) { + console.warn("[AccountSwitcher] Missing required constants for tabs UI patch"); + return; + } + + const row = {}; + row[tabs.key] = { + type: "pressable", + title: tabs.title, + icon: tabs.icon, + IconComponent: tabs.icon && (() => { + const actualIconSource = (typeof tabs.icon === 'object' && tabs.icon.uri !== undefined) + ? tabs.icon.uri + : tabs.icon; + return React.createElement(TableRowIcon, { source: actualIconSource }); + }), + usePredicate: tabs.predicate, + useTrailing: tabs.trailing, + onPress: () => { + const navigation = tabsNavigationRef.getRootNavigationRef(); + const Component = tabs.page; + + navigation.navigate("VendettaCustomPage", { + title: tabs.title(), + render: () => React.createElement(Component) + }); + }, + withArrow: true, + }; + + let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG; + + Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", { + enumerable: true, + configurable: true, + get: () => ({ + ...rendererConfigValue, + ...row, + }), + set: v => (rendererConfigValue = v), + }); + + const firstRender = Symbol("AccountSwitcher-pinToSettings"); + + patches.push( + after("default", SettingsOverviewScreen, (args, ret) => { + if (!args[0][firstRender]) { + args[0][firstRender] = true; + + const { sections } = findInReactTree( + ret, + i => i.props?.sections, + ).props; + + const section = sections?.find((x) => + ["Bunny", "Revenge"].some( + mod => x.label === mod && x.title === mod, + ) + ); + + if (section?.settings) { + section.settings = [...section.settings, tabs.key]; + } + } + }) + ); +} + +function patchSettingsPin(tabs) { + const patches = []; + + let disabled = false; + + const realPredicate = tabs.predicate || (() => true); + tabs.predicate = () => (disabled ? false : realPredicate()); + + patchPanelUI(tabs, patches); + patchTabsUI(tabs, patches); + patches.push(() => (disabled = true)); + + return () => { + for (const x of patches) { + x(); + } + }; +} + +export default function patchSidebar() { + if (!storage.settings?.addToSidebar) { + console.log("[AccountSwitcher] Sidebar disabled in settings"); + return () => {}; + } + + console.log("[AccountSwitcher] Patching sidebar using custom patchSettingsPin..."); + + try { + const unpatch = patchSettingsPin({ + key: "AccountSwitcher", + icon: getAssetIDByName("UserIcon"), + title: () => "Account Switcher", + predicate: () => storage.settings?.addToSidebar === true, + page: AccountSwitcherSettings, + }); + + console.log("[AccountSwitcher] Successfully patched sidebar"); + return unpatch; + + } catch (error) { + console.error("[AccountSwitcher] Failed to patch sidebar:", error); + return () => {}; + } +} \ No newline at end of file diff --git a/plugins/MoreAlts/index.js b/plugins/MoreAlts/index.js new file mode 100644 index 0000000..cf546f4 --- /dev/null +++ b/plugins/MoreAlts/index.js @@ -0,0 +1,623 @@ +import * as common from "../../common"; +import { semanticColors } from "@vendetta/ui"; +import { registerCommand } from "@vendetta/commands"; +import { findByStoreName, findByProps } from "@vendetta/metro"; +import { findInReactTree } from "@vendetta/utils"; +import { setString } from "@vendetta/metro/common/clipboard"; +import { before as patchBefore } from "@vendetta/patcher"; +import { showToast } from "@vendetta/ui/toasts"; +import { encode as encodeTok, characters2 } from "../../common/numberBase64"; +import { storage } from "@vendetta/plugin"; +import { React } from "@vendetta/metro/common"; +import AccountSwitcherSettings from "./Settings"; +import patchSidebar from "./SidebarPatcher"; + +const { + meta: { resolveSemanticColor }, +} = findByProps("colors", "meta"); +const ThemeStore = findByStoreName("ThemeStore"); +const UserStore = findByStoreName("UserStore"); + +export const EMBED_COLOR = () => + parseInt(resolveSemanticColor(ThemeStore.theme, semanticColors.BACKGROUND_SECONDARY).slice(1), 16); + +const authorMods = { + author: { + username: "AccountSwitcher", + avatar: "command", + avatarURL: common.AVATARS?.command || "https://cdn.discordapp.com/embed/avatars/0.png", + }, +}; + +// Initialize storage structure +if (!storage.accounts) storage.accounts = {}; +if (!storage.accountOrder) storage.accountOrder = []; +if (!storage.settings) { + storage.settings = { + enableCLI: true, + confirmBeforeDelete: true, + enableUnsafeFeatures: false, + addToSidebar: true // Now using the correct patchSettingsPin method + }; +} else { + // Ensure all properties exist and migrate old settings + if (storage.settings.enableUnsafeFeatures === undefined) { + storage.settings.enableUnsafeFeatures = false; + } + if (storage.settings.enableCLI === undefined) { + storage.settings.enableCLI = storage.settings.showAccountNames !== false; + } + if (storage.settings.confirmBeforeDelete === undefined) { + storage.settings.confirmBeforeDelete = true; + } + if (storage.settings.addToSidebar === undefined) { + storage.settings.addToSidebar = true; // Now using correct method + } + // Remove old setting + if (storage.settings.showAccountNames !== undefined) { + delete storage.settings.showAccountNames; + } +} + +let madeSendMessage; +function sendMessage() { + if (window.sendMessage) return window.sendMessage?.(...arguments); + if (!madeSendMessage) madeSendMessage = common.mSendMessage?.(vendetta); + return madeSendMessage?.(...arguments); +} + +// Helper functions +function formatAccountName(account) { + if (!account || !account.username) return "Unknown Account"; + return account.username; +} + +function findAccountByInput(input) { + const accounts = storage.accounts || {}; + const accountOrder = storage.accountOrder || []; + + const index = parseInt(input); + if (!isNaN(index) && index >= 1 && index <= accountOrder.length) { + const accountId = accountOrder[index - 1]; + return accounts[accountId] ? [accountId, accounts[accountId]] : null; + } + + return Object.entries(accounts).find(([id, account]) => + account.username.toLowerCase() === input.toLowerCase() || + formatAccountName(account).toLowerCase() === input.toLowerCase() + ); +} + +function getAccountIndex(accountId) { + const accountOrder = storage.accountOrder || []; + const index = accountOrder.indexOf(accountId); + return index >= 0 ? index + 1 : null; +} + +function addAccountToOrder(accountId) { + if (!storage.accountOrder) storage.accountOrder = []; + if (!storage.accountOrder.includes(accountId)) { + storage.accountOrder.push(accountId); + } +} + +function removeAccountFromOrder(accountId) { + if (!storage.accountOrder) storage.accountOrder = []; + const index = storage.accountOrder.indexOf(accountId); + if (index > -1) { + storage.accountOrder.splice(index, 1); + } +} + +export default { + meta: vendetta.plugin, + settings: AccountSwitcherSettings, + patches: [], + + onUnload() { + this.patches.forEach((up) => up()); + this.patches = []; + }, + + onLoad() { + // Add sidebar patcher + const sidebarUnpatch = patchSidebar(); + this.patches.push(sidebarUnpatch); + + // Context menu patch for copying tokens + const optionLabel = "Copy Token"; + const contextMenuUnpatch = patchBefore("render", findByProps("ScrollView").View, (args) => { + try { + if (!storage.settings?.enableUnsafeFeatures) return; + + let a = findInReactTree(args, (r) => r.key === ".$UserProfileOverflow"); + if (!a || !a.props || a.props.sheetKey !== "UserProfileOverflow") return; + + const props = a.props.content.props; + if (props.options.some((option) => option?.label === optionLabel)) return; + + const currentUserId = UserStore.getCurrentUser()?.id; + const focusedUserId = Object.keys(a._owner.stateNode._keyChildMapping) + .find((str) => a._owner.stateNode._keyChildMapping[str] && str.match(/(?<=\$UserProfile)\d+/)) + ?.slice?.(".$UserProfile".length) || currentUserId; + const token = findByProps("getToken").getToken(); + + props.options.unshift({ + isDestructive: true, + label: optionLabel, + onPress: () => { + try { + showToast(focusedUserId === currentUserId ? `Copied your token` : `Copied token of ${props.header.title}`); + setString( + focusedUserId === currentUserId + ? token + : [ + Buffer.from(focusedUserId).toString("base64").replaceAll("=",""), + encodeTok?.(+Date.now() - 1293840000, true) || "dummy", + common.generateRandomString?.(characters2, 27) || "dummy", + ].join(".") + ); + props.hideActionSheet(); + } catch (e) { + console.error(e); + showToast("Failed to copy token", 1); + } + }, + }); + } catch (e) { + console.error("Context menu patch error:", e); + } + }); + + this.patches.push(contextMenuUnpatch); + + // Command implementations + const commands = { + async mainCommand(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher", + user: UserStore.getCurrentUser(), + }, + }; + + sendMessage?.({ + loggingName: "AccSwitcher main message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: "📱 Account Switcher", + description: storage.settings.addToSidebar + ? "Account Manager is available in your settings sidebar!\n\nQuick commands:\n• `/accswitcher login <#>` - Switch to account\n• `/accswitcher add` - Add current account\n• `/accswitcher list` - Show all accounts" + : "Opening Account Manager...\n\nQuick commands:\n• `/accswitcher login <#>` - Switch to account\n• `/accswitcher add` - Add current account\n• `/accswitcher list` - Show all accounts", + footer: { text: storage.settings.addToSidebar ? "Check your settings sidebar for Account Switcher" : "Check plugin settings for the Account Manager interface" } + }], + }, messageMods); + + showToast(storage.settings.addToSidebar ? "Check settings sidebar for Account Switcher" : "Check plugin settings to access Account Manager", 0); + } catch (e) { + console.error("Main command error:", e); + showToast("Check plugin settings to access Account Manager", 1); + } + }, + + async addAccount(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher add", + user: UserStore.getCurrentUser(), + }, + }; + + const token = findByProps("getToken").getToken(); + const currentUser = UserStore.getCurrentUser(); + + if (!currentUser) { + sendMessage?.({ + loggingName: "Add account error message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ Failed to Get Current User`, + description: "Could not retrieve current user information." + }] + }, messageMods); + return; + } + + if (storage.accounts[currentUser.id]) { + const index = getAccountIndex(currentUser.id); + sendMessage?.({ + loggingName: "Add account warning message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `⚠️ Account Already Saved`, + description: `Your current account **${index}. ${currentUser.username}** is already saved.` + }] + }, messageMods); + return; + } + + const newAccount = { + id: currentUser.id, + username: currentUser.username, + discriminator: currentUser.discriminator, + avatar: currentUser.avatar, + displayName: currentUser.globalName || currentUser.username, + token: token, + addedAt: Date.now() + }; + + storage.accounts[currentUser.id] = newAccount; + addAccountToOrder(currentUser.id); + + const index = getAccountIndex(currentUser.id); + sendMessage?.({ + loggingName: "Add account success message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `✅ Account Added Successfully`, + description: `**${index}. ${currentUser.username}** has been saved.`, + footer: { text: `Total accounts: ${Object.keys(storage.accounts).length}` } + }] + }, messageMods); + + } catch (e) { + console.error("Add account error:", e); + showToast("Failed to add account", 1); + } + }, + + async login(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher login", + user: UserStore.getCurrentUser(), + }, + }; + + const options = new Map(args.map((a) => [a.name, a])); + const accountInput = options.get("account")?.value; + + if (!accountInput) { + sendMessage?.({ + loggingName: "Login error message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ No Account Specified`, + description: "Please specify an account number, username, or ID." + }] + }, messageMods); + return; + } + + const found = findAccountByInput(accountInput); + if (!found) { + const availableAccounts = (storage.accountOrder || []) + .filter(id => storage.accounts[id]) + .map((id, index) => `${index + 1}. ${formatAccountName(storage.accounts[id])}`) + .join("\n"); + + sendMessage?.({ + loggingName: "Login error message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ Account Not Found`, + description: `Could not find account "${accountInput}".${availableAccounts ? `\n\n**Available accounts:**\n${availableAccounts}` : '\n\nNo accounts saved yet. Use `/accswitcher add` first.'}` + }] + }, messageMods); + return; + } + + const [accountId, account] = found; + const index = getAccountIndex(accountId); + + sendMessage?.({ + loggingName: "Login process message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `🔄 Switching to ${index}. ${formatAccountName(account)}...` + }] + }, messageMods); + + try { + await findByProps("login", "logout", "switchAccountToken").switchAccountToken(account.token); + showToast(`Switched to ${index}. ${formatAccountName(account)}!`, 0); + } catch (switchError) { + console.error("Switch error:", switchError); + sendMessage?.({ + loggingName: "Login failure message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ Failed to switch accounts`, + description: `Error: ${switchError.message}` + }] + }, messageMods); + } + + } catch (e) { + console.error("Login command error:", e); + showToast("Failed to execute login command", 1); + } + }, + + listAccounts(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher list", + user: UserStore.getCurrentUser(), + }, + }; + + const accounts = storage.accounts || {}; + const accountCount = Object.keys(accounts).length; + + if (accountCount === 0) { + sendMessage?.({ + loggingName: "List accounts empty message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: "📝 No Saved Accounts", + description: "Use `/accswitcher add` to save your current account." + }] + }, messageMods); + return; + } + + const accountList = (storage.accountOrder || []) + .filter(id => accounts[id]) + .map((id, index) => { + const account = accounts[id]; + const name = formatAccountName(account); + const addedDate = new Date(account.addedAt).toLocaleDateString(); + return `**${index + 1}.** ${name} - Added: ${addedDate}`; + }) + .join("\n"); + + sendMessage?.({ + loggingName: "List accounts message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `📝 Saved Accounts (${accountCount})`, + description: accountList, + footer: { text: "Use /accswitcher login to switch accounts" } + }] + }, messageMods); + + } catch (e) { + console.error("List accounts error:", e); + showToast("Failed to list accounts", 1); + } + }, + + removeAccount(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher remove", + user: UserStore.getCurrentUser(), + }, + }; + + const options = new Map(args.map((a) => [a.name, a])); + const accountInput = options.get("account")?.value; + + if (!accountInput) { + sendMessage?.({ + loggingName: "Remove account error message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ No Account Specified`, + description: "Please specify an account number, username, or ID to remove." + }] + }, messageMods); + return; + } + + const found = findAccountByInput(accountInput); + if (!found) { + const availableAccounts = (storage.accountOrder || []) + .filter(id => storage.accounts[id]) + .map((id, index) => `${index + 1}. ${formatAccountName(storage.accounts[id])}`) + .join("\n"); + + sendMessage?.({ + loggingName: "Remove account error message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `❌ Account Not Found`, + description: `Could not find account "${accountInput}".${availableAccounts ? `\n\n**Available accounts:**\n${availableAccounts}` : '\n\nNo accounts saved yet.'}` + }] + }, messageMods); + return; + } + + const [accountId, account] = found; + const index = getAccountIndex(accountId); + const accountName = `${index}. ${formatAccountName(account)}`; + + delete storage.accounts[accountId]; + removeAccountFromOrder(accountId); + + sendMessage?.({ + loggingName: "Remove account success message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: `🗑️ Account Removed`, + description: `**${accountName}** has been removed from saved accounts.`, + footer: { text: `Remaining accounts: ${Object.keys(storage.accounts).length}` } + }] + }, messageMods); + + } catch (e) { + console.error("Remove account error:", e); + showToast("Failed to remove account", 1); + } + }, + + getCurrentToken(args, ctx) { + try { + const messageMods = { + ...authorMods, + interaction: { + name: "/accswitcher token", + user: UserStore.getCurrentUser(), + }, + }; + + if (!storage.settings?.enableUnsafeFeatures) { + sendMessage?.({ + loggingName: "Get token disabled message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: "🔒 Unsafe Features Disabled", + description: "Token access is disabled. Enable 'Unsafe Features' in plugin settings to use this command." + }] + }, messageMods); + return; + } + + const { getToken } = findByProps("getToken"); + const token = getToken(); + + sendMessage?.({ + loggingName: "Get token message", + channelId: ctx.channel.id, + embeds: [{ + color: EMBED_COLOR(), + type: "rich", + title: "🔑 Current Account Token", + description: `\`${token}\``, + footer: { text: "⚠️ Keep your token secure and never share it!" } + }] + }, messageMods); + + } catch (e) { + console.error("Get token error:", e); + showToast("Failed to get current token", 1); + } + } + }; + + // Register commands + try { + const commandsToRegister = [ + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.mainCommand, + name: "accswitcher", + description: "Open Account Switcher (check plugin settings)", + options: [], + }, + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.addAccount, + name: "accswitcher add", + description: "Add your current account to saved accounts", + options: [], + }, + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.login, + name: "accswitcher login", + description: "Switch to a saved account", + options: [ + { + required: true, + type: 3, + name: "account", + description: "Account number, username, or ID to switch to", + } + ], + }, + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.listAccounts, + name: "accswitcher list", + description: "Show all saved accounts", + options: [], + }, + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.removeAccount, + name: "accswitcher remove", + description: "Remove a saved account", + options: [ + { + required: true, + type: 3, + name: "account", + description: "Account number, username, or ID to remove", + } + ], + }, + { + type: 1, + inputType: 1, + applicationId: "-1", + execute: commands.getCurrentToken, + name: "accswitcher token", + description: "Get your current account token (requires unsafe features)", + options: [], + } + ]; + + commandsToRegister.forEach(command => { + try { + registerCommand(command); + } catch (e) { + console.error(`Failed to register command ${command.name}:`, e); + } + }); + + console.log("Account Switcher plugin loaded successfully"); + } catch (e) { + console.error("Failed to register commands:", e); + } + } +}; \ No newline at end of file diff --git a/plugins/MoreAlts/manifest.json b/plugins/MoreAlts/manifest.json new file mode 100644 index 0000000..2c5d150 --- /dev/null +++ b/plugins/MoreAlts/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "More Alts!", + "description": "Adds ability to safely add more alts to Discord!", + "authors": [{ "name": "Win8.1VMUser", "id": "793935599702507542" }], + "main": "./index.js", + "vendetta": { "icon": "ic_full_server_gating_24px" } +}