Add files via upload
This commit is contained in:
parent
2f16ec92a5
commit
00ae0a594c
8 changed files with 3429 additions and 0 deletions
814
plugins/MoreAlts/AccountActions.js
Normal file
814
plugins/MoreAlts/AccountActions.js
Normal file
|
|
@ -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
|
||||
};
|
||||
33
plugins/MoreAlts/Logger.js
Normal file
33
plugins/MoreAlts/Logger.js
Normal file
|
|
@ -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 };
|
||||
95
plugins/MoreAlts/PassPrompt.js
Normal file
95
plugins/MoreAlts/PassPrompt.js
Normal file
|
|
@ -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 };
|
||||
12
plugins/MoreAlts/PasswordUtils.js
Normal file
12
plugins/MoreAlts/PasswordUtils.js
Normal file
|
|
@ -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 };
|
||||
1646
plugins/MoreAlts/Settings.js
Normal file
1646
plugins/MoreAlts/Settings.js
Normal file
File diff suppressed because it is too large
Load diff
199
plugins/MoreAlts/SidebarPatcher.js
Normal file
199
plugins/MoreAlts/SidebarPatcher.js
Normal file
|
|
@ -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 () => {};
|
||||
}
|
||||
}
|
||||
623
plugins/MoreAlts/index.js
Normal file
623
plugins/MoreAlts/index.js
Normal file
|
|
@ -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 <number or username> 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
7
plugins/MoreAlts/manifest.json
Normal file
7
plugins/MoreAlts/manifest.json
Normal file
|
|
@ -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" }
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue