release/v1.3.1 #8
@@ -188,6 +188,57 @@ const handleSwitchAccount = async (loginName: string) => {
|
|||||||
} catch (e) { return false; }
|
} catch (e) { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Scraper Helper ---
|
||||||
|
const scrapeAccountData = async (account: Account) => {
|
||||||
|
const now = new Date();
|
||||||
|
try {
|
||||||
|
// 1. Refresh Basic Profile & Bans
|
||||||
|
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
|
||||||
|
const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure);
|
||||||
|
|
||||||
|
account.personaName = profile.personaName;
|
||||||
|
account.profileUrl = profile.profileUrl;
|
||||||
|
account.vacBanned = bans.vacBanned;
|
||||||
|
account.gameBans = bans.gameBans;
|
||||||
|
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
|
||||||
|
account.lastBanCheck = now.toISOString();
|
||||||
|
|
||||||
|
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
|
||||||
|
account.avatar = profile.avatar;
|
||||||
|
const localPath = await downloadAvatar(account.steamId, profile.avatar);
|
||||||
|
if (localPath) account.localAvatar = localPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Refresh Cooldowns if session is active
|
||||||
|
if (account.steamLoginSecure) {
|
||||||
|
try {
|
||||||
|
const result = await scrapeCooldown(account.steamId, account.steamLoginSecure);
|
||||||
|
account.authError = false;
|
||||||
|
account.lastScrapeTime = now.toISOString();
|
||||||
|
if (result.isActive) {
|
||||||
|
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
||||||
|
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
|
||||||
|
} else {
|
||||||
|
account.cooldownExpiresAt = undefined;
|
||||||
|
if (backend) await backend.pushCooldown(account.steamId, undefined);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Share updated state with backend
|
||||||
|
if (backend && !account._id.startsWith('shared_')) {
|
||||||
|
await backend.shareAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Scraper] Failed to scrape ${account.personaName}:`, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Sync Worker ---
|
// --- Sync Worker ---
|
||||||
const syncAccounts = async () => {
|
const syncAccounts = async () => {
|
||||||
initBackend();
|
initBackend();
|
||||||
@@ -244,30 +295,13 @@ const syncAccounts = async () => {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
|
// OPTIMIZATION: Ensure ALL authenticated accounts are shared with the server on every sync cycle
|
||||||
// this guarantees that even if a push failed previously, it will be reconciled now.
|
|
||||||
if (backend && !account._id.startsWith('shared_')) {
|
if (backend && !account._id.startsWith('shared_')) {
|
||||||
console.log(`[Sync] Reconciling account with server: ${account.personaName}`);
|
|
||||||
await backend.shareAccount(account);
|
await backend.shareAccount(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
const lastCheck = account.lastBanCheck ? new Date(account.lastBanCheck) : new Date(0);
|
||||||
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
if ((now.getTime() - lastCheck.getTime()) / 3600000 > 6 || !account.personaName) {
|
||||||
const profile = await fetchProfileData(account.steamId, account.steamLoginSecure);
|
await scrapeAccountData(account);
|
||||||
const bans = await scrapeBanStatus(profile.profileUrl, account.steamLoginSecure);
|
|
||||||
account.personaName = profile.personaName; account.profileUrl = profile.profileUrl;
|
|
||||||
account.vacBanned = bans.vacBanned; account.gameBans = bans.gameBans;
|
|
||||||
account.status = (bans.vacBanned || bans.gameBans > 0) ? 'banned' : 'none';
|
|
||||||
account.lastBanCheck = now.toISOString();
|
|
||||||
if (profile.avatar && (!account.localAvatar || profile.avatar !== account.avatar)) {
|
|
||||||
account.avatar = profile.avatar;
|
|
||||||
const localPath = await downloadAvatar(account.steamId, profile.avatar);
|
|
||||||
if (localPath) account.localAvatar = localPath;
|
|
||||||
}
|
|
||||||
if (account.loginName) {
|
|
||||||
const config = steamClient.extractAccountConfig(account.loginName);
|
|
||||||
if (config) { account.loginConfig = config; account.sessionUpdatedAt = new Date().toISOString(); }
|
|
||||||
}
|
|
||||||
if (backend) await backend.shareAccount(account);
|
|
||||||
scrapeChanges = true;
|
scrapeChanges = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,20 +310,8 @@ const syncAccounts = async () => {
|
|||||||
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
|
const lastScrape = account.lastScrapeTime ? new Date(account.lastScrapeTime) : new Date(0);
|
||||||
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
|
if ((now.getTime() - lastScrape.getTime()) / 3600000 > 8) {
|
||||||
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
|
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 60000) + 5000));
|
||||||
try {
|
await scrapeAccountData(account);
|
||||||
const result = await scrapeCooldown(account.steamId, account.steamLoginSecure);
|
scrapeChanges = true;
|
||||||
account.authError = false; account.lastScrapeTime = new Date().toISOString();
|
|
||||||
if (result.isActive) {
|
|
||||||
account.cooldownExpiresAt = result.expiresAt ? result.expiresAt.toISOString() : new Date(Date.now() + 86400000).toISOString();
|
|
||||||
if (backend) await backend.pushCooldown(account.steamId, account.cooldownExpiresAt);
|
|
||||||
} else if (account.cooldownExpiresAt) {
|
|
||||||
account.cooldownExpiresAt = undefined;
|
|
||||||
if (backend) await backend.pushCooldown(account.steamId, undefined);
|
|
||||||
}
|
|
||||||
scrapeChanges = true;
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.message.includes('cookie') || e.message.includes('Sign In')) { account.authError = true; scrapeChanges = true; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
@@ -437,6 +459,22 @@ ipcMain.handle('login-to-server', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
|
ipcMain.handle('get-server-user-info', () => ({ steamId: store.get('serverConfig').serverSteamId }));
|
||||||
ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; });
|
ipcMain.handle('sync-now', async () => { await syncAccounts(); return true; });
|
||||||
|
|
||||||
|
ipcMain.handle('scrape-account', async (event, steamId: string) => {
|
||||||
|
const accounts = store.get('accounts') as Account[];
|
||||||
|
const account = accounts.find(a => a.steamId === steamId);
|
||||||
|
if (!account) return false;
|
||||||
|
|
||||||
|
console.log(`[Main] Manually triggering scrape for ${account.personaName}...`);
|
||||||
|
const success = await scrapeAccountData(account);
|
||||||
|
if (success) {
|
||||||
|
store.set('accounts', accounts);
|
||||||
|
if (mainWindow) mainWindow.webContents.send('accounts-updated', accounts);
|
||||||
|
updateTrayMenu();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('add-account', async (event, { identifier }) => {
|
ipcMain.handle('add-account', async (event, { identifier }) => {
|
||||||
try {
|
try {
|
||||||
initBackend();
|
initBackend();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loginToServer: () => ipcRenderer.invoke('login-to-server'),
|
loginToServer: () => ipcRenderer.invoke('login-to-server'),
|
||||||
getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'),
|
getServerUserInfo: () => ipcRenderer.invoke('get-server-user-info'),
|
||||||
syncNow: () => ipcRenderer.invoke('sync-now'),
|
syncNow: () => ipcRenderer.invoke('sync-now'),
|
||||||
|
scrapeAccount: (steamId: string) => ipcRenderer.invoke('scrape-account', steamId),
|
||||||
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
|
getCommunityAccounts: () => ipcRenderer.invoke('get-community-accounts'),
|
||||||
getServerUsers: () => ipcRenderer.invoke('get-server-users'),
|
getServerUsers: () => ipcRenderer.invoke('get-server-users'),
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ interface AccountsContextType {
|
|||||||
updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>;
|
updateServerConfig: (config: Partial<ServerConfig>) => Promise<void>;
|
||||||
loginToServer: () => Promise<void>;
|
loginToServer: () => Promise<void>;
|
||||||
syncNow: () => Promise<void>;
|
syncNow: () => Promise<void>;
|
||||||
|
scrapeAccount: (steamId: string) => Promise<boolean>;
|
||||||
getCommunityAccounts: () => Promise<any[]>;
|
getCommunityAccounts: () => Promise<any[]>;
|
||||||
getServerUsers: () => Promise<any[]>;
|
getServerUsers: () => Promise<any[]>;
|
||||||
refreshAccounts: (showLoading?: boolean) => Promise<void>;
|
refreshAccounts: (showLoading?: boolean) => Promise<void>;
|
||||||
@@ -114,6 +115,12 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrapeAccount = async (steamId: string) => {
|
||||||
|
const success = await (window as any).electronAPI.scrapeAccount(steamId);
|
||||||
|
if (success) await syncNow();
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
const addAccount = async (data: { identifier: string }) => {
|
const addAccount = async (data: { identifier: string }) => {
|
||||||
await (window as any).electronAPI.addAccount(data);
|
await (window as any).electronAPI.addAccount(data);
|
||||||
await refreshAccounts();
|
await refreshAccounts();
|
||||||
@@ -194,7 +201,7 @@ export const AccountsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
|
accounts, serverConfig, isLoading, isSyncing, addAccount, updateAccount, deleteAccount,
|
||||||
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
|
switchAccount, openSteamAppLogin, openSteamLogin, updateServerConfig, loginToServer,
|
||||||
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts,
|
getCommunityAccounts, getServerUsers, shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, syncNow, refreshAccounts,
|
||||||
adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
|
scrapeAccount, adminGetStats, adminGetUsers, adminDeleteUser, adminGetAccounts, adminRemoveAccount
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AccountsContext.Provider>
|
</AccountsContext.Provider>
|
||||||
|
|||||||
@@ -376,11 +376,12 @@ const AccountRow: React.FC<{
|
|||||||
onSwitch: (login: string) => void,
|
onSwitch: (login: string) => void,
|
||||||
onAuth: () => void
|
onAuth: () => void
|
||||||
}> = ({ account, onDelete, onSwitch, onAuth }) => {
|
}> = ({ account, onDelete, onSwitch, onAuth }) => {
|
||||||
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig } = useAccounts();
|
const { shareAccountWithUser, revokeAccountAccess, revokeAllAccountAccess, getServerUsers, serverConfig, scrapeAccount } = useAccounts();
|
||||||
const [timeLeft, setTimeLeft] = useState<string | null>(null);
|
const [timeLeft, setTimeLeft] = useState<string | null>(null);
|
||||||
const [isShareOpen, setIsShareOpen] = useState(false);
|
const [isShareOpen, setIsShareOpen] = useState(false);
|
||||||
const [targetUserId, setTargetUserId] = useState('');
|
const [targetUserId, setTargetUserId] = useState('');
|
||||||
const [isSharing, setIsSharing] = useState(false);
|
const [isSharing, setIsSharing] = useState(false);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [serverUsers, setServerUsers] = useState<any[]>([]);
|
const [serverUsers, setServerUsers] = useState<any[]>([]);
|
||||||
|
|
||||||
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
|
const cooldownDate = account?.cooldownExpiresAt ? new Date(account.cooldownExpiresAt) : null;
|
||||||
@@ -404,6 +405,12 @@ const AccountRow: React.FC<{
|
|||||||
const [imgSrc, setImgSrc] = useState(avatarSrc);
|
const [imgSrc, setImgSrc] = useState(avatarSrc);
|
||||||
useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]);
|
useEffect(() => { setImgSrc(avatarSrc); }, [avatarSrc]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await scrapeAccount(account.steamId);
|
||||||
|
setIsRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenShare = async () => {
|
const handleOpenShare = async () => {
|
||||||
setIsShareOpen(true);
|
setIsShareOpen(true);
|
||||||
try {
|
try {
|
||||||
@@ -522,7 +529,12 @@ const AccountRow: React.FC<{
|
|||||||
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
|
{account.steamLoginSecure && !account.authError ? <VerifiedUserIcon fontSize="inherit" /> : (account.authError ? <LockResetIcon fontSize="inherit" /> : <BoltIcon fontSize="inherit" />)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{account.steamLoginSecure && !account.authError && (
|
{account.steamLoginSecure && !account.authError && (
|
||||||
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 'bold', fontSize: '0.6rem' }}>TRACKING</Typography>
|
||||||
|
<IconButton size="small" onClick={handleRefresh} disabled={isRefreshing} sx={{ p: 0.2, color: 'text.secondary', '&:hover': { color: 'primary.main' } }}>
|
||||||
|
{isRefreshing ? <CircularProgress size={10} color="inherit" /> : <SyncIcon sx={{ fontSize: 12 }} />}
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Reference in New Issue
Block a user