Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,4 @@ jobs:
- name: Build application
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn run build

- name: Upload build artifacts
uses: actions/upload-artifact@v5
with:
name: build-${{ matrix.os }}
path: dist/
retention-days: 7
run: yarn run build
6 changes: 3 additions & 3 deletions .github/workflows/version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,23 @@ jobs:
echo "Generating changelog from $PREV_TAG to ${{ steps.version.outputs.new_version }}"

# Features (feat:, add:, new:)
FEATURES=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -E "^- (feat|add|new):" | sed 's/^- (feat|add|new): /- /' || true)
FEATURES=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -E "^(\[skip ci\] )?(feat|add|new):" | sed -E 's/^(\[skip ci\] )?(feat|add|new): /- /' || true)
if [ -n "$FEATURES" ]; then
echo "### New Features" >> release_notes.md
echo "$FEATURES" >> release_notes.md
echo "" >> release_notes.md
fi

# Bug fixes (fix:, bug:)
FIXES=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -E "^- (fix|bug):" | sed 's/^- (fix|bug): /- /' || true)
FIXES=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -E "^(\[skip ci\] )?(fix|bug):" | sed -E 's/^(\[skip ci\] )?(fix|bug): /- /' || true)
if [ -n "$FIXES" ]; then
echo "### Bug Fixes" >> release_notes.md
echo "$FIXES" >> release_notes.md
echo "" >> release_notes.md
fi

# Other commits
OTHER=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD | grep -vE "^- (feat|add|new|fix|bug|chore):" || true)
OTHER=$(git log --pretty=format:"%s" $PREV_TAG..HEAD | grep -vE "^(\[skip ci\] )?(feat|add|new|fix|bug|chore):" || true)
if [ -n "$OTHER" ]; then
echo "### Other Changes" >> release_notes.md
echo "$OTHER" >> release_notes.md
Expand Down
102 changes: 97 additions & 5 deletions main/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { createWindow, getCurrentAccount, getDebugInfo, configFileExists, getAll
import SteamCommunity from 'steamcommunity';
import { addAuthenticator, finalizeAuthenticator, getAuthCode, loginAgain, refreshProfile } from './helpers/steam';
import { createEncryptedStore, initializeStore } from './store';
import { Account, IpcHandlers, MaFileData } from './types';
import { Account, Confirmation, IpcHandlers, MaFileData } from './types';
import { readFile } from 'fs/promises';
import { getConfirmationKey, time } from 'steam-totp';

// Type-safe IPC handler helper
function handleIpc<K extends keyof IpcHandlers>(
Expand Down Expand Up @@ -254,10 +255,6 @@ handleIpc('import-mafile', async (event, filePath) => {
}
};

if ('RefreshToken' in maFileData.Session && maFileData.Session.RefreshToken) {
accountData.refreshToken = maFileData.Session.RefreshToken;
}

// Add the account
const success = addAccount(maFileData.Session.SteamID, accountData);

Expand All @@ -266,4 +263,99 @@ handleIpc('import-mafile', async (event, filePath) => {
}

return maFileData.Session.SteamID;
});

handleIpc('get-confirmations', async (event) => {
const account = getCurrentAccount(false);
if (!account) {
throw new Error('No current account set');
}

const community = new SteamCommunity();
community.setCookies(account.cookies || []);

return new Promise((resolve, reject) => {
community.loggedIn(async (err, loggedIn) => {
if (err) {
event.sender.send('login-required');
return resolve([]);
}

const proceed = async () => {
community.getConfirmations(time(), getConfirmationKey(account.identitySecret, time(), 'conf'), async (err, confirmations) => {
if (err) {
return reject(err);
}

return resolve(confirmations as unknown as Confirmation[]);
});
};

if (loggedIn) {
return proceed();
}

// If we're not logged in, check if we have a refresh token to re-authenticate
if (!account?.refreshToken) {
event.sender.send('login-required');
return resolve([]);
}

loginAgain({
refreshToken: account.refreshToken,
})
.then(() => {
return proceed();
})
.catch(() => {
event.sender.send('login-required');
return resolve([]);
});
});
});
});

handleIpc('respond-to-confirmation', async (event, id: number, key: string, accept: boolean) => {
const account = getCurrentAccount(false);
if (!account) {
throw new Error('No current account set');
}

const community = new SteamCommunity();
community.setCookies(account.cookies || []);

return new Promise((resolve, reject) => {
const confTime = time();
const confKey = getConfirmationKey(account.identitySecret, confTime, accept ? 'allow' : 'cancel');
community.respondToConfirmation(id, key, confTime, confKey, accept, (err) => {
if (err) {
return reject(err);
}

resolve();
});
});
});

handleIpc('accept-all-confirmations', async () => {
const account = getCurrentAccount(false);
if (!account) {
throw new Error('No current account set');
}

const community = new SteamCommunity();
community.setCookies(account.cookies || []);

return new Promise((resolve, reject) => {
const confTime = time();
const confKey = getConfirmationKey(account.identitySecret, confTime, 'conf');
const allowKey = getConfirmationKey(account.identitySecret, confTime, 'allow');
community.acceptAllConfirmations(confTime, confKey, allowKey, (err) => {
if (err) {
return reject(err);
}

resolve();
});
});
});
13 changes: 13 additions & 0 deletions main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,25 @@ const handler = {
return invoke('import-mafile', filePath);
},

getConfirmations: () => {
return invoke('get-confirmations');
},
respondToConfirmation: (id: number, key: string, accept: boolean) => {
return invoke('respond-to-confirmation', id, key, accept);
},
acceptAllConfirmations: () => {
return invoke('accept-all-confirmations');
},

events: {
onLoginRequired: (callback: () => void) => {
ipcRenderer.on('login-required', () => {
callback();
});
},
removeOnLoginRequired: () => {
ipcRenderer.removeAllListeners('login-required');
}
}
};

