Compare commits

...

19 Commits

Author SHA1 Message Date
achiez 9ac4ffdc34 feat(update): introduce modern update system with JSON changelog, reminders and checksum validation
- Replace legacy AutoUpdater dialog with a custom update UI integrated into the application theme
- Add support for JSON-based changelog with HTML fallback for backward compatibility
- Implement "Remind later" and "Skip version" options to reduce intrusive update prompts
- Add visual update indicator and manual "Check for updates" action in the links menu
- Persist user update preferences (skipped versions and reminder delays)
- Introduce checksum validation (SHA256) for downloaded update packages
- Migrate changelog source format from HTML to JSON
- Automatically generate HTML changelog and GitHub release notes from JSON via CI
- Consolidate release automation into a single GitHub Actions workflow
- Remove obsolete legacy update dialog and related code
- Expand localization coverage for update-related UI
2026-03-13 00:17:45 +02:00
achiez 17635ccba0 fix(confirmations): roll back the erroneously assigned ConfirmCommand in PurchaseConfirmation 2026-03-12 23:56:57 +02:00
bohdanbtw 69589075e5 feat(import): support SDA-encrypted mafiles (#17)
NebulaAuth previously supported only plain JSON `.mafile` files. Steam Desktop Authenticator (SDA) can encrypt these files and stores per-file salt and IV values in `manifest.json`, which prevented users from importing accounts protected with SDA encryption.

Add support for importing SDA-encrypted mafiles. The importer now detects encrypted blobs, automatically locates the SDA manifest, prompts for the SDA password once, and decrypts files during import.

The import flow was also refactored to unify handling of plain and encrypted mafiles. SDA detection and decryption logic is moved into a dedicated helper, batch imports reuse the entered password, and the ViewModel import logic was simplified with explicit result handling instead of exception-driven control flow.

This allows users to migrate accounts from Steam Desktop Authenticator to NebulaAuth even when their mafiles are encrypted.
2026-03-12 21:22:18 +02:00
bohdanbtw 4b547e19c4 feat(confirmations): reveal items in grouped market sell confirmations (#18)
Grouped market sell confirmations previously appeared as a single row showing only the item count (e.g. "Sell 4 pcs"). This made it difficult to verify which items were being confirmed or their prices.

Add an expandable grouped confirmation that reveals individual items with their icon, name and price. Also allow confirming or canceling items individually while keeping confirm-all and cancel-all actions.

Includes a scrollable list for large batches and minor UI/code cleanup.

Co-authored-by: bohdanbtw [bohdanbtw@DESKTOP-ANGF2KJ](mailto:bohdanbtw@DESKTOP-ANGF2KJ)
2026-03-12 17:27:03 +02:00
achiez 053faec343 feat(export): add input trimming in export feature 2026-03-12 14:57:27 +02:00
github-actions b8ac76d2a9 chore(release): 1.8.2 2026-02-10 14:43:46 +00:00
Achies b81d45765a Merge pull request #15 from achiez/1.8.2
Release 1.8.2
2026-02-10 16:43:35 +02:00
achiez 372b8c6463 fix(maac): portable MacClient status reset and prepare release
- Add changelog/1.8.2.html with details for version 1.8.2
- Client status is properly updates after successful MAAC request
- Bump version to 1.8.2
2026-02-10 16:40:36 +02:00
github-actions 5de26381bb chore(release): 1.8.1 2026-01-25 17:25:47 +00:00
Achies 16fd4992f8 Merge pull request #14 from achiez/1.8.1
Release 1.8.1 → Merge into master
2026-01-25 19:25:39 +02:00
achiez 7835a53717 Prepare 1.8.1
- Added changelog/1.8.1.html with detailed update notes and links
- Improved MAAC auto-confirmation logic: now uses IsReady() for smarter account readiness checks and retries
- Added GetReadyAccounts() to MultiAccountAutoConfirmer for flexible account selection
- Fixed localization lookup in GetPortableMaClientStatus
- Set PortableMaClient initial status to Ok instead of test error
2026-01-25 19:21:50 +02:00
achiez f15632f2d0 Refactored MAAC error/status handling and UI feedback
- Introduced centralized MAACRequestHandler for error tracking, retries, and status escalation (Ok/Warning/Error) per account
- PortableMaClient now uses new Status property (PortableMaClientStatus) instead of IsError; error history tracked in PortableMaClientErrorData
- All MAAC requests now routed through MAACRequestHandler, providing unified error handling and retry logic
- MultiAccountAutoConfirmer now filters accounts by Status.StatusType == Ok
- Updated MainWindow UI: account name color reflects status (Success/Warning/Error); added "Unattach proxy" to context menu
- Removed obsolete PortableMaClientStatusToColorConverter and related bindings
- Settings: removed IgnorePatchTuesdayErrors, added MaacErrorThreshold and MaacRetryInterval for error escalation control
- Refactored SessionHandler: split into logic, API, and helpers; improved exception propagation and session management
- AdmissionHelper methods now handle null sessions and always transfer community cookies if session is missing
- Mafile export: always preserves SteamId, even if session data is not exported
- ViewModel: RemoveProxy command now parameterized and only enabled if Mafile has a proxy
- Enabled concurrent log writing in NLog config
- Minor code cleanups and improved exception signatures
2026-01-25 18:48:11 +02:00
achiez b56c0e578d Fix checkbox margin accordingly to new MaterialDesignInXaml update 2026-01-19 21:36:42 +02:00
achiez 969590d9f2 Add MafileExport feature and Perform Resharper Cleanup 2026-01-19 21:28:44 +02:00
achiez 3e87a0d50e Improve localization structure and add missing translations
Thanks to @77155331 for Chinese translation (Issue #11)
Thanks to @klNuno for French localization (PR #13)
2026-01-02 22:43:24 +02:00
achiez 690c98527f Update mafiles removal logic and changelog URL generation
- Fixed crash: MaFiles collection in MainVM is now a new ObservableCollection. Manual removal from collection is omitted
- Improved logic for moving mafiles to "removed": file extension is now always correct and name conflicts are handled more reliably
- Github workflow: Changelog URL in update.xml now uses repository name only for correct links
- On MaFiles collection change, PerformQuery is called to update search results
- Fixed parameter order in mafiles renaming result string to match localization
2026-01-02 22:09:31 +02:00
achiez 5242b0009b Refactor mafile storage: async, modular, bulk rename
- Fix crash: Updated SaveMafileAsync to properly update or add to MaFiles collection
- Fix naming logic: Added Filename reset logic to adding new mafile to prevent inconsistent and duplicating import
- Extracted mafile path and naming logic to MafilesStorageHelper for better modularity
- Refactored Storage methods to use MafilesStorageHelper
- Replaced all sync mafile save/add calls with async/await versions throughout the codebase
- Performed code cleanups and improved maintainability for mafile-related operations
2026-01-02 21:23:11 +02:00
Achies 1f14bd77e7 chore(update.xml) Fix changelog URL in update.xml 2025-11-14 22:35:39 +02:00
achiez f2b1cdfcc9 Fix MAACStorage.cs
- fixed collection was untracked if no `maac.json` file was created
- fixed incorrect change tracking for persisted accounts after reboot
2025-11-07 21:58:52 +02:00
92 changed files with 4490 additions and 1332 deletions
-78
View File
@@ -1,78 +0,0 @@
name: Publish release
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
release:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version
id: ver
run: echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
shell: bash
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build NebulaAuth
run: dotnet publish src/NebulaAuth/NebulaAuth.csproj -c Release -o build
- name: Package release
run: |
cd build
powershell Compress-Archive * "../NebulaAuth.${env:GITHUB_REF_NAME}.zip"
shell: pwsh
- name: Install pandoc
run: |
choco install pandoc -y
- name: Convert changelog HTML to Markdown (ul-only)
id: changelog
shell: bash
run: |
version="${{ steps.ver.outputs.version }}"
file="changelog/${version}.html"
echo "Looking for file: $file"
if [ -f "$file" ]; then
echo "✅ File found, extracting <ul> section"
sed -n '/<ul>/,/<\/ul>/p' "$file" > body.html
echo "Converting with pandoc..."
pandoc body.html -f html -t markdown --wrap=none -o changelog.md
echo "-------- Converted changelog.md --------"
cat changelog.md
echo "----------------------------------------"
echo "body<<EOF" >> $GITHUB_OUTPUT
cat changelog.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "❌ Changelog not found for version $version"
echo "body=No changelog found for $version" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ env.RELEASE_NAME }}
body: ${{ steps.changelog.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
env:
RELEASE_NAME: ${{ steps.ver.outputs.version }}
-65
View File
@@ -1,65 +0,0 @@
name: Prepare release
on:
push:
branches: [ master ]
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
workflow_dispatch:
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.PAT_TOKEN }}
- name: Read version from csproj
id: ver
shell: bash
run: |
VERSION=$(grep -oPm1 "(?<=<AssemblyVersion>)[^<]+" src/NebulaAuth/NebulaAuth.csproj)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Generate update.xml
run: |
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>${{ steps.ver.outputs.version }}</version>
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
<changelog>https://achiez.github.io/${{ github.repository }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
<mandatory>false</mandatory>
</item>
EOF
echo "Generated NebulaAuth/update.xml:"
cat NebulaAuth/update.xml
- name: Check if update.xml changed
id: diff
run: |
if git diff --exit-code NebulaAuth/update.xml >/dev/null 2>&1; then
echo "no_changes=true" >> $GITHUB_OUTPUT
else
echo "no_changes=false" >> $GITHUB_OUTPUT
fi
- name: Commit and tag
if: steps.diff.outputs.no_changes == 'false'
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add NebulaAuth/update.xml
git commit -m "chore(release): ${{ steps.ver.outputs.version }}"
git tag -a "${{ steps.ver.outputs.version }}" -m "Release ${{ steps.ver.outputs.version }}"
git push origin HEAD:master --tags
- name: Skip message
if: steps.diff.outputs.no_changes == 'true'
run: echo "No changes in version or update.xml — skipping release preparation."
+218
View File
@@ -0,0 +1,218 @@
name: Build and Publish Release
on:
push:
branches:
- master
paths:
- 'src/NebulaAuth/NebulaAuth.csproj'
- 'changelog/*.json'
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# --------------------------------------------------------
# Checkout
# --------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
# --------------------------------------------------------
# Setup .NET
# --------------------------------------------------------
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# --------------------------------------------------------
# Extract version via MSBuild
# --------------------------------------------------------
- name: Extract version from csproj
id: ver
run: |
VERSION=$(dotnet msbuild src/NebulaAuth/NebulaAuth.csproj -getProperty:AssemblyVersion)
echo "Detected version $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Validate JSON changelog
# --------------------------------------------------------
- name: Validate changelog JSON
run: |
VERSION="${{ steps.ver.outputs.version }}"
FILE="changelog/${VERSION}.json"
echo "Checking changelog file $FILE"
if [ ! -f "$FILE" ]; then
echo "ERROR: changelog JSON not found"
exit 1
fi
jq empty "$FILE"
# --------------------------------------------------------
# Generate HTML changelog (legacy AutoUpdater)
# --------------------------------------------------------
- name: Generate HTML changelog
run: |
VERSION="${{ steps.ver.outputs.version }}"
JSON="changelog/${VERSION}.json"
HTML="changelog/${VERSION}.html"
echo "Generating HTML changelog"
jq -r '
"<!DOCTYPE html>",
"<html>",
"<head>",
"<meta charset=\"UTF-8\">",
"<title>Changelog</title>",
"<style>",
"body { font-family: Segoe UI, sans-serif; background:#eeeeee; padding:20px; }",
".change { background:white; padding:25px; border-radius:10px; }",
"li { margin-bottom:6px; }",
"</style>",
"</head>",
"<body>",
"<div class=\"change\">",
"<ul>",
(.changes[] |
"<li><b>\(.type):</b> \(.text)" +
(if .link then " <a href=\"\(.link)\">details</a>" else "" end) +
"</li>"
),
"</ul>",
"</div>",
"</body>",
"</html>"
' "$JSON" > "$HTML"
echo "Generated $HTML"
# --------------------------------------------------------
# Build application
# --------------------------------------------------------
- name: Build NebulaAuth
run: |
dotnet publish src/NebulaAuth/NebulaAuth.csproj \
-c Release \
-o build \
-p:EnableWindowsTargeting=true
# --------------------------------------------------------
# Package ZIP
# --------------------------------------------------------
- name: Package release
run: |
cd build
zip -r ../NebulaAuth.${{ steps.ver.outputs.version }}.zip *
# --------------------------------------------------------
# Generate SHA256 checksum
# --------------------------------------------------------
- name: Generate checksum
id: checksum
run: |
FILE="NebulaAuth.${{ steps.ver.outputs.version }}.zip"
HASH=$(sha256sum "$FILE" | awk '{print $1}')
echo "checksum=$HASH" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Generate update.xml
# --------------------------------------------------------
- name: Generate update.xml
run: |
VERSION="${{ steps.ver.outputs.version }}"
CHECKSUM="${{ steps.checksum.outputs.checksum }}"
cat > NebulaAuth/update.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>$VERSION</version>
<url>https://github.com/${{ github.repository }}/releases/download/$VERSION/NebulaAuth.$VERSION.zip</url>
<changelog>https://achiez.github.io/${{ github.event.repository.name }}/changelog/$VERSION.html</changelog>
<mandatory>false</mandatory>
<checksum algorithm="SHA256">$CHECKSUM</checksum>
</item>
EOF
echo "Generated update.xml"
cat NebulaAuth/update.xml
# --------------------------------------------------------
# Generate GitHub Release Notes
# --------------------------------------------------------
- name: Generate release notes
id: notes
run: |
VERSION="${{ steps.ver.outputs.version }}"
FILE="changelog/${VERSION}.json"
BODY=$(jq -r '
.changes[] |
"- **\(.type):** \(.text)" +
(if .link then " (\(.link))" else "" end)
' "$FILE")
echo "body<<EOF" >> $GITHUB_OUTPUT
echo "$BODY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# --------------------------------------------------------
# Commit generated files
# --------------------------------------------------------
- name: Commit generated files
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add NebulaAuth/update.xml
git add changelog/*.html
git commit -m "chore(release): ${{ steps.ver.outputs.version }}" || echo "No changes"
git push
# --------------------------------------------------------
# Create git tag
# --------------------------------------------------------
- name: Create tag
run: |
VERSION="${{ steps.ver.outputs.version }}"
git tag -a "$VERSION" -m "Release $VERSION"
git push origin "$VERSION"
# --------------------------------------------------------
# Publish GitHub Release
# --------------------------------------------------------
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
name: Release ${{ steps.ver.outputs.version }}
body: ${{ steps.notes.outputs.body }}
files: |
NebulaAuth.${{ steps.ver.outputs.version }}.zip
+4 -8
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
# Visual Studio Version 18
VisualStudioVersion = 18.3.11520.95 d18.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{161BFADE-FCF3-45D1-82EA-8A1B187529F7}"
ProjectSection(SolutionItems) = preProject
@@ -24,12 +24,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
changelog\1.7.3.html = changelog\1.7.3.html
changelog\1.7.4.html = changelog\1.7.4.html
changelog\1.8.0.html = changelog\1.8.0.html
changelog\1.8.1.html = changelog\1.8.1.html
changelog\1.8.2.html = changelog\1.8.2.html
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamLibForked", "src\SteamLibForked\SteamLibForked.csproj", "{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth.LegacyConverter", "src\NebulaAuth.LegacyConverter\NebulaAuth.LegacyConverter.csproj", "{C8235305-E5C4-155B-C718-C0F239CA3AB7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaAuth", "src\NebulaAuth\NebulaAuth.csproj", "{C42F63B6-32F7-ED08-5B86-CD51989761AD}"
EndProject
Global
@@ -42,10 +42,6 @@ Global
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}.Release|Any CPU.Build.0 = Release|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8235305-E5C4-155B-C718-C0F239CA3AB7}.Release|Any CPU.Build.0 = Release|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C42F63B6-32F7-ED08-5B86-CD51989761AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+3 -3
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.8.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.0/NebulaAuth.1.8.0.zip</url>
<changelog>https://achiez.github.io/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.0.html</changelog>
<version>1.8.2</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.2/NebulaAuth.1.8.2.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.2.html</changelog>
<mandatory>false</mandatory>
</item>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<div class="change">
<div class="version">Version 1.8.1</div>
<div class="date">25.01.2026</div>
<div class="description">
<ul>
<li><b>NEW:</b> Added Mafile Export tool with templates, batch export and flexible field filtering.</li>
<li><b>IMPROVEMENT:</b> Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.</li>
<li><b>UPDATE:</b> Added “Unattach Proxy” option to the account context menu.</li>
<li><b>LOCALIZATION:</b> Added Chinese (Simplified) and French languages.</li>
<li><b>FIX:</b> Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.</li>
<li>
<b>DETAILS:</b> Full patch notes:
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-eng">ENG</a> |
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-rus">RUS</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+78
View File
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<div class="change">
<div class="version">Version 1.8.2</div>
<div class="date">10.02.2026</div>
<div class="description">
<ul>
<li><b>FIX:</b> AutoConfirm timer now correctly resets the status after a successful request (Warning → Ok).</li>
</ul>
</div>
</div>
</div>
</body>
</html>
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SteamLibForked\SteamLibForked.csproj" />
</ItemGroup>
</Project>
-176
View File
@@ -1,176 +0,0 @@
using System.Reflection;
using AchiesUtilities.Extensions;
using NebulaAuth.LegacyConverter;
using Newtonsoft.Json;
using SteamLib.Utility.MafileSerialization;
try
{
var mafileSerializer = new MafileSerializer(new MafileSerializerSettings
{
DeserializationOptions =
{
AllowDeviceIdGeneration = true,
AllowSessionIdGeneration = true
}
});
var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (currentPath != null)
Environment.CurrentDirectory = currentPath;
const string toStoreFolder = "ConvertedMafiles";
if (Directory.Exists(toStoreFolder) == false)
{
Directory.CreateDirectory(toStoreFolder);
}
else
{
var isEmpty = Directory.GetFiles(toStoreFolder).Length == 0;
Console.ForegroundColor = ConsoleColor.Yellow;
if (!isEmpty)
{
Console.WriteLine(
"WARNING! 'ConverterdMafiles' folder is not empty. Please backup data from it and then continue.");
Console.ResetColor();
Console.WriteLine("Press Y to continue");
while (Console.ReadKey(true).Key != ConsoleKey.Y)
{
}
}
}
var decryptMode = false;
while (true)
{
Console.WriteLine("Press 'D' to select decrypt mode. Press 'C' to convert mode. Press ESC to exit");
var key = Console.ReadKey(true);
switch (key.Key)
{
case ConsoleKey.D:
decryptMode = true;
break;
case ConsoleKey.C:
break;
case ConsoleKey.Escape:
return;
default:
continue;
}
break;
}
Manifest? manifest = null;
string? password = null;
if (decryptMode)
{
var files = Directory.GetFiles(currentPath, "manifest.json");
var manifestPath = files.FirstOrDefault();
if (manifestPath == null)
{
Console.WriteLine("No manifest.json found in current directory");
return;
}
var manifestText = File.ReadAllText(manifestPath);
try
{
manifest = JsonConvert.DeserializeObject<Manifest>(manifestText)!;
}
catch (Exception ex)
{
Console.WriteLine("Can't read manifest: " + ex);
return;
}
if (manifest.Encrypted == false)
{
Console.WriteLine("Manifest is not encrypted");
return;
}
while (true)
{
Console.WriteLine("Please enter your encryption password: ");
password = Console.ReadLine();
if (string.IsNullOrWhiteSpace(password))
continue;
break;
}
}
Console.WriteLine(currentPath);
foreach (var path in args)
{
if (Path.Exists(path) == false)
{
Console.WriteLine($"NOT VALID PATH: '{path}'");
continue;
}
Console.WriteLine("Reading: " + path);
try
{
var text = File.ReadAllText(path);
if (decryptMode)
{
var fileName = Path.GetFileName(path);
text = DecryptMafile(fileName, text);
if (text == null)
{
Console.WriteLine(path + " not found in manifest. Skipped");
continue;
}
}
var data = mafileSerializer.Deserialize(text);
var maf = data.Data;
var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
Write(legacy, fileNameWithoutExtension);
Console.WriteLine("DONE: " + fileNameWithoutExtension);
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("-----------------------------------------");
}
}
//Local Functions
void Write(string maf, string name)
{
var path = Path.Combine(toStoreFolder, name + "_legacy.mafile");
File.WriteAllText(path, maf);
}
string? DecryptMafile(string fileName, string cipherText)
{
if (password == null) return null;
var entry = manifest?.Entries.FirstOrDefault(x => x.Filename.EqualsIgnoreCase(fileName));
if (entry == null)
{
return null;
}
var iv = entry.EncryptionIv;
var salt = entry.EncryptionSalt;
return SDAEncryptor.DecryptData(password, salt, iv, cipherText);
}
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
+2 -7
View File
@@ -3,7 +3,6 @@ using System.Windows;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
namespace NebulaAuth;
@@ -16,12 +15,8 @@ public partial class App
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
splashScreen.Show(false, true);
base.OnStartup(e);
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Shell.Initialize();
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
await Shell.Initialize();
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;
mainWindow.Show();
+1 -1
View File
@@ -12,7 +12,6 @@
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
@@ -23,6 +22,7 @@
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
@@ -1,25 +1,19 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace NebulaAuth.Converters;
public class PortableMaClientStatusToColorConverter : IValueConverter
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is false)
{
return new SolidColorBrush(Color.FromRgb(187, 224, 139));
}
return new SolidColorBrush(Color.FromRgb(224, 139, 139));
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
return value is Visibility v && v != Visibility.Visible;
}
}
+45 -2
View File
@@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoUpdaterDotNET;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Update;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel.Linker;
@@ -51,6 +54,25 @@ public static class DialogsController
return null;
}
/// <summary>
/// Shows the SDA encryption password dialog for unlocking SDA-encrypted mafiles.
/// </summary>
/// <returns>The entered password if user clicked OK, otherwise null.</returns>
public static async Task<string?> ShowSdaPasswordDialog()
{
var vm = new SdaPasswordDialogVM();
var content = new SdaPasswordDialog
{
DataContext = vm
};
var result = await DialogHost.Show(content);
if (result is true)
{
return vm.Password;
}
return null;
}
public static async Task<bool> ShowProxyManager()
{
var vm = new ProxyManagerVM();
@@ -113,6 +135,27 @@ public static class DialogsController
await DialogHost.Show(dialog);
}
public static async Task ShowExportDialog()
{
var vm = new MafileExporterVM();
var dialog = new MafileExporterView
{
DataContext = vm
};
await DialogHost.Show(dialog);
}
public static async Task ShowUpdateDialog(UpdateInfoEventArgs args, ChangelogEntry? changelog,
string? htmlFallbackUrl)
{
var vm = new UpdateDialogVM(args, changelog, htmlFallbackUrl);
var dialog = new UpdateDialog
{
DataContext = vm
};
await DialogHost.Show(dialog);
}
#region CommonDialogs
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
@@ -120,7 +163,7 @@ public static class DialogsController
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
var result = await DialogHost.Show(content);
return result != null && (bool) result;
return result != null && (bool)result;
}
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
+5 -1
View File
@@ -31,6 +31,8 @@ public static class LocManager
LocalizationLanguage.English => "en",
LocalizationLanguage.Russian => "ru",
LocalizationLanguage.Ukrainian => "ua",
LocalizationLanguage.ChineseSimplified => "zh",
LocalizationLanguage.French => "fr",
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
};
}
@@ -107,5 +109,7 @@ public enum LocalizationLanguage
{
English,
Russian,
Ukrainian
Ukrainian,
ChineseSimplified,
French
}
+1 -1
View File
@@ -69,7 +69,7 @@ public static class TrayManager
private static void HideMainWindow()
{
if (!Init) return;
if (IsEnabled == false) return;
if (!IsEnabled) return;
_notifyIcon.Visible = true;
MainWindow.Hide();
}
+107 -58
View File
@@ -1,16 +1,36 @@
using System;
using AutoUpdaterDotNET;
using NebulaAuth.Model;
using NebulaAuth.Model.Update;
using Newtonsoft.Json;
using System;
using System.IO;
using AutoUpdaterDotNET;
using System.Net.Http;
using System.Windows;
namespace NebulaAuth.Core;
public static class UpdateManager
{
private const string UPDATE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/NebulaAuth/update.xml";
private const string BASE_URL =
"https://raw.githubusercontent.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/master/";
private const string UPDATE_URL = BASE_URL + "NebulaAuth/update.xml";
public static void CheckForUpdates()
private const string CHANGELOG_BASE_URL = BASE_URL + "changelog/";
private static readonly HttpClient HttpClient = new();
private static bool _isManualCheck;
public static bool HasPendingUpdate { get; private set; }
public static event Action? PendingUpdateDetected;
static UpdateManager()
{
AutoUpdater.CheckForUpdateEvent += HandleCheckForUpdateEvent;
}
public static void CheckForUpdates(bool manual = false)
{
_isManualCheck = manual;
var jsonPath = Path.Combine(Environment.CurrentDirectory, "update-settings.json");
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
AutoUpdater.ShowSkipButton = false;
@@ -18,6 +38,88 @@ public static class UpdateManager
AutoUpdater.Start(UPDATE_URL);
}
public static void SkipVersion(string version)
{
var settings = UpdateSettings.Load();
settings.SkipVersion(version);
}
public static void SetRemindAfter(TimeSpan delay)
{
var settings = UpdateSettings.Load();
settings.SetRemindAfter(DateTime.Now + delay);
}
private static async void HandleCheckForUpdateEvent(UpdateInfoEventArgs args)
{
try
{
var isManual = _isManualCheck;
_isManualCheck = false;
if (args.Error != null)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(LocManager.GetCommonOrDefault("Request error", "RequestError")));
}
return;
}
if (!args.IsUpdateAvailable)
{
if (isManual)
{
Application.Current.Dispatcher.Invoke(() =>
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("You are using the latest version", "UpdateService",
"LatestVersion")));
}
return;
}
var version = args.CurrentVersion.ToString();
var settings = UpdateSettings.Load();
if (!isManual && !settings.ShouldShow(version))
{
HasPendingUpdate = true;
Application.Current.Dispatcher.Invoke(() => PendingUpdateDetected?.Invoke());
return;
}
ChangelogEntry? changelog = null;
try
{
var jsonUrl = $"{CHANGELOG_BASE_URL}{version}.json";
var response = await HttpClient.GetAsync(jsonUrl).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
changelog = JsonConvert.DeserializeObject<ChangelogEntry>(json);
}
}
catch
{
// fallback to HTML changelog URL
}
var htmlFallbackUrl = changelog == null ? args.ChangelogURL : null;
await Application.Current.Dispatcher.BeginInvoke(async () =>
{
await DialogsController.ShowUpdateDialog(args, changelog, htmlFallbackUrl);
});
}
catch (Exception e)
{
Shell.Logger.Error(e, "Error while checking updates");
}
}
private static bool RequiresAdminAccess()
{
try
@@ -35,57 +137,4 @@ public static class UpdateManager
return true;
}
}
//static UpdateManager()
//{
// //AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
//}
//private static async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args)
//{
// if (args.Error == null)
// {
// if (args.IsUpdateAvailable)
// {
// DialogResult dialogResult;
// var dialog = new UpdaterView()
// {
// DataContext = new UpdaterVM(args)
// };
// await DialogHost.Show(dialog);
// Application.Current.Shutdown();
// }
// else
// {
// }
// }
// else
// {
// if (args.Error is WebException)
// {
// }
// else
// {
// }
// }
//}
//private static void RunUpdate(UpdateInfoEventArgs args)
//{
// Application.Current.Dispatcher.Invoke(() =>
// {
// if (AutoUpdater.DownloadUpdate(args))
// {
// Application.Current.Shutdown();
// }
// }, DispatcherPriority.ContextIdle);
//}
}
+67 -23
View File
@@ -73,7 +73,7 @@
Kind="FileReplaceOutline" />
</StackPanel>
<ToolBarTray>
<ToolBarTray Grid.Row="0">
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
<Menu FontSize="16">
@@ -92,7 +92,9 @@
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
Command="{Binding DebugCommand}" />
Command="{Binding OpenSetPasswordsDialogCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
Command="{Binding OpenExporterDialogCommand}" />
</MenuItem>
</MenuItem>
</Menu>
@@ -292,18 +294,42 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Stretch" Text="{Binding AccountName}">
<TextBlock.Resources>
<SolidColorBrush x:Key="BaseContentBrushProxy"
Color="{DynamicResource BaseContentColor}" />
</TextBlock.Resources>
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding AccountName}"
ToolTip="{Binding LinkedClient.Status.Message, FallbackValue={x:Null}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground"
Value="{DynamicResource BaseContentBrush}" />
<Style.Triggers>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Ok">
<Setter Property="Foreground"
Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Warning">
<Setter Property="Foreground"
Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger
Binding="{Binding LinkedClient.Status.StatusType}"
Value="Error">
<Setter Property="Foreground"
Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
<TextBlock.Foreground>
<Binding Path="LinkedClient.IsError"
Converter="{StaticResource PortableMaClientStatusToColorConverter}"
FallbackValue="{StaticResource BaseContentBrushProxy}" />
</TextBlock.Foreground>
</TextBlock>
<StackPanel
Visibility="{Binding LinkedClient, Converter={StaticResource NullableToVisibilityConverter}}"
Grid.Column="1" Orientation="Horizontal">
@@ -356,7 +382,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<md:PackIcon Kind="PlusCircle"
VerticalAlignment="Center" />
VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center"
Margin="5,0,0,0" Grid.Column="1"
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
@@ -391,7 +417,9 @@
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
Command="{Binding Path=RemoveGroupCommand}"
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
Command="{Binding RemoveProxyCommand}"
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
</ContextMenu>
</FrameworkElement.ContextMenu>
@@ -491,16 +519,32 @@
Text="{Binding SelectedMafile.AccountName, TargetNullValue=' ', FallbackValue=' ', Mode=OneWay}" />
<!--<Button Command="{Binding DebugCommand}">Debug</Button>-->
</ToolBarPanel>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
Margin="0,0,10,0">
<Hyperlink
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<md:PackIcon x:Name="UpdateBadgeIcon"
Kind="BellAlert" Width="15" Height="15"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,5,0"
Visibility="Collapsed"
ToolTip="{Tr CodeBehind.UpdateService.UpdateIndicatorHint}">
<md:PackIcon.Foreground>
<SolidColorBrush x:Name="UpdateBadgeBrush" Color="DodgerBlue" />
</md:PackIcon.Foreground>
</md:PackIcon>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" FontSize="16"
Margin="0,0,10,0">
<Hyperlink
NavigateUri="https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies"
Foreground="{DynamicResource AccentBrush}"
Command="{Binding OpenLinksViewCommand}">
by achies
</Hyperlink>
</TextBlock>
Command="{Binding OpenLinksViewCommand}">
<Hyperlink.Foreground>
<SolidColorBrush x:Name="ByAchiesBrush" Color="DodgerBlue" />
</Hyperlink.Foreground>
by achies
</Hyperlink>
</TextBlock>
</StackPanel>
</Grid>
</Border>
<md:Snackbar Grid.Row="1" MessageQueue="{Binding MessageQueue}" />
+52 -13
View File
@@ -1,16 +1,18 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using MaterialDesignThemes.Wpf;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.View.Dialogs;
using NebulaAuth.ViewModel;
using NebulaAuth.ViewModel.Other;
using System;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace NebulaAuth;
@@ -25,12 +27,49 @@ public partial class MainWindow
ThemeManager.InitializeTheme();
Title = Title + " " + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3);
Loaded += OnApplicationStarted;
UpdateManager.PendingUpdateDetected += OnPendingUpdateDetected;
}
private void OnPendingUpdateDetected()
{
Dispatcher.BeginInvoke(StartUpdateBlink);
}
private void StartUpdateBlink()
{
UpdateBadgeIcon.Visibility = Visibility.Visible;
var baseColor = FindResource("AccentBrush") is SolidColorBrush accent
? accent.Color
: ByAchiesBrush.Color;
var duration = new Duration(TimeSpan.FromSeconds(6));
var sb = new Storyboard();
sb.Children.Add(BuildBlinkAnimation(duration, "UpdateBadgeBrush"));
sb.Children.Add(BuildBlinkAnimation(duration, "ByAchiesBrush"));
sb.Completed += (_, _) =>
{
UpdateBadgeIcon.Visibility = Visibility.Collapsed;
ByAchiesBrush.Color = baseColor;
};
sb.Begin(this);
}
private static ColorAnimationUsingKeyFrames BuildBlinkAnimation(Duration duration, string targetName)
{
var anim = new ColorAnimationUsingKeyFrames { Duration = duration };
for (var i = 0; i <= 6; i++)
{
var color = i % 2 == 0 ? Colors.DodgerBlue : Colors.OrangeRed;
anim.KeyFrames.Add(new DiscreteColorKeyFrame(color, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i))));
}
Storyboard.SetTargetName(anim, targetName);
Storyboard.SetTargetProperty(anim, new PropertyPath(SolidColorBrush.ColorProperty));
return anim;
}
private async void OnApplicationStarted(object? sender, EventArgs e)
{
((MainVM) DataContext).CurrentDialogHost = DialogHostInstance;
if (Settings.Instance.IsPasswordSet == false) return;
if (!Settings.Instance.IsPasswordSet) return;
Topmost = false;
await Dispatcher.InvokeAsync(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
}
@@ -46,7 +85,7 @@ public partial class MainWindow
var result = await DialogHost.Show(dialog);
var pass = vm.Password;
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
if (result is true && !string.IsNullOrWhiteSpace(pass))
{
PHandler.SetPassword(pass);
}
@@ -79,12 +118,12 @@ public partial class MainWindow
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
if (!int.TryParse(e.Text, out _)) e.Handled = true;
}
private void Rectangle_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Handled = true;
return;
@@ -96,7 +135,7 @@ public partial class MainWindow
private async void Rectangle_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
var filePaths = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
if (filePaths.Length == 0) return;
if (DataContext is MainVM mainVm)
-1
View File
@@ -28,7 +28,6 @@ public partial class Mafile : MobileDataExtended
}
private string? _filename;
[JsonIgnore] private PortableMaClient? _linkedClient;
public void SetSessionData(MobileSessionData? sessionData)
@@ -0,0 +1,177 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using SteamLib.Exceptions.Authorization;
namespace NebulaAuth.Model.MAAC;
public static class MAACRequestHandler
{
private const string LOC_PATH = "MAAC";
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
public static void Register(Mafile mafile)
{
_errors[mafile] = new PortableMaClientErrorData();
}
public static void Unregister(Mafile mafile)
{
_errors.TryRemove(mafile, out _);
}
public static PortableMaClientStatus ClearErrors(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
data.Clear();
}
return PortableMaClientStatus.Ok();
}
/// <summary>
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
/// history.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="client"></param>
/// <param name="func"></param>
/// <returns></returns>
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
{
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
if (client.Mafile.PreviouslyHadNoErrors())
{
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
}
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
{
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
client.Mafile.AccountName);
result = await client.DoRequest(func);
if (result.IsSuccess)
{
InformRequestSuccessful(client.Mafile);
return result.Data;
}
}
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
{
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
}
AddError(client.Mafile, result.Exception!);
throw result.Exception!;
}
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
bool withSessionHandler = true)
{
try
{
return withSessionHandler
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
GetTimerPrefix(client.Mafile)))
: Result<T>.Success(await req());
}
catch (SessionInvalidException ex)
{
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
return Result<T>.Error(ex);
}
}
private static void InformRequestSuccessful(Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data) && data.Clear())
{
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, error history cleared",
mafile.AccountName);
}
var client = mafile.LinkedClient;
if (client != null && client.Status.StatusType != PortableMaClientStatusType.Ok)
{
client.SetStatus(PortableMaClientStatus.Ok());
}
}
private static void AddError(Mafile mafile, Exception ex)
{
if (_errors.TryGetValue(mafile, out var data))
{
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
data.AddEntry(ex);
}
}
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
{
if (_errors.TryGetValue(mafile, out var data))
{
var oldestError = data.GetOldestErrorTime();
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
{
return true;
}
}
return false;
}
private static bool PreviouslyHadNoErrors(this Mafile mafile)
{
if (_errors.TryGetValue(mafile, out var data))
{
return data.NoErrors;
}
return true;
}
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
{
if (!_errors.TryGetValue(mafile, out var data)) return false;
var latestError = data.GetTimeFromLastError();
if (latestError.HasValue && latestError.Value > timeAgo)
{
return true;
}
return false;
}
public static bool IsReady(Mafile maf)
{
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
}
private static string GetTimerPrefix(Mafile mafile)
{
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
}
private static string GetLocalization(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
}
private static string GetPortableMaClientStatus(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, "PortableMaClientStatus", key);
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MAAC;
@@ -93,6 +94,7 @@ public static class MAACStorage
public static void Initialize()
{
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
if (!File.Exists("maac.json")) return;
try
{
@@ -107,6 +109,7 @@ public static class MAACStorage
{
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
Clients[fileName] = storedClient;
}
}
@@ -117,8 +120,6 @@ public static class MAACStorage
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
}
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
}
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
@@ -58,8 +58,7 @@ public static class MultiAccountAutoConfirmer
private static async Task TimerConfirmInternal()
{
var clients = Lock.ReadLock(() => Clients.ToArray());
var enabledClients = clients.Where(x => x.LinkedClient is {IsError: false}).ToArray();
var enabledClients = GetReadyAccounts();
enabledClients = DistributeEvenly(enabledClients).ToArray();
var confirmed = 0;
await Task.Run(async () =>
@@ -69,7 +68,8 @@ public static class MultiAccountAutoConfirmer
var conf = 0;
try
{
conf = await client.LinkedClient!.Confirm();
conf = await MAACRequestHandler.HandleRequest(client.LinkedClient!,
() => client.LinkedClient!.Confirm());
}
catch (ObjectDisposedException)
{
@@ -114,6 +114,21 @@ public static class MultiAccountAutoConfirmer
}
}
private static IEnumerable<Mafile> GetReadyAccounts()
{
var clients = Lock.ReadLock(() => Clients.ToArray());
var res = new List<Mafile>();
foreach (var maf in clients)
{
if (maf.LinkedClient == null) continue;
if (MAACRequestHandler.IsReady(maf))
{
res.Add(maf);
}
}
return res;
}
public static bool TryAddToConfirm(Mafile mafile)
{
@@ -122,6 +137,7 @@ public static class MultiAccountAutoConfirmer
if (Clients.Contains(mafile)) return false;
mafile.LinkedClient = new PortableMaClient(mafile);
Clients.Add(mafile);
MAACRequestHandler.Register(mafile);
return true;
});
}
@@ -134,6 +150,7 @@ public static class MultiAccountAutoConfirmer
Clients.Remove(mafile);
mafile.LinkedClient?.Dispose();
mafile.LinkedClient = null;
MAACRequestHandler.Unregister(mafile);
});
}
+22 -95
View File
@@ -11,11 +11,9 @@ using AchiesUtilities.Web.Proxy;
using CommunityToolkit.Mvvm.ComponentModel;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Utility;
using SteamLib.Api.Mobile;
using SteamLib.Api.Trade;
using SteamLib.Authentication;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile.Confirmations;
using SteamLib.Web;
using SteamLibForked.Abstractions;
@@ -34,7 +32,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
[ObservableProperty] private bool _autoConfirmMarket;
[ObservableProperty] private bool _autoConfirmTrades;
[ObservableProperty] private bool _isError;
[ObservableProperty] private PortableMaClientStatus _status = PortableMaClientStatus.Ok();
public PortableMaClient(Mafile mafile)
{
@@ -46,6 +44,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
ClientHandler = pair.Handler;
UpdateCookies(mafile.SessionData);
Mafile.PropertyChanged += Mafile_PropertyChanged;
SetStatus(PortableMaClientStatus.Ok());
}
private void Mafile_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -56,37 +55,21 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
}
private void UpdateCookies(IMobileSessionData? sessionData)
{
Application.Current.Dispatcher.Invoke(() => IsError = sessionData == null);
var newStatus = MAACRequestHandler.ClearErrors(Mafile);
if (!newStatus.Equals(Status))
SetStatus(newStatus);
ClientHandler.CookieContainer.ClearAllCookies();
if (sessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
public async Task<int> Confirm()
{
Proxy.SetData(Mafile.Proxy?.Data ?? MaClient.DefaultProxy);
List<Confirmation> conf;
try
{
conf = (await HandleTimerRequest(GetConfirmations)).ToList();
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
return 0;
}
var conf = (await GetConfirmations()).ToList();
var toConfirm = new List<Confirmation>();
if (AutoConfirmMarket)
@@ -103,20 +86,12 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
if (toConfirm.Count == 0) return 0;
try
{
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
return 0;
}
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await SendConfirmations(toConfirm);
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
@@ -148,58 +123,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
}
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func)
{
Exception innerException;
try
{
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
}
catch (OperationCanceledException ex)
{
innerException = ex; //Ignored
}
catch (SessionInvalidException ex)
{
if (IgnoreTimerErrors())
{
Shell.Logger.Warn(
"Timer {accountName}: Session error while requesting in timer. Ignored due to IgnorePatchTuesdayErrors setting",
Mafile.AccountName);
}
else
{
Shell.Logger.Warn("Timer {accountName}: Session error while requesting in timer. Timer disabled",
Mafile.AccountName);
SetError();
}
innerException = ex;
}
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetTimerPrefix()))
{
innerException = ex;
}
throw new ApplicationException("Swallowed", innerException);
}
private bool IgnoreTimerErrors()
{
if (Settings.Instance.IgnorePatchTuesdayErrors == false) return false;
var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var pstNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, pstZone);
var startTime = pstNow.Date.AddHours(15).AddMinutes(55); // 15:55 PST //22:55 GMT //00:55 GMT+2
var endTime = pstNow.Date.AddHours(17).AddMinutes(15); // 17:15 PST //00:15 GMT //02:15 GMT+2
return pstNow.DayOfWeek == DayOfWeek.Tuesday && pstNow >= startTime && pstNow <= endTime;
}
private HttpClientHandlerPair Chp()
public HttpClientHandlerPair Chp()
{
return new HttpClientHandlerPair(Client, ClientHandler);
}
@@ -214,11 +138,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
return GetLocalization("TimerPrefix") + Mafile.AccountName + ": ";
}
public void SetError()
public void SetStatus(PortableMaClientStatus status)
{
Application.Current.Dispatcher.Invoke(() => IsError = true);
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
Application.Current.Dispatcher.Invoke(() => Status = status);
if (status.StatusType == PortableMaClientStatusType.Error)
{
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + status.Message);
}
}
public void Dispose()
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NebulaAuth.Model.MAAC;
public sealed class PortableMaClientErrorData
{
public SortedDictionary<DateTime, Exception> Values { get; } = new();
public bool NoErrors => Values.Count == 0;
private readonly object _lock = new();
public DateTime? GetOldestErrorTime()
{
if (Values.Count == 0) return null;
return Values.Keys.First();
}
public void AddEntry(Exception ex)
{
lock (_lock)
Values[DateTime.UtcNow] = ex;
}
public bool Clear()
{
if (NoErrors) return false;
lock (_lock)
Values.Clear();
return true;
}
public TimeSpan? GetTimeFromLastError()
{
if (Values.Count == 0) return null;
var lastEntry = Values.Keys.Last();
return DateTime.UtcNow - lastEntry;
}
}
@@ -0,0 +1,26 @@
namespace NebulaAuth.Model.MAAC;
public record PortableMaClientStatus(PortableMaClientStatusType StatusType, string? Message = null)
{
public static PortableMaClientStatus Ok()
{
return new PortableMaClientStatus(PortableMaClientStatusType.Ok);
}
public static PortableMaClientStatus Warning(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Warning, message);
}
public static PortableMaClientStatus Error(string? message)
{
return new PortableMaClientStatus(PortableMaClientStatusType.Error, message);
}
}
public enum PortableMaClientStatusType
{
Ok,
Warning,
Error
}
+29
View File
@@ -0,0 +1,29 @@
using System;
namespace NebulaAuth.Model.MAAC;
public struct Result<T>
{
public bool IsSuccess { get; }
public T Data => !IsSuccess ? throw new InvalidOperationException("No data available") : _data!;
public Exception? Exception { get; }
private readonly T? _data;
private Result(bool success, T? data, Exception? exception)
{
IsSuccess = success;
_data = data;
Exception = exception;
}
public static Result<T> Success(T data)
{
return new Result<T>(true, data, null);
}
public static Result<T> Error(Exception ex)
{
return new Result<T>(false, default, ex);
}
}
+2 -12
View File
@@ -38,17 +38,7 @@ public static class MaClient
{
ClientHandler.CookieContainer.ClearAllCookies();
if (account == null) return;
if (account.SessionData != null)
{
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
}
else
{
ClientHandler.CookieContainer.ClearSteamCookies();
ClientHandler.CookieContainer.AddMinimalMobileCookies();
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
@@ -153,7 +143,7 @@ public static class MaClient
if (mafile.SessionData.RefreshToken.IsExpired)
throw new SessionPermanentlyExpiredException();
if (ignoreAccessToken == false)
if (!ignoreAccessToken)
{
var access = mafile.SessionData.GetMobileToken();
if (access == null || access.Value.IsExpired)
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.MafileExport;
public class ExportResult
{
public string? Error { get; }
public Dictionary<Mafile, string>? Exported { get; }
public List<string>? NotFound { get; }
public List<string>? Conflict { get; }
[MemberNotNullWhen(true, nameof(Exported))]
[MemberNotNullWhen(true, nameof(NotFound))]
[MemberNotNullWhen(true, nameof(Conflict))]
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
List<string>? conflict)
{
Error = error;
Exported = exported;
NotFound = notFound;
Conflict = conflict;
}
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
: this(null, exported, notFound, conflict)
{
}
public ExportResult(string? error)
: this(error, null, null, null)
{
}
}
@@ -0,0 +1,16 @@
namespace NebulaAuth.Model.MafileExport;
public class MafileExportTemplate
{
public string Name { get; set; }
public bool UseLoginAsMafileName { get; set; }
public bool IncludeSharedSecret { get; set; }
public bool IncludeIdentitySecret { get; set; }
public bool IncludeRCode { get; set; }
public bool IncludeSessionData { get; set; }
public bool IncludeOtherInfo { get; set; }
public bool IncludeNebulaProxy { get; set; }
public bool IncludeNebulaPassword { get; set; }
public bool IncludeNebulaGroup { get; set; }
public string? Path { get; set; }
}
@@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.Session;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporter
{
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
{
if (template.Path == null || !PathIsValid(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
}
if (!Directory.Exists(template.Path))
{
Directory.CreateDirectory(template.Path);
}
if (IsNebulaUtilizedDirectory(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
"MafileExporter.NebulaUtilizedDirectory"));
}
if (!IsAllowedToWrite(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
}
var exported = new Dictionary<Mafile, string>();
var notFound = new List<string>();
var conflict = new List<string>();
foreach (var key in keys)
{
SteamId? steamId = null;
if (SteamId64.TryParse(key, out var id64))
{
steamId = id64;
}
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
if (maf != null)
{
var fileName = await ExportMafile(template.Path, template, maf);
if (fileName == null)
{
conflict.Add(key);
continue;
}
exported[maf] = fileName;
}
else
{
notFound.Add(key);
}
}
return new ExportResult(exported, notFound, conflict);
}
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
{
// We preserve SteamId even if other info is not included
var session = template.IncludeSessionData
? mafile.SessionData
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
var serializeMaf = new Mafile
{
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
AccountName = mafile.AccountName,
SessionData = session,
SteamId = mafile.SteamId,
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
LinkedClient = null,
Filename = null
};
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
var fileName = strategy.GetMafileName(serializeMaf);
var fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return null;
}
await File.WriteAllTextAsync(fullPath, serialized);
return fullPath;
}
private static bool PathIsValid(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;
try
{
var full = Path.GetFullPath(path);
var invalidPathChars = Path.GetInvalidPathChars();
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
var invalidFileChars = Path.GetInvalidFileNameChars();
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
{
if (string.IsNullOrEmpty(part)) continue;
if (part is [_, ':']) continue;
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsAllowedToWrite(string path)
{
try
{
var testFilePath = Path.Combine(path, "test.tmp");
using (var fs = File.Create(testFilePath))
{
}
File.Delete(testFilePath);
return true;
}
catch
{
return false;
}
}
private static bool IsNebulaUtilizedDirectory(string path)
{
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporterStorage
{
private const string FILE_NAME = "export_templates.json";
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
public static void Initialize()
{
LoadOrCreateTemplate();
}
public static MafileExportTemplate? GetTemplate(string name)
{
return Templates.FirstOrDefault(x => x.Name == name);
}
public static void AddTemplate(MafileExportTemplate template)
{
Templates.Add(template);
Save();
}
public static void DeleteTemplate(MafileExportTemplate template)
{
if (Templates.Remove(template))
{
Save();
}
}
private static void LoadOrCreateTemplate()
{
if (!File.Exists(FILE_NAME))
{
Templates.Clear();
foreach (var mafileExportTemplate in CreateDefaultTemplate())
{
Templates.Add(mafileExportTemplate);
}
Save();
return;
}
var json = File.ReadAllText(FILE_NAME);
try
{
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
if (loadedTemplates != null)
{
Templates.Clear();
foreach (var tmpl in loadedTemplates)
{
Templates.Add(tmpl);
}
}
}
catch (Exception ex)
{
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
Templates.Clear();
Save();
}
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
File.WriteAllText(FILE_NAME, json);
}
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
{
return
[
new MafileExportTemplate
{
Name = "Default",
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
},
new MafileExportTemplate
{
Name = "Auth only",
UseLoginAsMafileName = true,
IncludeSharedSecret = true,
IncludeIdentitySecret = false,
IncludeRCode = false,
IncludeSessionData = false,
IncludeOtherInfo = false,
IncludeNebulaProxy = false,
IncludeNebulaPassword = false,
IncludeNebulaGroup = false,
Path = null
}
];
}
}
@@ -0,0 +1,8 @@
namespace NebulaAuth.Model.Mafiles;
public enum AddMafileResult
{
Added,
AlreadyExist,
Error
}
@@ -0,0 +1,50 @@
using System;
namespace NebulaAuth.Model.Mafiles;
public class MafileImportSummary
{
public int Added { get; private set; }
public int NotAdded { get; private set; }
public int Errors { get; private set; }
public void AddedOne()
{
Added++;
}
public void SkippedOne()
{
NotAdded++;
}
public void ErrorOne()
{
Errors++;
}
public void Add(int added = 0, int notAdded = 0, int errors = 0)
{
Added += added;
NotAdded += notAdded;
Errors += errors;
}
public void Apply(AddMafileResult result)
{
switch (result)
{
case AddMafileResult.Added:
AddedOne();
break;
case AddMafileResult.AlreadyExist:
SkippedOne();
break;
case AddMafileResult.Error:
ErrorOne();
break;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
}
@@ -0,0 +1,11 @@
namespace NebulaAuth.Model.Mafiles;
public class MafilesBulkRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + Conflict;
public int Errors { get; set; }
public int Conflict { get; set; }
public string BackupFileName { get; set; }
}
@@ -0,0 +1,38 @@
using System.IO;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.Mafiles;
internal static class MafilesStorageHelper
{
/// <summary>
/// Returns <see cref="Mafile.Filename" /> or creates a new one and updates the property.
/// <see cref="Mafile.Filename" /> is always not <see langword="null" /> after this method call.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static string GetOrUpdateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(Storage.MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(Storage.MafilesDirectory, fileName);
}
/// <summary>
/// Creates mafile file name according to the current settings.
/// </summary>
/// <param name="data"></param>
/// <param name="useAccountName"></param>
/// <returns></returns>
public static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -11,24 +11,30 @@ using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.Mafiles;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model;
namespace NebulaAuth.Model.Mafiles;
public static class Storage
{
public const string DIR_MAFILES = "maFiles";
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
public static readonly string[] MafilesDirectories;
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
static Storage()
{
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
}
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
Directory.CreateDirectory(MafilesDirectory);
@@ -45,7 +51,7 @@ public static class Storage
await Task.Run(() =>
{
return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = threadCount },
async (file, ct) =>
{
try
@@ -67,25 +73,46 @@ public static class Storage
/// </summary>
/// <param name="path"></param>
/// <param name="overwrite"></param>
/// <exception cref="FormatException"></exception>
/// <returns> Result of adding mafile</returns>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static void AddNewMafile(string path, bool overwrite)
public static async Task<AddMafileResult> AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = ReadMafile(path);
data = await ReadMafileAsync(path);
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
catch (Exception ex) when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Can't load mafile");
throw new FormatException("File data is not valid", ex);
}
await AddNewMafileInternal(data, overwrite);
return AddMafileResult.Added;
}
/// <summary>
/// </summary>
/// <param name="data"></param>
/// <param name="overwrite"></param>
/// <returns> Result of adding mafile</returns>
/// <exception cref="SessionInvalidException"></exception>
public static Task<AddMafileResult> AddNewMafileFromData(Mafile data, bool overwrite)
{
return AddNewMafileInternal(data, overwrite);
}
private static async Task<AddMafileResult> AddNewMafileInternal(Mafile data, bool overwrite)
{
ArgumentNullException.ThrowIfNull(data);
if (string.IsNullOrWhiteSpace(data.AccountName))
throw new FormatException("File data is not valid. Missing AccountName");
{
Shell.Logger.Warn("Mafile account name is empty");
return AddMafileResult.Error;
}
try
{
@@ -93,22 +120,19 @@ public static class Storage
}
catch (Exception ex)
{
throw new FormatException("Can't generate code on this mafile", ex);
Shell.Logger.Error(ex, "Can't generate code for mafile {accountName}", data.AccountName);
return AddMafileResult.Error;
}
if (overwrite == false && File.Exists(GetOrCreateMafilePath(data)))
data.Filename = null;
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
return AddMafileResult.AlreadyExist;
}
SaveMafile(data);
}
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
return NebulaSerializer.Deserialize(str, path);
await SaveMafileAsync(data);
return AddMafileResult.Added;
}
public static async Task<Mafile> ReadMafileAsync(string path)
@@ -117,66 +141,52 @@ public static class Storage
return NebulaSerializer.Deserialize(str, path);
}
public static void SaveMafile(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
if (existed != null)
{
var index = MaFiles.IndexOf(existed);
MaFiles[index] = data;
}
else
{
MaFiles.Add(data);
}
}
public static async Task SaveMafileAsync(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
if (existed != null)
// Replace if exist
for (var i = 0; i < MaFiles.Count; i++)
{
var index = MaFiles.IndexOf(existed);
MaFiles[index] = data;
}
else
{
MaFiles.Add(data);
var existed = MaFiles[i];
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
{
MaFiles[i] = data;
return;
}
}
MaFiles.Add(data);
}
public static void UpdateMafile(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static async Task UpdateMafileAsync(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var sourcePath = GetOrCreateMafilePath(data);
var destinationPath = Path.Combine(DIR_REMOVED_MAFILES, data.Filename + ".mafile");
var destinationPathFinal = destinationPath;
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
var i = 0;
while (File.Exists(destinationPathFinal))
{
i++;
destinationPathFinal = destinationPath + $" ({i})";
destinationPathFinal = destinationPathBase + $" ({i})";
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
}
File.Copy(sourcePath, destinationPathFinal, false);
@@ -184,24 +194,6 @@ public static class Storage
MaFiles.Remove(data);
}
private static string GetOrCreateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(MafilesDirectory, fileName);
}
private static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
public static string? TryGetMafilePath(Mafile data)
{
@@ -221,9 +213,10 @@ public static class Storage
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static async Task<MafileRenameResult> RenameMafiles(bool loginAsFileName, IProgress<double>? progress = null)
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
IProgress<double>? progress = null)
{
if (MaFiles.Count == 0) return new MafileRenameResult();
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
var now = DateTime.Now;
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
@@ -248,14 +241,14 @@ public static class Storage
if (counter % 5 == 0)
{
progress?.Report(counter / (double) files.Count);
progress?.Report(counter / (double)files.Count);
await Task.Delay(10);
}
}
}
var mafiles = MaFiles.ToList();
var res = new MafileRenameResult
var res = new MafilesBulkRenameResult
{
Total = mafiles.Count,
BackupFileName = backupFileName
@@ -266,7 +259,7 @@ public static class Storage
{
try
{
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
@@ -314,14 +307,4 @@ public static class Storage
res.Errors += 1;
}
}
public class MafileRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + Conflict;
public int Errors { get; set; }
public int Conflict { get; set; }
public string BackupFileName { get; set; }
}
}
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafilesLegacy;
/// <summary>
/// Helper class for detecting and handling SDA encrypted mafiles.
/// This is used in the legacy mafile handling code to support importing SDA encrypted mafiles without requiring the
/// user to manually decrypt them first.
/// </summary>
public static class SDAEncryptionHelper
{
private const string ManifestFileName = "manifest.json";
public static bool LooksLikeSdaEncryptedBlob(string content)
{
if (string.IsNullOrWhiteSpace(content))
return false;
var trimmed = content.Trim();
if (trimmed.StartsWith('{') || trimmed.Length < 64)
return false;
Span<byte> buffer = stackalloc byte[trimmed.Length];
return Convert.TryFromBase64String(trimmed, buffer, out _);
}
public static Context? TryDetect(string mafilePath, SDAManifest? sdaManifest)
{
if (string.IsNullOrWhiteSpace(mafilePath))
return null;
if (sdaManifest == null)
{
var dir = Path.GetDirectoryName(mafilePath);
if (string.IsNullOrWhiteSpace(dir))
return null;
var manifestPath = FindManifestPath(dir);
if (manifestPath == null)
return null;
sdaManifest = TryReadEncryptedManifest(manifestPath);
}
if (sdaManifest is not {Encrypted: true} || sdaManifest.Entries.Length == 0)
return null;
var fileName = Path.GetFileName(mafilePath);
if (string.IsNullOrWhiteSpace(fileName))
return null;
var entries = sdaManifest.Entries.ToDictionary(x => Path.GetFileName(x.Filename), StringComparer.OrdinalIgnoreCase);
return new Context(sdaManifest, entries, null);
}
private static string? FindManifestPath(string startDirectory)
{
var current = startDirectory;
for (var i = 0; i < 2; i++)
{
var candidate = Path.Combine(current, ManifestFileName);
if (File.Exists(candidate))
return candidate;
var parent = Directory.GetParent(current);
if (parent?.FullName == null)
break;
current = parent.FullName;
}
return null;
}
private static SDAManifest? TryReadEncryptedManifest(string manifestPath)
{
try
{
var manifestText = File.ReadAllText(manifestPath);
var manifest = JsonConvert.DeserializeObject<SDAManifest>(manifestText);
if (manifest is not {Encrypted: true} || manifest.Entries.Length == 0)
return null;
return manifest;
}
catch (Exception)
{
return null;
}
}
public static string? TryDecrypt(string content, string path, Context context)
{
if (string.IsNullOrWhiteSpace(context.Password))
return null;
// If we have password, check if we have entry
var entry = context.GetEntry(path);
if (entry == null)
{
return null;
}
// We have entry, try to decrypt
return SDAEncryptor.TryDecryptData(context.Password, entry.EncryptionSalt, entry.EncryptionIv, content,
out var decrypted)
? decrypted
: null;
}
/// <summary>
/// Result of SDA encrypted mafile detection. When not null, the caller can decrypt
/// using SDAEncryptor.DecryptData(password, Entry.EncryptionSalt, Entry.EncryptionIv, fileContent).
/// </summary>
public sealed class Context
{
public SDAManifest SdaManifest { get; }
public Dictionary<string, SDAManifestEntry> Entries { get; }
public string? Password { get; }
public Context(SDAManifest sdaManifest, Dictionary<string, SDAManifestEntry> entries, string? password)
{
SdaManifest = sdaManifest;
Entries = entries;
Password = password;
}
public Context WithPassword(string? password)
{
return new Context(SdaManifest, Entries, password);
}
public SDAManifestEntry? GetEntry(string path)
{
var fileName = Path.GetFileName(path);
return Entries.GetValueOrDefault(fileName);
}
}
}
@@ -1,7 +1,10 @@
using System.Security.Cryptography;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
namespace NebulaAuth.LegacyConverter;
namespace NebulaAuth.Model.MafilesLegacy;
#pragma warning disable all
#pragma warning disable SYSLIB0023
@@ -83,6 +86,20 @@ public static class SDAEncryptor
}
}
public static bool TryDecryptData(string password, string passwordSalt, string IV, string encryptedData, [NotNullWhen(true)] out string? plaintext)
{
plaintext = null;
try
{
plaintext = DecryptData(password, passwordSalt, IV, encryptedData);
return plaintext != null;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Tries to decrypt and return data given an encrypted base64 encoded string. Must use the same
/// password, salt, IV, and ciphertext that was used during the original encryption of the data.
@@ -4,15 +4,15 @@
//Pragma disable for legacy code
namespace NebulaAuth.LegacyConverter;
namespace NebulaAuth.Model.MafilesLegacy;
public class Manifest
public class SDAManifest
{
[JsonProperty("encrypted")] public bool Encrypted { get; set; }
[JsonProperty("first_run")] public bool FirstRun { get; set; }
[JsonProperty("entries")] public Entry[] Entries { get; set; }
[JsonProperty("entries")] public SDAManifestEntry[] Entries { get; set; }
[JsonProperty("periodic_checking")] public bool PeriodicChecking { get; set; }
@@ -28,7 +28,7 @@ public class Manifest
[JsonProperty("auto_confirm_trades")] public bool AutoConfirmTrades { get; set; }
}
public class Entry
public class SDAManifestEntry
{
[JsonProperty("encryption_iv")] public string EncryptionIv { get; set; }
+2 -2
View File
@@ -38,7 +38,7 @@ public static class NebulaSerializer
var data = Serializer.Deserialize(cont);
var mobileData = data.Data;
var info = data.Info;
if (info.IsExtended == false)
if (!info.IsExtended)
throw new FormatException("Mafile is not extended data");
@@ -58,7 +58,7 @@ public static class NebulaSerializer
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
{
if (dictionary.TryGetValue(name, out var prop) == false) return default;
if (!dictionary.TryGetValue(name, out var prop)) return default;
var value = prop.Value;
try
{
+1 -1
View File
@@ -28,7 +28,7 @@ public static class ProxyStorage
static ProxyStorage()
{
if (File.Exists("proxies.json") == false)
if (!File.Exists("proxies.json"))
return;
try
{
@@ -34,37 +34,28 @@ public static partial class SessionHandler
string? snackbarPrefix = null)
{
using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler");
var mobileTokenExpired = MobileTokenExpired(mafile);
var refreshTokenExpired = RefreshTokenExpired(mafile);
var password = GetPassword(mafile);
Exception? currentException = null;
if (!mobileTokenExpired)
if (!mafile.MobileTokenExpired())
{
try
{
return await func();
}
catch (SessionInvalidException ex)
when (refreshTokenExpired == false || password != null)
{
if (ex is SessionPermanentlyExpiredException)
{
Shell.Logger.Debug(ex, "RefreshToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
refreshTokenExpired = true;
}
else
{
Shell.Logger.Debug(ex, "MobileToken on mafile {name} {steamid} is expired", mafile.AccountName,
mafile.SessionData?.SteamId);
}
Shell.Logger.Warn(ex, "Session on mafile {name} {steamid} is invalid when mobile token is not expired",
mafile.AccountName, mafile.SessionData?.SteamId);
currentException = ex;
}
}
//State: mobileToken invalid/expired, refreshToken maybe not expired
if (!refreshTokenExpired)
if (!mafile.RefreshTokenExpired())
{
Shell.Logger.Info("Trying to refresh session on mafile {name} {steamid} using refresh token",
mafile.AccountName,
mafile.SessionData?.SteamId);
var refreshed = await RefreshInternal(chp, mafile);
if (refreshed)
{
@@ -77,19 +68,14 @@ public static partial class SessionHandler
}
catch (SessionInvalidException ex)
{
Shell.Logger.Info(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
Shell.Logger.Warn(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
mafile.AccountName, mafile.SessionData?.SteamId);
if (password == null)
throw;
currentException = ex;
}
}
}
Shell.Logger.Debug("Session on mafile {name} {steamid} is invalid/expired", mafile.AccountName,
mafile.SessionData?.SteamId);
//State: mobileToken invalid/expired, refreshToken invalid/expired
if (password != null)
if (mafile.HasPassword(out var password))
{
var logged = await LoginAgainInternal(chp, mafile, password, true);
if (logged)
@@ -100,41 +86,10 @@ public static partial class SessionHandler
}
}
//Nothing to do more, everything is expired
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG,
currentException);
}
private static bool MobileTokenExpired(Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(Mafile mafile)
{
var refreshToken = mafile.SessionData?.RefreshToken;
return refreshToken == null || refreshToken.Value.IsExpired;
}
private static string? GetPassword(Mafile mafile)
{
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
return PHandler.Decrypt(mafile.Password);
}
}
catch
{
// ignored
}
return null;
}
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
{
try
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using AchiesUtilities.Web.Models;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
@@ -27,7 +28,6 @@ public partial class SessionHandler //API
mafile.SessionData.SetMobileToken(newToken);
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
mafile.SetSessionData(mafile.SessionData);
await Storage.UpdateMafileAsync(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
@@ -49,7 +49,6 @@ public partial class SessionHandler //API
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
//Triggers PropertyChanged event for PortableMaClient handling session updated from MaClient
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
mafile.SetSessionData((MobileSessionData) result);
if (PHandler.IsPasswordSet)
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
@@ -0,0 +1,42 @@
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model;
public partial class SessionHandler //Helpers
{
private static bool MobileTokenExpired(this Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(this Mafile mafile)
{
var refreshToken = mafile.SessionData?.RefreshToken;
return refreshToken == null || refreshToken.Value.IsExpired;
}
private static bool HasPassword(this Mafile mafile, [NotNullWhen(true)] out string? plainPassword)
{
plainPassword = GetPassword(mafile);
return !string.IsNullOrWhiteSpace(plainPassword);
}
private static string? GetPassword(Mafile mafile)
{
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
return PHandler.Decrypt(mafile.Password);
}
}
catch
{
// ignored
}
return null;
}
}
+4 -2
View File
@@ -25,7 +25,7 @@ public partial class Settings : ObservableObject
static Settings()
{
if (File.Exists("settings.json") == false)
if (!File.Exists("settings.json"))
{
Instance = new Settings();
Instance.PropertyChanged += SettingsOnPropertyChanged;
@@ -94,7 +94,6 @@ public partial class Settings : ObservableObject
[ObservableProperty] private bool _legacyMode = true;
[ObservableProperty] private bool _allowAutoUpdate;
[ObservableProperty] private bool _useAccountNameAsMafileName;
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
[ObservableProperty] private double _leftOpacity = 0.4;
@@ -108,6 +107,9 @@ public partial class Settings : ObservableObject
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
#endregion
}
+16 -2
View File
@@ -1,6 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NebulaAuth.Core;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.MafileExport;
using NebulaAuth.Model.Mafiles;
using NLog;
using NLog.Extensions.Logging;
using SteamLib.Core;
@@ -14,8 +20,11 @@ public static class Shell
public static Logger Logger { get; private set; } = null!;
public static ILogger ExtensionsLogger { get; private set; } = null!;
public static void Initialize()
public static async Task Initialize()
{
File.Delete("log.log");
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Logger = LogManager.GetLogger("Logger");
var lp = new NLogLoggerProvider();
var logger = lp.CreateLogger("SteamLib");
@@ -27,13 +36,18 @@ public static class Shell
try
{
TimeAligner.AlignTime();
await TimeAligner.AlignTimeAsync();
}
catch (Exception ex)
{
throw new CantAlignTimeException("", ex);
}
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
ExtensionsLogger.LogDebug("Application started");
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NebulaAuth.Model.Update;
public class ChangelogEntry
{
[JsonProperty("version")]
public string Version { get; set; } = string.Empty;
[JsonProperty("date")]
public string Date { get; set; } = string.Empty;
[JsonProperty("changes")]
public List<ChangeItem> Changes { get; set; } = new();
}
public class ChangeItem
{
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
[JsonProperty("text")]
public string Text { get; set; } = string.Empty;
[JsonProperty("link")]
public string? Link { get; set; }
}
@@ -0,0 +1,59 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace NebulaAuth.Model.Update;
public class UpdateSettings
{
private static readonly string FilePath = Path.Combine("settings", "update.json");
[JsonProperty("skippedVersion")]
public string? SkippedVersion { get; set; }
[JsonProperty("remindAfter")]
public DateTime? RemindAfter { get; set; }
public static UpdateSettings Load()
{
try
{
if (!File.Exists(FilePath)) return new UpdateSettings();
var json = File.ReadAllText(FilePath);
return JsonConvert.DeserializeObject<UpdateSettings>(json) ?? new UpdateSettings();
}
catch
{
return new UpdateSettings();
}
}
public void Save()
{
var dir = Path.GetDirectoryName(FilePath)!;
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(FilePath, json);
}
public void SkipVersion(string version)
{
SkippedVersion = version;
RemindAfter = null;
Save();
}
public void SetRemindAfter(DateTime remindAfter)
{
RemindAfter = remindAfter;
Save();
}
public bool ShouldShow(string version)
{
if (SkippedVersion == version) return false;
if (RemindAfter.HasValue && DateTime.Now < RemindAfter.Value) return false;
return true;
}
}
+5 -2
View File
@@ -5,8 +5,11 @@
throwExceptions="true">
<targets>
<target xsi:type="File" name="File" fileName="log.log"
deleteOldFileOnStartup="true">
<target xsi:type="File"
name="File"
fileName="${basedir}/logs/log-${shortdate}.log"
concurrentWrites="true"
keepFileOpen="false">
<layout xsi:type='CompoundLayout'>
<layout xsi:type="JsonLayout" includeEventProperties="true" IncludeScopeProperties="true">
<attribute name="time" layout="${longdate}" />
+5 -2
View File
@@ -10,7 +10,7 @@
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
<AssemblyVersion>1.8.0</AssemblyVersion>
<AssemblyVersion>1.8.2</AssemblyVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
@@ -28,7 +28,7 @@
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
@@ -53,6 +53,9 @@
<Compile Update="View\Dialogs\LoginAgainOnImportDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="View\Dialogs\SdaPasswordDialog.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
+7 -5
View File
@@ -11,13 +11,16 @@ public static class LanguageUtility
{
var userCulture = CultureInfo.CurrentUICulture;
var userLang = userCulture.TwoLetterISOLanguageName;
var userRegion = userCulture.Name;
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
switch (userLang)
{
case "ru": return LocalizationLanguage.Russian;
case "uk": return LocalizationLanguage.Ukrainian;
case "en": return LocalizationLanguage.English;
case "zh": return LocalizationLanguage.ChineseSimplified;
case "fr": return LocalizationLanguage.French;
}
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
@@ -28,9 +31,8 @@ public static class LanguageUtility
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
];
if (cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase)))
return LocalizationLanguage.Russian;
return LocalizationLanguage.English;
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
? LocalizationLanguage.Russian
: LocalizationLanguage.English;
}
}
+90 -31
View File
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
@@ -6,7 +6,6 @@
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
<DataTemplate DataType="{x:Type confirmations:Confirmation}">
<Grid>
<Grid.ColumnDefinitions>
@@ -175,36 +174,96 @@
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start" materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
<Expander.Header>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Grid.Column="1">
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<!-- Confirm all -->
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="CheckAll" />
</Button>
<!-- Cancel all -->
<Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</Expander.Header>
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
MaxHeight="320">
<ItemsControl ItemsSource="{Binding Confirmations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
MaxHeight="32" Margin="0,0,10,0"
Source="{Binding ItemImageUri}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
</StackPanel>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
Margin="8,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Expander>
</DataTemplate>
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
<Grid>
@@ -58,7 +58,7 @@
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<Grid Grid.Row="2" Margin="10,10,10,0">
@@ -74,10 +74,10 @@
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
</UIElement.InputBindings>
</ComboBox>
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
<Grid Grid.Row="4" Margin="10,10,10,0">
<Grid.ColumnDefinitions>
@@ -0,0 +1,71 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.SdaPasswordDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:SdaPasswordDialogVM}"
Background="{DynamicResource WindowBackground}">
<Grid MinHeight="200" MinWidth="400" MaxWidth="500">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Lock" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontSize="16" VerticalAlignment="Center" Foreground="{DynamicResource BaseContentBrush}"
Margin="7,0,0,0" HorizontalAlignment="Left"
Text="{Tr SdaPasswordDialog.Title}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" CommandParameter="{StaticResource False}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<Grid Grid.Row="2" Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Tr SdaPasswordDialog.Message}" TextWrapping="Wrap"
Foreground="{DynamicResource BaseContentBrush}" Margin="10,10,10,8" />
<TextBox Grid.Row="1" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
materialDesign:TextFieldAssist.HasClearButton="True"
Padding="10" FontSize="15" Margin="10,0,10,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr SdaPasswordDialog.PasswordHint}"
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<Grid Grid.Row="2" Margin="10,16,10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button IsEnabled="{Binding IsFormValid}" IsDefault="True" Margin="0,5,5,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource True}" Content="{Tr Common.OK}" />
<Button IsCancel="True" Grid.Column="1" Margin="5,5,0,5"
Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
CommandParameter="{StaticResource False}" Content="{Tr Common.Cancel}" />
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class SdaPasswordDialog
{
public SdaPasswordDialog()
{
InitializeComponent();
}
}
@@ -0,0 +1,172 @@
<UserControl x:Class="NebulaAuth.View.Dialogs.UpdateDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
d:DataContext="{d:DesignInstance other:UpdateDialogVM}"
Background="{DynamicResource WindowBackground}"
MinWidth="420" MaxWidth="600">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Update" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18">
<Run Text="{Tr UpdateDialog.Title, IsDynamic=False}" />
<Run FontWeight="Bold" Text="{Binding Version, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Grid.Row="1" Opacity="0.7" />
<!-- Changelog -->
<Grid Grid.Row="2" Margin="12,10,12,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Tr UpdateDialog.Changelog}" FontSize="14" FontWeight="SemiBold"
Margin="0,0,0,8"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}" />
<!-- JSON changelog items -->
<materialDesign:Card Grid.Row="1" Padding="10">
<ScrollViewer MaxHeight="350" VerticalScrollBarVisibility="Auto"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource BooleanToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding Changelog.Changes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,6,0,6">
<Border CornerRadius="3" Padding="4,2,4,2" Margin="0,0,8,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{DynamicResource MaterialDesign.Brush.Primary}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="NEW">
<Setter Property="Background" Value="{DynamicResource SuccessBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="FIX">
<Setter Property="Background" Value="{DynamicResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="IMPROVEMENT">
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="SECURITY">
<Setter Property="Background" Value="{DynamicResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Type}" FontSize="11" FontWeight="Bold"
Foreground="White" />
</Border>
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"
VerticalAlignment="Center" FontSize="14" MaxWidth="420" />
<Button Style="{StaticResource MaterialDesignIconForegroundButton}"
Width="20" Height="20"
Padding="2"
Margin="4,0,0,0"
VerticalAlignment="Center"
Command="{Binding DataContext.OpenLinkCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding Link}"
ToolTip="Open Link"
Visibility="{Binding Link, Converter={StaticResource NullableToVisibilityConverter}}">
<materialDesign:PackIcon Kind="OpenInNew" Width="14" Height="14" />
</Button>
</StackPanel>
<Separator Opacity="0.2" Margin="0,2,0,2"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</materialDesign:Card>
<!-- No changelog fallback -->
<TextBlock Grid.Row="1" Text="{Tr UpdateDialog.NoChangelog}" FontSize="13" Opacity="0.6"
Visibility="{Binding HasJsonChangelog, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
</Grid>
<!-- Buttons -->
<Grid Grid.Row="3" Margin="12,6,12,14">
<!-- Normal buttons -->
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
<Button Style="{StaticResource MaterialDesignRaisedButton}"
Command="{Binding UpdateNowCommand}"
HorizontalAlignment="Stretch"
Margin="0,0,0,8"
Content="{Tr UpdateDialog.UpdateNow}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="8" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Style="{StaticResource MaterialDesignOutlinedButton}"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr UpdateDialog.RemindLater}" />
<Button Grid.Column="2"
Style="{StaticResource MaterialDesignFlatButton}"
Command="{Binding SkipVersionCommand}"
Content="{Tr UpdateDialog.SkipVersion}" />
</Grid>
</StackPanel>
<!-- Remind later options -->
<StackPanel Visibility="{Binding ShowRemindOptions, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock Text="{Tr UpdateDialog.RemindAfter}" Margin="0,0,0,8" FontSize="14" />
<WrapPanel>
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1H"
Content="{Tr UpdateDialog.RemindOptions.1Hour}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="1D"
Content="{Tr UpdateDialog.RemindOptions.1Day}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="3D"
Content="{Tr UpdateDialog.RemindOptions.3Days}" />
<Button Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0,0,4,4"
Command="{Binding SelectRemindOptionCommand}"
CommandParameter="7D"
Content="{Tr UpdateDialog.RemindOptions.7Days}" />
</WrapPanel>
<Button Style="{StaticResource MaterialDesignFlatButton}"
HorizontalAlignment="Left"
Command="{Binding ToggleRemindOptionsCommand}"
Content="{Tr Common.Cancel}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace NebulaAuth.View.Dialogs;
public partial class UpdateDialog
{
public UpdateDialog()
{
InitializeComponent();
}
}
+10 -1
View File
@@ -58,7 +58,7 @@
</Button>
<Button Margin="0,0,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="Website_Click" IsEnabled="False">
Click="Documentation_Click" IsEnabled="False">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Viewbox Width="22" Height="22" Margin="0 0 8 0">
<Canvas Width="22" Height="22">
@@ -72,6 +72,15 @@
</StackPanel>
</Button>
<Button Margin="0,5,0,5" FontSize="18" Style="{StaticResource MaterialDesignFlatButton}"
Click="CheckForUpdates_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<materialDesign:PackIcon Kind="Update" Width="22" Height="22" Margin="0 0 8 0"
Foreground="{DynamicResource MaterialDesign.Brush.Primary}" />
<TextBlock Text="{Tr LinksView.CheckForUpdates}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" IsCancel="True" Width="0"
Height="0" Visibility="Visible" />
</StackPanel>
+9 -1
View File
@@ -1,4 +1,6 @@
using System.Diagnostics;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
@@ -31,4 +33,10 @@ public partial class LinksView : UserControl
{
Process.Start(new ProcessStartInfo("https://yourwebsite.com") {UseShellExecute = true});
}
private void CheckForUpdates_Click(object sender, RoutedEventArgs e)
{
DialogHost.Close(null);
UpdateManager.CheckForUpdates(manual: true);
}
}
+240
View File
@@ -0,0 +1,240 @@
<UserControl x:Class="NebulaAuth.View.MafileExporterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
MinWidth="400"
MaxWidth="400"
Background="{DynamicResource WindowBackground}"
d:DataContext="{d:DesignInstance other:MafileExporterVM}"
d:Background="#141119"
d:Foreground="White"
FontSize="18">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FileExport" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr ExportDialog.ExportTitle}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Grid Grid.Row="2" Margin="15" TextElement.FontSize="14">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0"
Padding="10"
Style="{StaticResource MaterialDesignOutlinedComboBox}"
Margin="0,0,5,0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
ItemsSource="{Binding Templates}"
DisplayMemberPath="Name"
SelectedItem="{Binding CurrentTemplate}"
Visibility="{Binding EditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
<TextBox x:Name="EditTemplateNameTextBox"
Grid.Column="0"
Padding="10"
Margin="0,0,5,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
Text="{Binding CurrentTemplate.Name,
FallbackValue=''}"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
Visibility="{Binding EditMode, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBox.InputBindings>
<KeyBinding Key="Enter"
Command="{Binding ToggleEditModeCommand}" />
</TextBox.InputBindings>
</TextBox>
<Button Command="{Binding AddTemplateCommand}" Grid.Column="1">
<materialDesign:PackIcon Kind="Add" />
</Button>
<Button Command="{Binding ToggleEditModeCommand}" Click="EditButton_Click" Grid.Column="2">
<materialDesign:PackIcon Kind="Edit" />
</Button>
<Button Grid.Column="3" Command="{Binding RemoveCurrentTemplateCommand}">
<materialDesign:PackIcon Kind="Trash" />
</Button>
</Grid>
<Grid Margin="0,15,0,0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
MouseDoubleClick="PathTB_DoubleClick" materialDesign:TextFieldAssist.HasClearButton="True"
Text="{Binding CurrentTemplate.Path, FallbackValue=''}" Padding="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}" Grid.Column="0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.SelectPathHint}" />
<Button Command="{Binding OpenSelectDirectoryDialogCommand}" Grid.Column="1" Margin="5,0,0,0">
<materialDesign:PackIcon Kind="FolderOpen" />
</Button>
</Grid>
<Border Grid.Row="2" BorderThickness="1" CornerRadius="4" Margin="0,15,0,0" Padding="2">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.4" />
</Border.BorderBrush>
<Expander x:Name="TemplateExpander"
Header="{Tr ExportDialog.ExportSettingsHeader}"
materialDesign:ExpanderAssist.HorizontalHeaderPadding="5"
materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
HorizontalAlignment="Stretch"
Padding="5,0,5,5"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}">
<StackPanel>
<TextBlock FontWeight="DemiBold"
Text="{Tr ExportDialog.ExportSettingsCommon}" />
<CheckBox IsChecked="{Binding CurrentTemplate.UseLoginAsMafileName, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.UseLoginAsMafileName}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.UseLoginAsMafileName}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSharedSecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSharedSecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSharedSecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeIdentitySecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeIdentitySecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeIdentitySecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeRCode, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeRCode}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeRCode}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSessionData, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSessionData}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSessionData}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeOtherInfo, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeOtherInfo}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeOtherInfo}" />
<TextBlock FontWeight="DemiBold"
Margin="0,10,0,0"
Text="{Tr ExportDialog.ExportSettingsNebula}" />
<Grid Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{Binding CurrentTemplate.IncludeNebulaProxy, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaProxy}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaProxy}" />
<CheckBox Grid.Column="1"
HorizontalAlignment="Center"
IsChecked="{Binding CurrentTemplate.IncludeNebulaPassword, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaPassword}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaPassword}" />
<CheckBox Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CurrentTemplate.IncludeNebulaGroup, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaGroup}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaGroup}" />
</Grid>
</StackPanel>
</Expander>
</Border>
<TextBox GotFocus="ExportTB_GotFocus" PreviewMouseUp="ExportTB_GotFocus" Grid.Row="3"
Style="{StaticResource MaterialDesignOutlinedTextBox}" MaxHeight="600" Margin="0,10,0,0"
MinHeight="250" Height="250" AcceptsReturn="True"
Text="{Binding AccountsText, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ExportCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}}"
Name="AccountsTB" />
<TextBlock Grid.Row="3"
Text="{Tr ExportDialog.ExportAccountsPlaceholder}"
Margin="18,27,10,10"
Opacity="0.6"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="{Binding Text.IsEmpty,
ElementName=AccountsTB,
Converter={StaticResource BooleanToVisibilityConverter}}" />
<nebulaAuth:HintBox Margin="0,10,0,0" Severity="{Binding HintBoxSeverity}" Grid.Row="4"
Text="{Binding HintText}" />
</Grid>
</Grid>
<Separator Grid.Row="3" />
<Button Command="{Binding ExportCommand}" Style="{StaticResource MaterialDesignFlatDarkBgButton}" Grid.Row="4"
Margin="20,7,20,7" MaxWidth="150" Content="{Tr ExportDialog.ExportButton}" />
</Grid>
</UserControl>
@@ -0,0 +1,32 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace NebulaAuth.View;
public partial class MafileExporterView : UserControl
{
public MafileExporterView()
{
InitializeComponent();
}
private void PathTB_DoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is TextBox textBox)
{
textBox.SelectAll();
}
}
private void ExportTB_GotFocus(object sender, RoutedEventArgs e)
{
TemplateExpander.IsExpanded = false;
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(() => { EditTemplateNameTextBox.Focus(); }, DispatcherPriority.Input);
}
}
+5 -13
View File
@@ -55,27 +55,19 @@
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding Language}"
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding HideToTray}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding HideToTray}"
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding LegacyMode}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
Content="{Tr SettingsDialog.LegacyMafileMode}"
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
Margin="0,18,0,0"
BorderThickness="2"
IsChecked="{Binding IgnorePatchTuesdayErrors}"
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
<TextBlock TextWrapping="WrapWithOverflow"
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
</CheckBox>
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
</CheckBox>-->
<CheckBox Margin="0,20,0,0" IsChecked="{Binding UseIcon}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding UseIcon}"
Content="{Tr SettingsDialog.UseIndicator}"
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
@@ -191,10 +183,10 @@
PreviewMouseUp="Slider_OnMouseUp" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
IsChecked="{Binding ApplyBlurBackground}"
FontSize="15" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
FontSize="15"
IsChecked="{Binding RippleDisabled}" />
<Button Margin="0,15,0,0" Command="{Binding ResetThemeDefaultsCommand}"
-55
View File
@@ -1,55 +0,0 @@
<UserControl x:Class="NebulaAuth.View.UpdaterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
mc:Ignorable="d"
Foreground="WhiteSmoke"
FontFamily="{materialDesign:MaterialDesignFont}"
d:DataContext="{d:DesignInstance other:UpdaterVM}"
Background="{DynamicResource WindowBackground}"
Padding="10"
MaxWidth="500">
<d:DesignerProperties.DesignStyle>
<Style TargetType="UserControl">
<Setter Property="Background" Value="{DynamicResource WindowBackground}" />
</Style>
</d:DesignerProperties.DesignStyle>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontSize="24" Text="Обновления" />
<Separator Grid.Row="1" />
<TextBlock FontSize="16" Margin="5,10,0,10" TextWrapping="WrapWithOverflow" Grid.Row="2">
<Run Text="Доступна новая версия:" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.CurrentVersion}" />
<Run Text="&#x0a;Вы используете версию" />
<Run FontWeight="Bold" Text="{Binding UpdateInfoEventArgs.InstalledVersion}" />
<Run Text="Хотите обновить программу?" />
</TextBlock>
<Expander Header="Что изменилось?" Grid.Row="3">
<!--<wpf:WebView2 HorizontalAlignment="Stretch" Height="300"
Source="{Binding UpdateInfoEventArgs.ChangelogURL}"
/>-->
</Expander>
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="OK" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
<Button IsCancel="True" Grid.Column="1" Content="Cancel"
Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" />
</Grid>
</Grid>
</UserControl>
-9
View File
@@ -1,9 +0,0 @@
namespace NebulaAuth.View;
public partial class UpdaterView
{
public UpdaterView()
{
InitializeComponent();
}
}
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NLog;
using SteamLib;
@@ -224,7 +225,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
Logger.Error(ex, "Error during saving Nebula data to mafile");
}
Storage.SaveMafile(mafile);
await Storage.SaveMafileAsync(mafile);
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
}
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.ViewModel.Linker;
using Newtonsoft.Json;
+6 -24
View File
@@ -10,8 +10,8 @@ using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using SteamLib.SteamMobile;
using SteamLibForked.Exceptions.Authorization;
@@ -22,7 +22,6 @@ public partial class MainVM : ObservableObject
{
[UsedImplicitly] public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
public Mafile? SelectedMafile
{
get => _selectedMafile;
@@ -32,7 +31,7 @@ public partial class MainVM : ObservableObject
public bool IsMafileSelected => SelectedMafile != null;
public DialogHost CurrentDialogHost { get; set; } = null!;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = new(Storage.MaFiles);
private Mafile? _selectedMafile;
@@ -43,16 +42,14 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
UpdateManager.CheckForUpdates();
UpdateManager.CheckForUpdates(manual: false);
}
[RelayCommand]
public async Task Debug()
{
await DialogsController.ShowSetAccountsPasswordDialog();
}
private void SetMafile(Mafile? mafile)
{
if (mafile != SelectedMafile)
@@ -71,7 +68,9 @@ public partial class MainVM : ObservableObject
LoginAgainCommand.NotifyCanExecuteChanged();
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
ConfirmLoginCommand.NotifyCanExecuteChanged();
RemoveProxyCommand.NotifyCanExecuteChanged();
}
}
@@ -114,7 +113,7 @@ public partial class MainVM : ObservableObject
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SearchText = string.Empty;
PerformQuery();
}
@@ -134,12 +133,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
public async Task LinkAccount()
{
await DialogsController.ShowLinkerDialog();
}
[RelayCommand]
public async Task MoveAccount()
{
@@ -172,7 +165,6 @@ public partial class MainVM : ObservableObject
if (result.Success)
{
Storage.MoveToRemoved(selectedMafile);
MaFiles.Remove(selectedMafile);
}
}
else
@@ -218,16 +210,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
private static string GetLocalization(string key)
{
const string locPath = "MainVM";
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -76,18 +76,27 @@ public partial class MainVM //Confirmations
return SendConfirmation(SelectedMafile, confirmation, false);
}
private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
{
bool result;
try
{
if (confirmation is MarketMultiConfirmation multi)
switch (confirmation)
{
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
}
else
{
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
case MarketMultiConfirmation multi:
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
break;
case MarketConfirmation market:
result = await MaClient.SendConfirmation(mafile, market, confirm);
break;
default:
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
break;
}
}
catch (Exception ex)
@@ -96,13 +105,49 @@ public partial class MainVM //Confirmations
return;
}
if (result)
{
Confirmations.Remove(confirmation);
}
else
if (!result)
{
SnackbarController.SendSnackbar(GetLocalization("ConfirmationError"));
return;
}
UpdateConfirmationState(confirmation);
}
private void UpdateConfirmationState(Confirmation confirmation)
{
if (confirmation is MarketConfirmation item)
{
var parent = Confirmations
.OfType<MarketMultiConfirmation>()
.FirstOrDefault(p => p.Confirmations.Contains(item));
if (parent == null)
{
Confirmations.Remove(item);
return;
}
parent.Confirmations.Remove(item);
if (parent.Confirmations.Count == 0)
{
Confirmations.Remove(parent);
return;
}
if (parent.Confirmations.Count == 1)
{
var remaining = parent.Confirmations[0];
var idx = Confirmations.IndexOf(parent);
Confirmations[idx] = remaining;
return;
}
parent.Time = parent.Confirmations.FirstOrDefault()?.Time ?? parent.Time;
return;
}
Confirmations.Remove(confirmation);
}
}
+102 -43
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
@@ -13,6 +13,8 @@ using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Model.MafilesLegacy;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
@@ -25,6 +27,7 @@ public partial class MainVM //File //TODO: Refactor
{
public Settings Settings => Settings.Instance;
[RelayCommand]
private void OpenMafileFolder()
{
@@ -57,48 +60,50 @@ public partial class MainVM //File //TODO: Refactor
[RelayCommand]
private Task AddMafile()
{
var openFileDialog = new OpenFileDialog
var dialog = new OpenFileDialog
{
Filter = "Mafile|*.mafile;*.maFile",
Multiselect = false
};
var fs = openFileDialog.ShowDialog();
if (fs != true) return Task.CompletedTask;
var path = openFileDialog.FileName;
return AddMafile([path]);
return dialog.ShowDialog() == true
? AddMafile([dialog.FileName])
: Task.CompletedTask;
}
public async Task AddMafile(string[] path)
{
bool? confirmOverwrite = null;
var added = 0;
var notAdded = 0;
var errors = 0;
var summary = new MafileImportSummary();
SDAEncryptionHelper.Context? sdaContext = null;
var sdaPasswordPrompted = false;
foreach (var str in path)
{
try
{
Storage.AddNewMafile(str, confirmOverwrite ?? false);
added++;
var (mafile, passwordPrompted, context) = await TryReadMafile(str, sdaContext, sdaPasswordPrompted);
sdaContext = context;
sdaPasswordPrompted = passwordPrompted;
if (mafile == null)
{
summary.ErrorOne();
continue;
}
var overwrite = confirmOverwrite ?? false;
var res = await Storage.AddNewMafileFromData(mafile, overwrite);
if (res == AddMafileResult.AlreadyExist && confirmOverwrite == null)
{
confirmOverwrite =
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
res = await Storage.AddNewMafileFromData(mafile, confirmOverwrite.Value);
}
summary.Apply(res);
}
catch (FormatException)
{
errors++;
}
catch (IOException)
{
confirmOverwrite ??=
await DialogsController.ShowConfirmCancelDialog(GetLocalization("ConfirmMafileOverwrite"));
if (confirmOverwrite == true)
{
Storage.AddNewMafile(str, true);
added++;
}
else if (confirmOverwrite == false)
{
notAdded++;
}
summary.ErrorOne();
}
catch (MafileNeedReloginException ex)
{
@@ -107,11 +112,11 @@ public partial class MainVM //File //TODO: Refactor
var mafile = ex.Mafile;
if (await HandleAddMafileWithoutSession(mafile))
{
added++;
summary.AddedOne();
}
else
{
errors++;
summary.ErrorOne();
}
}
else
@@ -123,23 +128,50 @@ public partial class MainVM //File //TODO: Refactor
}
}
var msg = GetLocalization("Import");
if (added > 0)
{
msg += $" {GetLocalization("ImportAdded")} {added}.";
}
ShowImportSummary(summary);
}
if (notAdded > 0)
{
msg += $" {GetLocalization("ImportSkipped")} {notAdded}.";
}
if (errors > 0)
private async Task<MafileReadResult> TryReadMafile(string path, SDAEncryptionHelper.Context? sdaContext,
bool sdaPasswordPrompted)
{
try
{
msg += $" {GetLocalization("ImportErrors")} {errors}.";
}
var content = await File.ReadAllTextAsync(path);
Mafile mafile;
if (!SDAEncryptionHelper.LooksLikeSdaEncryptedBlob(content))
{
mafile = NebulaSerializer.Deserialize(content, path);
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
}
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
// Looks like encrypted, let's see if we have manifest
sdaContext ??= SDAEncryptionHelper.TryDetect(path, sdaContext?.SdaManifest);
if (sdaContext != null)
{
// We have manifest, but we must ensure we have password
if (sdaContext.Password == null && !sdaPasswordPrompted)
{
sdaPasswordPrompted = true;
var password = await DialogsController.ShowSdaPasswordDialog();
sdaContext = sdaContext.WithPassword(password);
}
var decrypted = SDAEncryptionHelper.TryDecrypt(content, path, sdaContext);
if (decrypted == null) return new MafileReadResult(null, sdaPasswordPrompted, sdaContext);
content = decrypted;
}
// If we are here, it means that either file is not encrypted, or we successfully decrypted it
mafile = NebulaSerializer.Deserialize(content, path);
return new MafileReadResult(mafile, sdaPasswordPrompted, sdaContext);
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Failed to import mafile");
throw new FormatException($"Failed to read mafile {Path.GetFileName(path)}", ex);
}
}
private async Task<bool> HandleAddMafileWithoutSession(Mafile data)
@@ -190,6 +222,28 @@ public partial class MainVM //File //TODO: Refactor
return result;
}
private static void ShowImportSummary(MafileImportSummary summary)
{
var msg = GetLocalization("Import");
if (summary.Added > 0)
{
msg += $" {GetLocalization("ImportAdded")} {summary.Added}.";
}
if (summary.NotAdded > 0)
{
msg += $" {GetLocalization("ImportSkipped")} {summary.NotAdded}.";
}
if (summary.Errors > 0)
{
msg += $" {GetLocalization("ImportErrors")} {summary.Errors}.";
}
SnackbarController.SendSnackbar(msg, TimeSpan.FromSeconds(2));
}
[RelayCommand]
private async Task RemoveMafile()
{
@@ -239,7 +293,7 @@ public partial class MainVM //File //TODO: Refactor
}
var arr = files.Cast<string>().ToArray();
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
if (arr.All(p => !p.ContainsIgnoreCase("mafile"))) return;
await AddMafile(arr);
@@ -293,4 +347,9 @@ public partial class MainVM //File //TODO: Refactor
if (mafile is not Mafile maf) return false;
return maf.Password != null && PHandler.IsPasswordSet;
}
private record MafileReadResult(
Mafile? Mafile,
bool SdaPasswordPrompted,
SDAEncryptionHelper.Context? SdaContext);
}
+3 -3
View File
@@ -5,8 +5,8 @@ using AchiesUtilities.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -88,7 +88,7 @@ public partial class MainVM //Groups
Storage.UpdateMafile(mafile);
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
QueryGroups();
if (Groups.All(g => g.Equals(mafGroup) == false))
if (Groups.All(g => !g.Equals(mafGroup)))
{
SelectedGroup = null;
}
@@ -104,7 +104,7 @@ public partial class MainVM //Groups
private void QueryGroups()
{
var groups = Storage.MaFiles
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
.Where(m => !string.IsNullOrWhiteSpace(m.Group))
.Select(m => m.Group)
.Distinct()
.Order()
+1 -1
View File
@@ -130,7 +130,7 @@ public partial class MainVM //MAAC
{
mafiles = mafiles.ToArray();
var turnOn = mafiles.All(m => m.LinkedClient == null || GetCurrentMode(m.LinkedClient) == false);
var turnOn = mafiles.All(m => m.LinkedClient == null || !GetCurrentMode(m.LinkedClient));
if (turnOn)
{
foreach (var mafile in mafiles)
+38
View File
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.View;
namespace NebulaAuth.ViewModel;
public partial class MainVM : ObservableObject
{
[RelayCommand]
private Task OpenSetPasswordsDialog()
{
return DialogsController.ShowSetAccountsPasswordDialog();
}
[RelayCommand]
private Task OpenExporterDialog()
{
return DialogsController.ShowExportDialog();
}
[RelayCommand]
public Task LinkAccount()
{
return DialogsController.ShowLinkerDialog();
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
}
+14 -6
View File
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -68,13 +69,20 @@ public partial class MainVM
CheckProxyExist();
}
[RelayCommand]
private void RemoveProxy()
[RelayCommand(CanExecute = nameof(RemoveProxyCanExecute))]
private void RemoveProxy(Mafile? mafile)
{
if (SelectedProxy == null) return;
if (SelectedMafile == null) return;
SelectedMafile.Proxy = null;
SetProxy(null, false); //Not system, triggered by user
mafile ??= SelectedMafile;
if (mafile?.Proxy == null) return;
mafile.Proxy = null;
if (mafile == SelectedMafile)
SetProxy(null, false); //Not system, triggered by user
}
private bool RemoveProxyCanExecute(Mafile? mafile)
{
mafile ??= SelectedMafile;
return mafile is { Proxy: not null };
}
private void CheckProxyExist()
@@ -0,0 +1,373 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model.MafileExport;
namespace NebulaAuth.ViewModel.Other;
public partial class MafileExporterVM : ObservableObject
{
public MafileExportTemplateVM? CurrentTemplate
{
get => _currentTemplate;
set => SetCurrentTemplate(value);
}
public ObservableCollection<MafileExportTemplateVM> Templates { get; }
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ExportCommand))]
private string? _accountsText;
private string? _cachedName;
private MafileExportTemplateVM? _currentTemplate;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddTemplateCommand), nameof(RemoveCurrentTemplateCommand))]
private bool _editMode;
[ObservableProperty] private HintBoxSeverity _hintBoxSeverity = HintBoxSeverity.Error;
[ObservableProperty] private string? _hintText;
public MafileExporterVM()
{
var templates = MafileExporterStorage.Templates
.Select(MafileExportTemplateVM.FromModel);
Templates = new ObservableCollection<MafileExportTemplateVM>(templates);
MafileExporterStorage.Templates.CollectionChanged += OnTemplatesOnCollectionChanged;
CurrentTemplate = Templates.FirstOrDefault();
}
private void SetCurrentTemplate(MafileExportTemplateVM? value)
{
if (_currentTemplate != null)
{
_currentTemplate.PropertyChanged -= OnCurrentTemplatePropertyChanged;
}
SetProperty(ref _currentTemplate, value, nameof(CurrentTemplate));
if (value != null)
{
value.PropertyChanged += OnCurrentTemplatePropertyChanged;
}
ToggleEditModeCommand.NotifyCanExecuteChanged();
RemoveCurrentTemplateCommand.NotifyCanExecuteChanged();
OpenSelectDirectoryDialogCommand.NotifyCanExecuteChanged();
ExportCommand.NotifyCanExecuteChanged();
}
private void OnTemplatesOnCollectionChanged(object? s, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (MafileExportTemplate newItem in e.NewItems!)
{
Templates.Add(MafileExportTemplateVM.FromModel(newItem));
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (MafileExportTemplate oldItem in e.OldItems!)
{
var vmToRemove = Templates.FirstOrDefault(x => x.Name == oldItem.Name);
if (vmToRemove != null)
{
Templates.Remove(vmToRemove);
}
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (MafileExportTemplate newItem in e.NewItems!)
{
var vmToReplace = Templates.FirstOrDefault(x => x.Name == newItem.Name);
if (vmToReplace != null)
{
var index = Templates.IndexOf(vmToReplace);
Templates[index] = MafileExportTemplateVM.FromModel(newItem);
}
}
break;
case NotifyCollectionChangedAction.Reset:
Templates.Clear();
break;
case NotifyCollectionChangedAction.Move:
default:
break;
}
}
private void OnCurrentTemplatePropertyChanged(object? sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
var currentTemplate = CurrentTemplate;
if (currentTemplate == null) return;
var key = propertyChangedEventArgs.PropertyName == nameof(MafileExportTemplateVM.Name)
? _cachedName!
: currentTemplate.Name;
var existed = MafileExporterStorage.GetTemplate(key);
if (existed != null)
{
currentTemplate.ApplyTo(existed);
MafileExporterStorage.Save();
}
else
{
MafileExporterStorage.AddTemplate(currentTemplate.ToModel());
}
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
private void ToggleEditMode()
{
EditMode = !EditMode;
if (EditMode) _cachedName = CurrentTemplate?.Name;
}
[RelayCommand(CanExecute = nameof(NotEditMode))]
private void AddTemplate()
{
ResetHintText();
var name = "New Template";
var i = 0;
while (Templates.Any(x => x.Name == name))
{
name = $"New Template ({++i})";
}
var template = new MafileExportTemplate
{
Name = name,
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
};
MafileExporterStorage.AddTemplate(template);
CurrentTemplate = Templates.FirstOrDefault(x => x.Name == name);
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNullAndNotEditMode))]
private void RemoveCurrentTemplate()
{
if (CurrentTemplate != null)
{
ResetHintText();
var index = Templates.IndexOf(CurrentTemplate);
var existed = MafileExporterStorage.GetTemplate(CurrentTemplate.Name);
if (existed != null)
{
MafileExporterStorage.DeleteTemplate(existed);
}
if (Templates.Count > 0)
{
if (index >= Templates.Count)
{
index = Templates.Count - 1;
}
CurrentTemplate = Templates[index];
}
else
{
CurrentTemplate = null;
}
}
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
private void OpenSelectDirectoryDialog()
{
if (CurrentTemplate == null) return;
var dialog = new FolderBrowserDialog
{
UseDescriptionForTitle = true,
ShowNewFolderButton = true
};
var result = dialog.ShowDialog();
if (result == DialogResult.OK)
{
CurrentTemplate!.Path = dialog.SelectedPath;
}
}
[RelayCommand(CanExecute = nameof(ExportCanExecute))]
private async Task Export()
{
var lines = AccountsText;
var template = CurrentTemplate;
if (string.IsNullOrWhiteSpace(lines) || template == null)
{
return;
}
var split = Regex
.Split(lines, "\r\n|\r|\n")
.Select(x => x.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToArray();
ResetHintText();
ExportResult res;
try
{
res = await MafileExporter.ExportMafiles(split, template.ToModel());
}
catch (Exception ex)
{
SetHintText(ex.Message);
return;
}
if (!res.Success)
{
SetHintText(res.Error);
return;
}
var hint = string.Format(LocManager.GetCodeBehindOrDefault("Exported", "MafileExporterVM.Result.Exported"),
res.Exported.Count);
if (res.NotFound.Count > 0)
{
hint += string.Format(LocManager.GetCodeBehindOrDefault("NotFound", "MafileExporterVM.Result.NotFound"),
res.NotFound.Count);
}
if (res.Conflict.Count > 0)
{
hint += string.Format(LocManager.GetCodeBehindOrDefault("Conflict", "MafileExporterVM.Result.Conflict"),
res.Conflict.Count);
}
SetHintText(hint, HintBoxSeverity.Info);
var errors = res.NotFound.Concat(res.Conflict).ToHashSet();
var errorLines = split.Where(errors.Contains);
var text = string.Join(Environment.NewLine, errorLines);
AccountsText = text;
}
private void SetHintText(string? text, HintBoxSeverity severity = HintBoxSeverity.Error)
{
HintText = text;
HintBoxSeverity = severity;
}
private void ResetHintText()
{
SetHintText(null);
}
#region CanExecute
private bool ExportCanExecute()
{
return CurrentTemplateNotNull() && !string.IsNullOrWhiteSpace(AccountsText);
}
private bool CurrentTemplateNotNull()
{
return CurrentTemplate != null;
}
private bool CurrentTemplateNotNullAndNotEditMode()
{
return CurrentTemplateNotNull() && NotEditMode();
}
private bool NotEditMode()
{
return !EditMode;
}
#endregion
}
public partial class MafileExportTemplateVM : ObservableObject
{
[ObservableProperty] private bool _includeIdentitySecret;
[ObservableProperty] private bool _includeNebulaGroup;
[ObservableProperty] private bool _includeNebulaPassword;
[ObservableProperty] private bool _includeNebulaProxy;
[ObservableProperty] private bool _includeOtherInfo;
[ObservableProperty] private bool _includeRCode;
[ObservableProperty] private bool _includeSessionData;
[ObservableProperty] private bool _includeSharedSecret;
[ObservableProperty] private string _name;
[ObservableProperty] private string? _path;
[ObservableProperty] private bool _useLoginAsMafileName;
public static MafileExportTemplateVM FromModel(MafileExportTemplate x)
{
return new MafileExportTemplateVM
{
Name = x.Name,
UseLoginAsMafileName = x.UseLoginAsMafileName,
IncludeSharedSecret = x.IncludeSharedSecret,
IncludeIdentitySecret = x.IncludeIdentitySecret,
IncludeRCode = x.IncludeRCode,
IncludeSessionData = x.IncludeSessionData,
IncludeOtherInfo = x.IncludeOtherInfo,
IncludeNebulaProxy = x.IncludeNebulaProxy,
IncludeNebulaPassword = x.IncludeNebulaPassword,
IncludeNebulaGroup = x.IncludeNebulaGroup,
Path = x.Path
};
}
public MafileExportTemplate ToModel()
{
return new MafileExportTemplate
{
Name = Name,
UseLoginAsMafileName = UseLoginAsMafileName,
IncludeSharedSecret = IncludeSharedSecret,
IncludeIdentitySecret = IncludeIdentitySecret,
IncludeRCode = IncludeRCode,
IncludeSessionData = IncludeSessionData,
IncludeOtherInfo = IncludeOtherInfo,
IncludeNebulaProxy = IncludeNebulaProxy,
IncludeNebulaPassword = IncludeNebulaPassword,
IncludeNebulaGroup = IncludeNebulaGroup,
Path = Path
};
}
public void ApplyTo(MafileExportTemplate model)
{
model.Name = Name;
model.UseLoginAsMafileName = UseLoginAsMafileName;
model.IncludeSharedSecret = IncludeSharedSecret;
model.IncludeIdentitySecret = IncludeIdentitySecret;
model.IncludeRCode = IncludeRCode;
model.IncludeSessionData = IncludeSessionData;
model.IncludeOtherInfo = IncludeOtherInfo;
model.IncludeNebulaProxy = IncludeNebulaProxy;
model.IncludeNebulaPassword = IncludeNebulaPassword;
model.IncludeNebulaGroup = IncludeNebulaGroup;
model.Path = Path;
}
}
@@ -61,7 +61,7 @@ public partial class ProxyManagerVM : ObservableObject
var split = input
.Split(Environment.NewLine)
.Where(s => string.IsNullOrWhiteSpace(s) == false)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(x => x.Trim())
.ToArray();
@@ -72,7 +72,7 @@ public partial class ProxyManagerVM : ObservableObject
var i = 0;
foreach (var s in split.Where(s => string.IsNullOrWhiteSpace(s) == false))
foreach (var s in split.Where(s => !string.IsNullOrWhiteSpace(s)))
{
i++;
var str = s;
@@ -0,0 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace NebulaAuth.ViewModel.Other;
public partial class SdaPasswordDialogVM : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsFormValid))]
private string _password = string.Empty;
public bool IsFormValid => !string.IsNullOrWhiteSpace(Password);
}
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.ViewModel.Other;
+7 -10
View File
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel.Other;
@@ -53,7 +54,7 @@ public partial class SettingsVM : ObservableObject
var targetValue = UseAccountNameAsMafileNamePreview;
if (UseAccountNameAsMafileName == targetValue) return;
RenameMafilesProgress = 0;
Storage.MafileRenameResult? result = null;
MafilesBulkRenameResult? result = null;
try
{
result = await Storage.RenameMafiles(targetValue, new Progress<double>(p => RenameMafilesProgress = p));
@@ -85,7 +86,7 @@ public partial class SettingsVM : ObservableObject
{
var l = GetLoc("PartialSuccess");
RenameResultText =
string.Format(l, result.Total, result.Renamed, result.Errors, result.Conflict, result.BackupFileName);
string.Format(l, result.Total, result.Renamed, result.Conflict, result.Errors, result.BackupFileName);
}
string GetLoc(string key)
@@ -127,7 +128,9 @@ public partial class SettingsVM : ObservableObject
{
{LocalizationLanguage.English, "English"},
{LocalizationLanguage.Russian, "Русский"},
{LocalizationLanguage.Ukrainian, "Українська"}
{LocalizationLanguage.Ukrainian, "Українська"},
{LocalizationLanguage.ChineseSimplified, "简体中文"},
{LocalizationLanguage.French, "Français"}
};
public Color? IconColor
@@ -141,7 +144,7 @@ public partial class SettingsVM : ObservableObject
get => IconColor != null;
set
{
if (value == false)
if (!value)
IconColor = null;
else
IconColor = Color.FromRgb(202, 39, 39);
@@ -184,12 +187,6 @@ public partial class SettingsVM : ObservableObject
}
}
public bool IgnorePatchTuesdayErrors
{
get => Settings.IgnorePatchTuesdayErrors;
set => Settings.IgnorePatchTuesdayErrors = value;
}
#endregion
#region Theme
@@ -0,0 +1,73 @@
using System;
using System.Diagnostics;
using System.Windows;
using AutoUpdaterDotNET;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Model.Update;
namespace NebulaAuth.ViewModel.Other;
public partial class UpdateDialogVM : ObservableObject
{
public UpdateInfoEventArgs Args { get; }
public ChangelogEntry? Changelog { get; }
public string? HtmlFallbackUrl { get; }
public string Version => Args.CurrentVersion.ToString();
public bool HasJsonChangelog => Changelog?.Changes?.Count > 0;
[ObservableProperty] private bool _showRemindOptions;
public UpdateDialogVM(UpdateInfoEventArgs args, ChangelogEntry? changelog, string? htmlFallbackUrl)
{
Args = args;
Changelog = changelog;
HtmlFallbackUrl = htmlFallbackUrl;
}
[RelayCommand]
private void UpdateNow()
{
DialogHost.Close(null);
if (AutoUpdater.DownloadUpdate(Args))
{
Application.Current.Shutdown();
}
}
[RelayCommand]
private void ToggleRemindOptions()
{
ShowRemindOptions = !ShowRemindOptions;
}
[RelayCommand]
private void SelectRemindOption(string option)
{
var delay = option switch
{
"1H" => TimeSpan.FromHours(1),
"1D" => TimeSpan.FromDays(1),
"3D" => TimeSpan.FromDays(3),
"7D" => TimeSpan.FromDays(7),
_ => TimeSpan.FromDays(1)
};
Core.UpdateManager.SetRemindAfter(delay);
DialogHost.Close(null);
}
[RelayCommand]
private void SkipVersion()
{
Core.UpdateManager.SkipVersion(Version);
DialogHost.Close(null);
}
[RelayCommand]
private void OpenLink(string url)
{
if (string.IsNullOrEmpty(url)) return;
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
}
@@ -1,14 +0,0 @@
using AutoUpdaterDotNET;
using CommunityToolkit.Mvvm.ComponentModel;
namespace NebulaAuth.ViewModel.Other;
public class UpdaterVM : ObservableObject
{
public UpdateInfoEventArgs UpdateInfoEventArgs { get; }
public UpdaterVM(UpdateInfoEventArgs args)
{
UpdateInfoEventArgs = args;
}
}
File diff suppressed because it is too large Load Diff
@@ -152,7 +152,7 @@ public static class SteamAuthenticatorLinkerApi
var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
var j = JObject.Parse(content);
if (j["success"]!.Value<bool>() == false)
if (!j["success"]!.Value<bool>())
return CheckPhoneResult.GeneralFailure;
if (j["is_voip"]!.Value<bool>())
@@ -187,7 +187,7 @@ public static class SteamAuthenticatorLinkerApi
var resp = await client.PostProto<IsAccountWaitingForEmailConfirmation_Response>(reqUri,
new EmptyMessage());
if (resp.IsWaiting == false) return true;
if (!resp.IsWaiting) return true;
await Task.Delay(resp.SecondsToWait * 1000);
}
@@ -44,10 +44,15 @@ public static class AdmissionHelper
/// <summary>
/// Clear and set new session
/// </summary>
public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData,
public static void SetSteamCookies(this CookieContainer container, ISessionData? sessionData,
string setLanguage = "english")
{
container.ClearSteamCookies(setLanguage);
if (sessionData == null)
{
TransferCommunityCookies(container);
return;
}
AddRefreshToken(container, sessionData.RefreshToken);
@@ -68,7 +73,7 @@ public static class AdmissionHelper
{
var uri = SteamDomains.GetDomainUri(domain);
foreach (var cookie in container.GetCookies(uri)
.Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
.Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
{
cookie.Expired = true;
}
@@ -79,11 +84,16 @@ public static class AdmissionHelper
/// <summary>
/// Clear and set new session
/// </summary>
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData mobileSession,
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData? mobileSession,
string setLanguage = "english")
{
container.ClearSteamCookies(setLanguage);
container.AddMinimalMobileCookies();
if (mobileSession == null)
{
TransferCommunityCookies(container);
return;
}
AddRefreshToken(container, mobileSession.RefreshToken);
@@ -106,12 +116,19 @@ public static class AdmissionHelper
/// Market, Trading and other pages won't be authorized
/// </summary>
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
IMobileSessionData mobileSession,
IMobileSessionData? mobileSession,
string setLanguage = "english")
{
container.ClearSteamCookies(setLanguage);
container.AddMinimalMobileCookies();
if (mobileSession == null)
{
TransferCommunityCookies(container);
return;
}
AddRefreshToken(container, mobileSession.RefreshToken);
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
@@ -131,7 +148,7 @@ public static class AdmissionHelper
}
var mobileToken = mobileSession.GetMobileToken();
if (domainCookieSet == false && mobileToken is {IsExpired: false})
if (!domainCookieSet && mobileToken is {IsExpired: false})
{
var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
@@ -193,7 +210,7 @@ public static class AdmissionHelper
foreach (Cookie cookie in cookies)
{
if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired ||
if (!cookie.Domain.Contains("steamcommunity.com") || cookie.Expired ||
cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue;
@@ -253,7 +270,7 @@ public static class AdmissionHelper
var cookies = container.GetAllCookies();
return cookies
.FirstOrDefault(c => c.Name.Equals(SESSION_ID_COOKIE_NAME, StringComparison.InvariantCultureIgnoreCase)
&& c.Expired == false
&& !c.Expired
&& c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))?
.Value;
}
@@ -44,7 +44,7 @@ public static class SteamTokenHelper
{
result = default;
var match = SteamTokenRegex.Match(input);
if (match.Success == false)
if (!match.Success)
{
if (trying) return false;
throw new ArgumentException("Provided Token is not valid SteamLoginSecure or SteamAccess token");
@@ -64,7 +64,7 @@ public static class SteamTokenHelper
}
var steamIdStr = jwt.Subject;
if (steamIdStr == null || long.TryParse(steamIdStr, out var steamId) == false || steamId < SteamId64.SEED)
if (steamIdStr == null || !long.TryParse(steamIdStr, out var steamId) || steamId < SteamId64.SEED)
{
if (trying) return false;
throw new ArgumentException(
@@ -101,7 +101,7 @@ public static class SteamTokenHelper
if (jwt.Audiences.ToList().Count > 0)
{
var aud = audiences[0];
if (AudDomains.TryGetValue(aud, out domain) == false && aud != "web")
if (!AudDomains.TryGetValue(aud, out domain) && aud != "web")
{
if (trying)
{
@@ -180,13 +180,13 @@ public static class SteamTokenHelper
internal static string CombineLoginValueIfNeeded(long steamId, string loginValue)
{
return CheckIfProperLoginValue(loginValue) == false ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
return !CheckIfProperLoginValue(loginValue) ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
}
public static string ExtractJwtToken(string steamLoginSecure)
{
var match = SteamTokenRegex.Match(steamLoginSecure);
if (match.Success == false) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
if (!match.Success) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
return match.Groups["jwt"].Value;
}
}
@@ -16,7 +16,7 @@ public class SessionInvalidException : Exception
{
}
public SessionInvalidException(string message, Exception? inner) : base(message, inner)
public SessionInvalidException(string? message, Exception? inner) : base(message, inner)
{
}
}
@@ -17,7 +17,7 @@ public class SessionPermanentlyExpiredException : SessionInvalidException
{
}
public SessionPermanentlyExpiredException(string message, Exception inner) : base(message, inner)
public SessionPermanentlyExpiredException(string? message, Exception? inner) : base(message, inner)
{
}
}
@@ -11,7 +11,7 @@ public static class ProtoHttpClientExtension
this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken)
where TProtoResponse : class, IProtoMsg
{
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
return await ProtoResponse<TProtoResponse>.FromHttpResponseAsync(resp, cancellationToken);
}
@@ -19,7 +19,7 @@ public static class ProtoHttpClientExtension
string? protoMsg,
CancellationToken cancellationToken)
{
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
resp.EnsureSuccessStatusCode();
return ProtoHelpers.GetEResult(resp);
}
@@ -97,7 +97,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
}
public static Task<ProtoResponse<TProtoResponse>> PostProtoMsg<TProtoResponse>(this HttpClient client, string uri,
@@ -105,7 +105,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str, cancellationToken);
}
#endregion
@@ -117,7 +117,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
return res.GetResponseEnsureSuccess();
}
@@ -126,7 +126,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str,
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str,
cancellationToken);
return res.GetResponseEnsureSuccess();
}
@@ -179,7 +179,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
}
public static Task<ProtoResponse<TProtoResponse>> GetProtoMsg<TProtoResponse>(this HttpClient client, string uri,
@@ -187,7 +187,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str, cancellationToken);
}
#endregion
@@ -199,7 +199,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
res.Result.EnsureSuccessEResult();
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
@@ -210,7 +210,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str,
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str,
cancellationToken);
res.Result.EnsureSuccessEResult();
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
@@ -74,18 +74,18 @@ public class SteamAuthenticatorLinker
long? phoneNumber = null;
if (hasPhone == false && Options.PhoneNumberProvider != null)
if (!hasPhone && Options.PhoneNumberProvider != null)
{
phoneNumber = await Options.PhoneNumberProvider.GetPhoneNumber(Consumer);
}
if (hasPhone == false && phoneNumber != null)
if (!hasPhone && phoneNumber != null)
{
Logger?.LogInformation("Attaching phone number {phoneNumber}", phoneNumber);
//if (await this.IsValidPhoneNumber(phoneNumber.Value) == false)
// throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidPhoneNumber);
if (await this.AttachPhone(phoneNumber.Value) == false)
if (!await this.AttachPhone(phoneNumber.Value))
throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantAttachPhone);
@@ -150,7 +150,7 @@ public class SteamAuthenticatorLinker
Logger?.LogInformation("Finalizing link");
var result = await this.FinalizeLink(code, resp.SharedSecret, isPhone);
if (result.Success == false)
if (!result.Success)
{
var error = result.Error switch
{
@@ -5,24 +5,21 @@ namespace SteamLib.Utility.MafileSerialization;
internal class LegacyMafile
{
[JsonProperty("shared_secret")]
[JsonRequired]
public string SharedSecret { get; set; } = default!;
public string SharedSecret { get; set; } = null!;
[JsonProperty("identity_secret")]
[JsonRequired]
public string IdentitySecret { get; set; } = default!;
public string IdentitySecret { get; set; } = null!;
[JsonProperty("device_id")]
[JsonRequired]
public string DeviceId { get; set; } = default!;
public string DeviceId { get; set; } = null!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = default!;
[JsonProperty("account_name")] public string AccountName { get; set; } = default!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
[JsonProperty("account_name")] public string AccountName { get; set; } = null!;
[JsonProperty("Session")] public object? SessionData { get; set; }
[JsonProperty("server_time")] public long ServerTime { get; set; } //Unused
[JsonProperty("steamid")] public long SteamId { get; set; }
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = default!; //Unused
[JsonProperty("uri")] public string Uri { get; set; } = default!; //Unused
[JsonProperty("token_gid")] public string TokenGid { get; set; } = default!; //Unused
[JsonProperty("secret_1")] public string Secret1 { get; set; } = default!; //Unused
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = null!; //Unused
[JsonProperty("uri")] public string Uri { get; set; } = null!; //Unused
[JsonProperty("token_gid")] public string TokenGid { get; set; } = null!; //Unused
[JsonProperty("secret_1")] public string Secret1 { get; set; } = null!; //Unused
}
@@ -38,7 +38,7 @@ public partial class MafileSerializer //Validate
public static void IsValidBase64(string name, string base64)
{
var buffer = new Span<byte>(new byte[base64.Length]);
if (Convert.TryFromBase64String(base64, buffer, out _) == false)
if (!Convert.TryFromBase64String(base64, buffer, out _))
throw new ArgumentException($"{name} is not valid base64 string");
}
@@ -39,7 +39,7 @@ public static class MobileConfirmationScrapper
throw new SessionInvalidException();
}
if (conf.Success == false)
if (!conf.Success)
{
var error = LoadConfirmationsError.Unknown;
if (conf.Message != null && ErrorMessages.TryGetValue(conf.Message, out var e))