Expand Down
13 changes: 10 additions & 3 deletions main/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SteamUser from 'steam-user';
import SteamCommunity from 'steamcommunity';
import CConfirmation from 'steamcommunity/classes/CConfirmation';

export interface ThunderConfig {
initialized: boolean;
Expand All @@ -12,9 +13,9 @@ export interface Account {
id64: string;
personaName: string;
accountName: string;
sharedSecret?: string;
identitySecret?: string;
recoveryCode?: string;
sharedSecret: string;
identitySecret: string;
recoveryCode: string;
avatarUrl: string;

// Steam login stuff
Expand Down Expand Up @@ -79,6 +80,9 @@ interface AddAuthenticatorSuccess {
recoveryCode: string;
}

export type Confirmation = Omit<CConfirmation, 'getOfferID' | 'respond'> & {
sending: string;
};

export interface IpcHandlers {
'debug-info': () => Promise<DebugInfo | null>;
Expand All @@ -95,4 +99,7 @@ export interface IpcHandlers {
'get-auth-code': () => Promise<string>;
'show-mafile-dialog': () => Promise<string | null>;
'import-mafile': (filePath: string) => Promise<string>;
'get-confirmations': () => Promise<Confirmation[]>;
'respond-to-confirmation': (id: number, key: string, accept: boolean) => Promise<void>;
'accept-all-confirmations': () => Promise<void>;
}
7 changes: 7 additions & 0 deletions renderer/components/Icons/DocumentCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DocumentCheckIcon({ className }: { className?: string }) {
return (
<svg className={`${className}`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.125 2.25h-4.5c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125v-9M10.125 2.25h.375a9 9 0 0 1 9 9v.375M10.125 2.25A3.375 3.375 0 0 1 13.5 5.625v1.5c0 .621.504 1.125 1.125 1.125h1.5a3.375 3.375 0 0 1 3.375 3.375M9 15l2.25 2.25L15 12" />
</svg>
);
}
36 changes: 31 additions & 5 deletions renderer/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import HomeIcon from './Icons/Home';
import GithubIcon from './Icons/Github';
Expand All @@ -7,6 +7,7 @@ import Image from 'next/image';
import ArrowLRIcon from './Icons/ArrowsLR';
import Popup from './Popup/Popup';
import ExternalIcon from './Icons/External';
import DocumentCheckIcon from './Icons/DocumentCheck';

export default function Sidebar() {
const { currentAccount } = useAccount();
Expand All @@ -17,11 +18,18 @@ export default function Sidebar() {

const handleOpenSteam = (url: string) => {
window.electron.openSteamWindow(url);
};

useEffect(() => {
// Perhaps our session expired, listen for event
window.electron.events.onLoginRequired(() => {
setIsPopupOpen(true);
});
};

return () => {
window.electron.events.removeOnLoginRequired();
};
}, []);

const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -66,8 +74,16 @@ export default function Sidebar() {
Home
</Link>
</li>
{/* Button to open https://steamcommunity.com in a new thunder tab with the cookies of the current account */}
<li>
<Link
href="/confirmations"
className="flex items-center px-3 py-2 rounded-lg hover:bg-gray-800 transition-colors duration-200"
>
<DocumentCheckIcon className="w-5 h-5 mr-2" />
Confirmations
</Link>
</li>
<li className="border-t border-gray-700 pt-1">
<Link
href="#"
onClick={() => handleOpenSteam('https://steamcommunity.com')}
Expand All @@ -77,6 +93,16 @@ export default function Sidebar() {
<span className="mr-2">Steam</span>
</Link>
</li>
<li>
<Link
href="#"
onClick={() => handleOpenSteam('https://steamcommunity.com/my/tradeoffers')}
className="flex items-center px-3 py-2 rounded-lg hover:bg-gray-800 transition-colors duration-200"
>
<ExternalIcon className="w-5 h-5 mr-2" />
<span className="mr-2">Trade offers</span>
</Link>
</li>
</ul>
</nav>

Expand Down Expand Up @@ -137,7 +163,7 @@ export default function Sidebar() {
{/* TODO: Refactor popup to manage its own state more. Doesn't feel right to have password as part of sidebar state */}
{isPopupOpen && (
<Popup
title={`Session expired. Please enter the password for "${currentAccount?.personaName}" to login again.`}
title={`Session expired. Please enter the password for "${currentAccount?.personaName}" to login again, then retry.`}
close={() => setIsPopupOpen(false)}
>

Expand Down Expand Up @@ -186,7 +212,7 @@ export default function Sidebar() {
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Logging...
Logging in...
</span>
) : (
'Login'
Expand Down
Loading