Repository: Citrinate/FreePackages
Branch: main
Commit: 042a89c9e47a
Files: 113
Total size: 1.5 MB
Directory structure:
gitextract_ow9uday2/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── question.md
│ ├── RELEASE_TEMPLATE.md
│ └── workflows/
│ └── publish.yml
├── .gitignore
├── .gitmodules
├── Directory.Build.props
├── Directory.Packages.props
├── FreePackages/
│ ├── .gitignore
│ ├── Commands.cs
│ ├── Data/
│ │ ├── Cache/
│ │ │ ├── BotCache.cs
│ │ │ └── GlobalCache.cs
│ │ ├── External/
│ │ │ ├── ASFInfo.cs
│ │ │ └── CardApps.cs
│ │ └── PICS/
│ │ └── ProductInfo.cs
│ ├── FreePackages.cs
│ ├── FreePackages.csproj
│ ├── Handlers/
│ │ ├── PICSHandler.cs
│ │ ├── PackageHandler.cs
│ │ └── SteamHandler.cs
│ ├── Helpers/
│ │ ├── DeterministicHasher.cs
│ │ └── StatusReporter.cs
│ ├── IPC/
│ │ ├── Api/
│ │ │ └── FreePackagesController.cs
│ │ ├── Requests/
│ │ │ └── QueueLicensesRequest.cs
│ │ └── Responses/
│ │ └── FreeSubResponse.cs
│ ├── Json.cs
│ ├── Localization/
│ │ ├── README.md
│ │ ├── Strings.de-DE.resx
│ │ ├── Strings.resx
│ │ ├── Strings.ru-RU.resx
│ │ ├── Strings.tr-TR.resx
│ │ ├── Strings.uk-UA.resx
│ │ ├── Strings.zh-Hans.resx
│ │ └── Strings.zh-Hant.resx
│ ├── PackageFilter/
│ │ ├── FilterConfig.cs
│ │ ├── Filterables/
│ │ │ ├── FilterableApp.cs
│ │ │ └── FilterablePackage.cs
│ │ └── PackageFilter.cs
│ ├── PackageQueue/
│ │ ├── ActivationQueue.cs
│ │ ├── Package.cs
│ │ ├── PackageQueue.cs
│ │ └── RemovalQueue.cs
│ └── WebRequest.cs
├── FreePackages.Tests/
│ ├── Apps.cs
│ ├── Filters.cs
│ ├── FreePackages.Tests.csproj
│ ├── Packages.cs
│ ├── TestData/
│ │ ├── app_which_is_free.txt
│ │ ├── app_with_categories.txt
│ │ ├── app_with_content_descriptors.txt
│ │ ├── app_with_deck_playable.txt
│ │ ├── app_with_deck_unknown.txt
│ │ ├── app_with_deck_unsupported.txt
│ │ ├── app_with_deck_verified.txt
│ │ ├── app_with_dlc.txt
│ │ ├── app_with_language_support.txt
│ │ ├── app_with_purchase_restricted_countries.txt
│ │ ├── app_with_release_state.txt
│ │ ├── app_with_required_app.txt
│ │ ├── app_with_restricted_countries.txt
│ │ ├── app_with_review_score.txt
│ │ ├── app_with_state.txt
│ │ ├── app_with_tags.txt
│ │ ├── app_with_type.txt
│ │ ├── demo_which_will_be_removed.txt
│ │ ├── demo_with_fewer_categories_than_parent.txt
│ │ ├── demo_with_fewer_categories_than_parent_parent.txt
│ │ ├── demo_with_fewer_content_descriptors_than_parent.txt
│ │ ├── demo_with_fewer_content_descriptors_than_parent_parent.txt
│ │ ├── demo_with_fewer_languages_than_parent.txt
│ │ ├── demo_with_fewer_languages_than_parent_parent.txt
│ │ ├── demo_with_fewer_tags_than_parent.txt
│ │ ├── demo_with_fewer_tags_than_parent_parent.txt
│ │ ├── package_which_is_free.txt
│ │ ├── package_which_is_no_cost.txt
│ │ ├── package_with_deactivated_demo.txt
│ │ ├── package_with_demo_which_will_be_removed.txt
│ │ ├── package_with_disallowed_app.txt
│ │ ├── package_with_free_weekend.txt
│ │ ├── package_with_purchase_restricted_countries.txt
│ │ ├── package_with_restricted_countries.txt
│ │ ├── package_with_single_app.txt
│ │ ├── package_with_single_app_app_1.txt
│ │ ├── package_with_timed_activation.txt
│ │ ├── playtest_with_hidden_parent.txt
│ │ ├── playtest_with_hidden_parent_parent.txt
│ │ ├── playtest_with_no_categories.txt
│ │ ├── playtest_with_no_categories_parent.txt
│ │ ├── playtest_with_no_languages.txt
│ │ ├── playtest_with_no_languages_parent.txt
│ │ ├── playtest_with_no_waitlist.txt
│ │ ├── playtest_with_no_waitlist_parent.txt
│ │ ├── userdata_empty.json
│ │ ├── userdata_with_excluded_content_descriptors.json
│ │ ├── userdata_with_excluded_tags.json
│ │ ├── userdata_with_followed_apps.json
│ │ ├── userdata_with_ignored_apps.json
│ │ ├── userdata_with_wishlist_apps.json
│ │ └── userinfo_empty.json
│ └── generate_test_data.sh
├── FreePackages.sln
├── FreePackagesImporter/
│ ├── README.md
│ └── code.user.js
├── LICENSE
├── README.md
├── SECURITY.md
├── build.bat
├── build.sh
├── crowdin.yml
└── github-pandoc.css
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.cs]
csharp_prefer_braces = true:suggestion
csharp_new_line_before_open_brace = none:suggestion
csharp_new_line_before_else = false:suggestion
csharp_new_line_before_catch = false:suggestion
csharp_new_line_before_finally = false:suggestion
csharp_new_line_before_members_in_object_initializers = false:suggestion
csharp_new_line_before_members_in_anonymous_types = false:suggestion
csharp_new_line_between_query_expression_clauses = false:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
csharp_style_pattern_matching_over_as_with_null_check = false:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
csharp_preserve_single_line_statements = false:suggestion
csharp_preserve_single_line_blocks = true:suggestion
csharp_indent_case_contents = true:suggestion
csharp_indent_switch_labels = true:suggestion
csharp_space_after_cast = true:suggestion
csharp_space_after_keywords_in_control_flow_statements = true:suggestion
csharp_space_between_method_declaration_parameter_list_parentheses = false:suggestion
csharp_space_between_method_call_parameter_list_parentheses = false:suggestion
csharp_space_between_parentheses = false:suggestion
csharp_space_before_colon_in_inheritance_clause = true:suggestion
csharp_space_after_colon_in_inheritance_clause = true:suggestion
csharp_space_around_binary_operators = before_and_after:suggestion
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:suggestion
csharp_space_between_method_call_name_and_opening_parenthesis = false:suggestion
csharp_space_between_method_call_empty_parameter_list_parentheses = false:suggestion
dotnet_sort_system_directives_first = true:suggestion
dotnet_style_require_accessibility_modifiers = always:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# PascalCase for naming everything but parameters
dotnet_naming_rule.all_members_must_be_capitalized.symbols = all_symbols
dotnet_naming_symbols.all_symbols.applicable_kinds = class,struct,interface,enum,property,method,field,event,delegate
dotnet_naming_symbols.all_symbols.applicable_accessibilities = *
dotnet_naming_rule.all_members_must_be_capitalized.style = all_symbols
dotnet_naming_style.all_symbols.capitalization = pascal_case
dotnet_naming_rule.all_members_must_be_capitalized.severity = suggestion
# camelCase for naming parameters
dotnet_naming_rule.parameters_must_be_camel_case.symbols = params
dotnet_naming_symbols.params.applicable_kinds = parameter
dotnet_naming_symbols.params.applicable_accessibilities = *
dotnet_naming_rule.parameters_must_be_camel_case.style = params
dotnet_naming_style.params.capitalization = camel_case
dotnet_naming_rule.parameters_must_be_camel_case.severity = suggestion
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
*.sh text eol=lf
# Custom for Visual Studio
*.cs diff=csharp
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Plugin Version**
What version of the plugin are you using.
**ArchiSteamFarm Version and Variant**
What version of ArchiSteamFarm are you using and do you use ASF-generic or some other variant.
**Logs**
Provide the logs from when the bug occurred. You can find these in the `log.txt` file directly in the ASF directory.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question about the project
title: ''
labels: question
assignees: ''
---
================================================
FILE: .github/RELEASE_TEMPLATE.md
================================================
This version requires ArchiSteamFarm VX.X.X.X or newer
### Changelog
-
================================================
FILE: .github/workflows/publish.yml
================================================
name: publish
on: [push, pull_request]
env:
PLUGIN_NAME: "FreePackages"
DOTNET_SDK_VERSION: 10.0
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
with:
submodules: recursive
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.0.0
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- name: Verify .NET Core
run: dotnet --info
- name: Prepare for publishing
run: dotnet restore
- name: Run FreePackages.Tests
run: dotnet test FreePackages.Tests -p:ContinuousIntegrationBuild=true --nologo
- name: Publish
run: |
dotnet publish ${{ env.PLUGIN_NAME }} -c "Release" -o "out/generic" -p:ContinuousIntegrationBuild=true --nologo
mkdir -p ./out/dist/${{ env.PLUGIN_NAME }}
cp ./out/generic/${{ env.PLUGIN_NAME }}.dll ./out/dist/${{ env.PLUGIN_NAME }}
( cd ./out/generic/ ; cp --parents ./*/${{ env.PLUGIN_NAME }}.resources.dll ../dist/${{ env.PLUGIN_NAME }} || : )
- name: Create README
uses: docker://pandoc/core:3.1
with:
args: --metadata title="${{ env.PLUGIN_NAME }}" --standalone --columns 2000 -f markdown -t html --embed-resources --standalone -c ./github-pandoc.css -o ./out/dist/${{ env.PLUGIN_NAME }}/README.html README.md
- name: Upload ${{ env.PLUGIN_NAME }}
uses: actions/upload-artifact@v4.0.0
with:
name: ${{ env.PLUGIN_NAME }}
path: out/dist/${{ env.PLUGIN_NAME }}
release:
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write
attestations: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
- name: Download ${{ env.PLUGIN_NAME }} artifact
uses: actions/download-artifact@v4.1.7
with:
name: ${{ env.PLUGIN_NAME }}
path: out
- name: Create Zip
run: |
cd out
7z a -tzip -mx7 ${{ env.PLUGIN_NAME }}.zip *
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-path: 'out/${{ env.PLUGIN_NAME }}.zip'
- name: Create ${{ env.PLUGIN_NAME }} GitHub release
uses: ncipollo/release-action@v1.13.0
with:
artifacts: out/${{ env.PLUGIN_NAME }}.zip
artifactContentType: application/zip
name: ${{ env.PLUGIN_NAME }} V${{ github.ref_name }}
tag: ${{ github.ref_name }}
bodyFile: .github/RELEASE_TEMPLATE.md
token: ${{ secrets.GITHUB_TOKEN }}
makeLatest: false
prerelease: true
draft: true
================================================
FILE: .gitignore
================================================
# _ ____ _____
# / \ / ___| | ___|
# / _ \ \___ \ | |_
# / ___ \ ___) || _|
# /_/ \_\|____/ |_|
# Ignore all files in custom in-tree config directory (if exists)
ArchiSteamFarm/config
# Ignore local log + debug of development builds
ArchiSteamFarm/log.txt
ArchiSteamFarm/debug
# Ignore standard out folders for publishing
**/out
# Ignore crowdin CLI secret (if exists)
tools/ArchiCrowdin/crowdin_identity.yml
# _ _
# | | (_) _ __ _ _ __ __
# | | | || '_ \ | | | |\ \/ /
# | |___ | || | | || |_| | > <
# |_____||_||_| |_| \__,_|/_/\_\
#
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# ___ ____
# _ __ ___ __ _ ___ / _ \ / ___|
# | '_ ` _ \ / _` | / __|| | | |\___ \
# | | | | | || (_| || (__ | |_| | ___) |
# |_| |_| |_| \__,_| \___| \___/ |____/
#
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# __ __ ____ _
# | \/ | ___ _ __ ___ | _ \ ___ __ __ ___ | | ___ _ __
# | |\/| | / _ \ | '_ \ / _ \ | | | | / _ \\ \ / // _ \| | / _ \ | '_ \
# | | | || (_) || | | || (_) || |_| || __/ \ V /| __/| || (_) || |_) |
# |_| |_| \___/ |_| |_| \___/ |____/ \___| \_/ \___||_| \___/ | .__/
# |_|
#
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore
#User Specific
*.userprefs
*.usertasks
#Mono Project Files
*.pidb
*.resources
test-results/
# __ __ _ _ ____ _ _ _
# \ \ / /(_) ___ _ _ __ _ | |/ ___| | |_ _ _ __| |(_) ___
# \ \ / / | |/ __|| | | | / _` || |\___ \ | __|| | | | / _` || | / _ \
# \ V / | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) |
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/
#
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
.vscode/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
**/wwwroot/lib/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# __ __ _ _ ____ _ _ _ ____ _
# \ \ / /(_) ___ _ _ __ _ | |/ ___| | |_ _ _ __| |(_) ___ / ___| ___ __| | ___
# \ \ / / | |/ __|| | | | / _` || |\___ \ | __|| | | | / _` || | / _ \ | | / _ \ / _` | / _ \
# \ V / | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) || |___| (_) || (_| || __/
# \_/ |_||___/ \__,_| \__,_||_||____/ \__| \__,_| \__,_||_| \___/ \____|\___/ \__,_| \___|
#
# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# __ __ _ _
# \ \ / /(_) _ __ __| | ___ __ __ ___
# \ \ /\ / / | || '_ \ / _` | / _ \\ \ /\ / // __|
# \ V V / | || | | || (_| || (_) |\ V V / \__ \
# \_/\_/ |_||_| |_| \__,_| \___/ \_/\_/ |___/
#
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# https://www.paraesthesia.com/archive/2022/09/30/strongly-typed-resources-with-net-core/
*.Designer.cs
================================================
FILE: .gitmodules
================================================
[submodule "ArchiSteamFarm"]
path = ArchiSteamFarm
url = https://github.com/JustArchiNET/ArchiSteamFarm.git
================================================
FILE: Directory.Build.props
================================================
FreePackages
1.6.3.3
Citrinate
$(Authors)
Copyright © $([System.DateTime]::UtcNow.Year) $(Company)
$(PluginName) description.
Apache-2.0
https://github.com/$(Company)/$(PluginName)
$(PackageProjectUrl)/releases
$(PackageProjectUrl).git
false
false
false
../resources/$(PluginName).snk.pub
true
true
../resources/$(PluginName).snk
false
true
================================================
FILE: Directory.Packages.props
================================================
================================================
FILE: FreePackages/.gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
**/out
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
*.zip
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
================================================
FILE: FreePackages/Commands.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
namespace FreePackages {
internal static class Commands {
internal static async Task Response(Bot bot, EAccess access, ulong steamID, string message, string[] args) {
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if (string.IsNullOrEmpty(message)) {
return null;
}
switch (args.Length) {
case 1:
switch (args[0].ToUpperInvariant()) {
case "FREEPACKAGES" when access >= EAccess.Master:
return String.Format("{0} {1}", nameof(FreePackages), (typeof(FreePackages).Assembly.GetName().Version ?? new Version("0")).ToString());
case "CANCELREMOVE" or "CANCELREMOVAL":
return ResponseCancelRemove(bot, access);
case "CONFIRMREMOVE" or "CONFIRMREMOVAL":
return ResponseConfirmRemove(bot, access);
case "CLEARQUEUE" or "CLEARFREEPACKAGESQUEUE":
return ResponseClearQueue(bot, access);
case "QSA":
return ResponseQueueStatus(access, steamID, "ASF");
case "QSTATUS" or "QUEUESTATUS":
return ResponseQueueStatus(bot, access);
case "REMOVEFREEPACKAGES":
return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID)).ConfigureAwait(false);
case "REMOVEFREEPACKAGES^":
return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), excludePlayed: true).ConfigureAwait(false);
case "REMOVEALLFREEPACKAGES":
return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), removeAll: true).ConfigureAwait(false);
case "REMOVEALLFREEPACKAGES^":
return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), removeAll: true, excludePlayed: true).ConfigureAwait(false);
default:
return null;
};
default:
switch (args[0].ToUpperInvariant()) {
case "CANCELREMOVE" or "CANCELREMOVAL":
return ResponseCancelRemove(access, steamID, args[1]);
case "CONFIRMREMOVE" or "CONFIRMREMOVAL":
return ResponseConfirmRemove(access, steamID, args[1]);
case "CLEARQUEUE" or "CLEARFREEPACKAGESQUEUE":
return ResponseClearQueue(access, steamID, args[1]);
case "DONTREMOVE" when args.Length > 2:
return ResponseDontRemove(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","));
case "DONTREMOVE":
return ResponseDontRemove(bot, access, args[1]);
case "QSTATUS" or "QUEUESTATUS":
return ResponseQueueStatus(access, steamID, args[1]);
case "QLICENSE" or "QUEUELICENSE" or "QLICENCE" or "QUEUELICENCE" when args.Length > 2:
return ResponseQueueLicense(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","));
case "QLICENSE" or "QUEUELICENSE" or "QLICENCE" or "QUEUELICENCE" :
return ResponseQueueLicense(bot, access, args[1]);
case "QLICENSE^" or "QUEUELICENSE^" or "QLICENCE^" or "QUEUELICENCE^" when args.Length > 2:
return ResponseQueueLicense(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","), useFilter: true);
case "QLICENSE^" or "QUEUELICENSE^" or "QLICENCE^" or "QUEUELICENCE^" :
return ResponseQueueLicense(bot, access, args[1], useFilter: true);
case "REMOVEFREEPACKAGES":
return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID)).ConfigureAwait(false);
case "REMOVEFREEPACKAGES^":
return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), excludePlayed: true).ConfigureAwait(false);
case "REMOVEALLFREEPACKAGES":
return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), removeAll: true).ConfigureAwait(false);
case "REMOVEALLFREEPACKAGES^":
return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), removeAll: true, excludePlayed: true).ConfigureAwait(false);
default:
return null;
}
}
}
private static string? ResponseCancelRemove(Bot bot, EAccess access) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].CancelRemoval());
}
private static string? ResponseCancelRemove(EAccess access, ulong steamID, string botNames) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseCancelRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static string? ResponseConfirmRemove(Bot bot, EAccess access) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ConfirmRemoval());
}
private static string? ResponseConfirmRemove(EAccess access, ulong steamID, string botNames) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseConfirmRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static string? ResponseClearQueue(Bot bot, EAccess access) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ClearQueue());
}
private static string? ResponseClearQueue(EAccess access, ulong steamID, string botNames) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseClearQueue(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static string? ResponseDontRemove(Bot bot, EAccess access, string licenses) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
// https://github.com/JustArchiNET/ArchiSteamFarm/blob/d972c93072dd8d2bf0f2cecda3561dc3ba77a9ed/ArchiSteamFarm/Steam/Interaction/Commands.cs#L626C3-L626C34
StringBuilder response = new();
string[] entries = licenses.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string entry in entries) {
uint gameID;
string type;
int index = entry.IndexOf('/', StringComparison.Ordinal);
if ((index > 0) && (entry.Length > index + 1)) {
if (!uint.TryParse(entry[(index + 1)..], out gameID) || (gameID == 0)) {
response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));
continue;
}
type = entry[..index];
} else if (uint.TryParse(entry, out gameID) && (gameID > 0)) {
type = "SUB";
} else {
response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));
continue;
}
EPackageType packageType;
type = type.ToUpperInvariant();
if (type == "A" || type == "APP") {
packageType = EPackageType.RemoveApp;
} else {
packageType = EPackageType.RemoveSub;
}
response.AppendLine(FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ModifyRemovables(packageType, gameID)));
}
return response.Length > 0 ? response.ToString() : null;
}
private static string? ResponseDontRemove(EAccess access, ulong steamID, string botNames, string licenses) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseDontRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), licenses));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static string? ResponseQueueStatus(Bot bot, EAccess access) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].GetStatus());
}
private static string? ResponseQueueStatus(EAccess access, ulong steamID, string botNames) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseQueueStatus(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static string? ResponseQueueLicense(Bot bot, EAccess access, string licenses, bool useFilter = false, [CallerMemberName] string? previousMethodName = null) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
// https://github.com/JustArchiNET/ArchiSteamFarm/blob/d972c93072dd8d2bf0f2cecda3561dc3ba77a9ed/ArchiSteamFarm/Steam/Interaction/Commands.cs#L626C3-L626C34
StringBuilder response = new();
string[] entries = licenses.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string entry in entries) {
uint gameID;
string type;
int index = entry.IndexOf('/', StringComparison.Ordinal);
if ((index > 0) && (entry.Length > index + 1)) {
if (!uint.TryParse(entry[(index + 1)..], out gameID) || (gameID == 0)) {
response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));
continue;
}
type = entry[..index];
} else if (uint.TryParse(entry, out gameID) && (gameID > 0)) {
type = "SUB";
} else {
response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));
continue;
}
EPackageType packageType;
type = type.ToUpperInvariant();
if (type == "A" || type == "APP") {
packageType = EPackageType.App;
} else {
packageType = EPackageType.Sub;
}
response.AppendLine(FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].AddPackage(packageType, gameID, useFilter)));
}
if (previousMethodName == nameof(Response)) {
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
}
return response.Length > 0 ? response.ToString() : null;
}
private static string? ResponseQueueLicense(EAccess access, ulong steamID, string botNames, string licenses, bool useFilter = false) {
if (String.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
IEnumerable results = bots.Select(bot => ResponseQueueLicense(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), licenses, useFilter));
List responses = new(results.Where(result => !String.IsNullOrEmpty(result)));
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
}
private static async Task ResponseRemoveFreePackages(Bot bot, EAccess access, StatusReporter statusReporter, bool excludePlayed = false, bool removeAll = false) {
if (access < EAccess.Master) {
return null;
}
if (!bot.IsConnectedAndLoggedOn) {
return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
}
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
return FormatBotResponse(bot, Strings.PluginNotEnabled);
}
IDocument? accountLicensesPage = await WebRequest.GetAccountLicenses(bot);
if (accountLicensesPage == null) {
return FormatBotResponse(bot, Strings.LicensePageFetchFail);
}
Regex removablePackageIDsRegex = new Regex("RemoveFreeLicense\\(\\s*(?[0-9]+),\\s*'(?[A-Za-z0-9+/=]*)'", RegexOptions.CultureInvariant); // matches the parameters of: RemoveFreeLicense( 45946, 'UmV2ZXJzaW9uOiBUaGUgRXNjYXBl' );
MatchCollection removablePackageMatches = removablePackageIDsRegex.Matches(accountLicensesPage.Source.Text);
if (removablePackageMatches.Count == 0) {
return FormatBotResponse(bot, Strings.LicensePageEmpty);
}
Dictionary removeablePackages = new();
foreach (Match match in removablePackageMatches) {
string name;
try {
name = Encoding.UTF8.GetString(Convert.FromBase64String(match.Groups["encodedName"].Value));
} catch (Exception e) {
bot.ArchiLogger.LogGenericException(e);
return FormatBotResponse(bot, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, "encodedName"));
}
string subIDString = match.Groups["subID"].Value;
if (!uint.TryParse(subIDString, out uint subID)) {
return FormatBotResponse(bot, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, "subID"));
}
removeablePackages[subID] = name;
}
Utilities.InBackground(
async() => {
await PackageHandler.Handlers[bot.BotName].ScanRemovables(removeablePackages, excludePlayed, removeAll, statusReporter).ConfigureAwait(false);
}
);
int removableScanTimeEstimateMinutes = (int) Math.Round(2.5 * ((double) removeablePackages.Count / ProductInfo.ItemsPerProductInfoRequest) * ((double) ProductInfo.ProductInfoLimitingDelaySeconds / 60));
return FormatBotResponse(bot, String.Format(Strings.RemovalWaitMessage, removableScanTimeEstimateMinutes, String.Format("!cancelremove {0}", bot.BotName)));
}
private static async Task ResponseRemoveFreePackages(EAccess access, ulong steamID, string botName, StatusReporter statusReporter, bool excludePlayed = false, bool removeAll = false) {
if (String.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)) : null;
}
return await ResponseRemoveFreePackages(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), statusReporter, excludePlayed, removeAll).ConfigureAwait(false);
}
internal static string FormatStaticResponse(string response) => ArchiSteamFarm.Steam.Interaction.Commands.FormatStaticResponse(response);
internal static string FormatBotResponse(Bot bot, string response) => bot.Commands.FormatBotResponse(response);
}
}
================================================
FILE: FreePackages/Data/Cache/BotCache.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
using SteamKit2;
namespace FreePackages {
internal sealed class BotCache : SerializableFile {
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet Packages { get; private set; } = new(new PackageComparer());
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet Activations { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet ChangedApps { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet ChangedPackages { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet NewOwnedPackages { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet SeenPackages { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet WaitlistedPlaytests { get; private set; } = new();
[JsonInclude]
[JsonDisallowNull]
internal ConcurrentHashSet IgnoredApps { get; private set; } = new();
private HashSet SeenPackageIDActivations = new();
private readonly object LockObject = new();
[JsonConstructor]
internal BotCache() { }
internal BotCache(string filePath) : this() {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
FilePath = filePath;
}
protected override Task Save() => Save(this);
internal static async Task CreateOrLoad(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
if (!File.Exists(filePath)) {
return new BotCache(filePath);
}
BotCache? botCache;
try {
string json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogGenericError(string.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));
return null;
}
botCache = json.ToJsonObject();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return null;
}
if (botCache == null) {
ASF.ArchiLogger.LogNullError(botCache);
return null;
}
botCache.Packages = new(botCache.Packages.GroupBy(package => package, new PackageComparer()).Select(group => group.First()), new PackageComparer());
botCache.FilePath = filePath;
return botCache;
}
internal bool AddPackage(Package package) {
if (Packages.Contains(package)) {
return false;
}
Packages.Add(package);
Utilities.InBackground(Save);
return true;
}
internal bool AddPackages(IEnumerable packages) {
if (!packages.Except(Packages).Any()) {
// There are no new packages to add
return false;
}
Packages.UnionWith(packages);
Utilities.InBackground(Save);
return true;
}
internal bool RemovePackage(Package package) {
Packages.Remove(package);
Utilities.InBackground(Save);
return true;
}
internal bool RemoveAppPackages(HashSet appIDsToRemove) {
Packages.Where(x => x.Type == EPackageType.App && appIDsToRemove.Contains(x.ID)).ToList().ForEach(x => Packages.Remove(x));
Utilities.InBackground(Save);
return true;
}
internal Package? GetNextPackage(HashSet types) {
// Return the package which should be activated first, prioritizing first packages which have a start and end date
ulong now = DateUtils.DateTimeToUnixTime(DateTime.UtcNow);
Package? package = Packages.FirstOrDefault(x => x.StartTime != null && now > x.StartTime && types.Contains(x.Type));
if (package != null) {
return package;
}
return Packages.FirstOrDefault(x => x.StartTime == null && types.Contains(x.Type));
}
internal void AddActivation(DateTime activation, uint count = 1, IReadOnlyCollection? packageIDs = null) {
var activationsToPrune = Activations.Where(x => x < DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)).ToList();
if (activationsToPrune.Count > 0) {
activationsToPrune.ForEach(x => Activations.Remove(x));
}
lock(LockObject) {
int numUnseenPackageActivations = packageIDs?.Where(packageID => !SeenPackageIDActivations.Contains(packageID)).Count() ?? 0;
if (packageIDs == null || numUnseenPackageActivations > 0) {
if (packageIDs != null) {
SeenPackageIDActivations.UnionWith(packageIDs);
}
for (int i = 0; i < Math.Max(count, numUnseenPackageActivations); i++) {
Activations.Add(activation.AddSeconds(-1 * i));
}
}
}
Utilities.InBackground(Save);
}
internal int NumActivationsPastPeriod() {
return Activations.Where(activation => activation > DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)).Count();
}
internal DateTime? GetLastActivation() {
// Can't use Activations.Max() because it's missing on non-generic ASF
DateTime? lastActivation = null;
foreach (DateTime activation in Activations) {
if (lastActivation == null || activation > lastActivation) {
lastActivation = activation;
}
}
return lastActivation;
}
internal void AddChanges(HashSet? appIDs = null, HashSet? packageIDs = null, HashSet? newOwnedPackageIDs = null, bool ignoreFailedApps = false) {
if (appIDs != null) {
ChangedApps.UnionWith(appIDs);
if (ignoreFailedApps) {
ChangedApps.ExceptWith(IgnoredApps);
}
}
if (packageIDs != null) {
ChangedPackages.UnionWith(packageIDs);
}
if (newOwnedPackageIDs != null) {
NewOwnedPackages.UnionWith(newOwnedPackageIDs);
}
Utilities.InBackground(Save);
}
internal void RemoveChange(uint? appID = null, uint? packageID = null, uint? newOwnedPackageID = null) {
if (appID != null) {
ChangedApps.Remove(appID.Value);
}
if (packageID != null) {
ChangedPackages.Remove(packageID.Value);
}
if (newOwnedPackageID != null) {
NewOwnedPackages.Remove(newOwnedPackageID.Value);
}
}
internal void SaveChanges() {
Utilities.InBackground(Save);
}
internal void ClearQueue() {
Packages.RemoveWhere(package => ActivationQueue.ActivationTypes.Contains(package.Type));
ChangedApps.Clear();
ChangedPackages.Clear();
Utilities.InBackground(Save);
}
internal void CancelRemoval() {
Packages.RemoveWhere(package => RemovalQueue.RemovalTypes.Contains(package.Type));
Utilities.InBackground(Save);
}
internal void AddWaitlistedPlaytest(uint appID) {
WaitlistedPlaytests.Add(appID);
Utilities.InBackground(Save);
}
internal void UpdateSeenPackages(List newLicenses) {
SeenPackages.UnionWith(newLicenses.Select(license => license.PackageID));
// Keep track of how many free licenses we activated to enforce the free packages limit
// This is to catch packages that were activated, but didn't return a success status, or were activated outside of the plugin
/* NOTE: The below code will not capture all recent activations. If Steam removes a demo from your account, but you add it back,
then the package will re-appear with the original TimeCreated value. Activations like these are instead logged when steam reports a
successful activation.
*/
// TODO: Do other PaymentMethod values also count against the free package limit?
foreach(SteamApps.LicenseListCallback.License license in newLicenses) {
if (license.PaymentMethod == EPaymentMethod.Complimentary &&
license.TimeCreated.ToLocalTime() > DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)
) {
AddActivation(license.TimeCreated.ToLocalTime(), packageIDs: [ license.PackageID ]);
}
}
Utilities.InBackground(Save);
}
internal void IgnoreApp(uint appID) {
IgnoredApps.Add(appID);
Utilities.InBackground(Save);
}
}
}
================================================
FILE: FreePackages/Data/Cache/GlobalCache.cs
================================================
using System;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
namespace FreePackages {
internal sealed class GlobalCache : SerializableFile {
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(FreePackages)}.cache");
[JsonInclude]
internal uint LastChangeNumber { get; private set; }
[JsonInclude]
internal uint LastASFInfoItemCount { get; private set; }
public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
public bool ShouldSerializeLastASFInfoItemCount() => LastASFInfoItemCount > 0;
[JsonConstructor]
internal GlobalCache() {
FilePath = SharedFilePath;
}
protected override Task Save() => Save(this);
internal static async Task CreateOrLoad() {
if (!File.Exists(SharedFilePath)) {
return new GlobalCache();
}
GlobalCache? globalCache;
try {
string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogGenericError(string.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));
return new GlobalCache();
}
globalCache = json.ToJsonObject();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return new GlobalCache();
}
if (globalCache == null) {
ASF.ArchiLogger.LogNullError(globalCache);
return new GlobalCache();
}
return globalCache;
}
internal void UpdateChangeNumber(uint currentChangeNumber) {
LastChangeNumber = currentChangeNumber;
Utilities.InBackground(Save);
}
internal void UpdateASFInfoItemCount(uint currentASFInfoItemCount) {
LastASFInfoItemCount = currentASFInfoItemCount;
Utilities.InBackground(Save);
}
}
}
================================================
FILE: FreePackages/Data/External/ASFInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Web.Responses;
using FreePackages.Localization;
// There are limitations to using PICS for discovery such that an account that's online 24/7 can still miss certain free games
// To fill in some of these gaps, we periodically check the free apps/subs list provided by https://github.com/C4illin/ASFinfo
// For more information see here: https://github.com/Citrinate/FreePackages/commit/7541807f10e8dde53b1352a2c103b867e5446fa1#commitcomment-137669223
namespace FreePackages {
internal static class ASFInfo {
private static Uri Source = new("https://gist.githubusercontent.com/C4illin/e8c5cf365d816f2640242bf01d8d3675/raw/Steam%2520Codes");
private static readonly Regex SourceLine = new Regex("(?[as])/(?[0-9]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); // Match examples: a/12345 or s/12345
private static TimeSpan UpdateFrequency = TimeSpan.FromHours(1);
private static Timer UpdateTimer = new(async e => await DoUpdate().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
internal static void Update() {
UpdateTimer.Change(TimeSpan.FromMinutes(15), UpdateFrequency);
}
private static async Task DoUpdate() {
ArgumentNullException.ThrowIfNull(ASF.WebBrowser);
ArgumentNullException.ThrowIfNull(FreePackages.GlobalCache);
StreamResponse? response = await ASF.WebBrowser.UrlGetToStream(Source).ConfigureAwait(false);
if (response == null) {
ASF.ArchiLogger.LogNullError(response);
return;
}
if (response.Content == null) {
ASF.ArchiLogger.LogNullError(response.Content);
return;
}
HashSet appIDs = new();
HashSet packageIDs = new();
uint itemCount = 0;
try {
using (StreamReader sr = new StreamReader(response.Content)) {
while (sr.Peek() >= 0) {
itemCount++;
string? line = sr.ReadLine();
if (line == null) {
ASF.ArchiLogger.LogNullError(line);
return;
}
if (itemCount <= FreePackages.GlobalCache.LastASFInfoItemCount) {
continue;
}
Match item = SourceLine.Match(line);
if (!item.Success) {
ASF.ArchiLogger.LogGenericError(String.Format("{0}: {1}", Strings.ASFInfoParseFailed, line));
return;
}
if (!uint.TryParse(item.Groups["id"].Value, out uint id)) {
ASF.ArchiLogger.LogGenericError(String.Format("{0}: {1}", Strings.ASFInfoParseFailed, line));
return;
}
if (item.Groups["type"].Value == "a") {
// App
appIDs.Add(id);
} else if (item.Groups["type"].Value == "s") {
// Sub
packageIDs.Add(id);
}
}
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return;
}
if (appIDs.Count == 0 && packageIDs.Count == 0) {
return;
}
PackageHandler.Handlers.Values.ToList().ForEach(x => x.BotCache.AddChanges(appIDs, packageIDs));
FreePackages.GlobalCache.UpdateASFInfoItemCount(itemCount);
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
}
}
}
================================================
FILE: FreePackages/Data/External/CardApps.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Web.Responses;
using FreePackages.Localization;
namespace FreePackages {
internal static class CardApps {
internal static HashSet AppIDs = new();
private static Timer UpdateTimer = new(async e => await DoUpdate().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
private static TimeSpan UpdateFrequency = TimeSpan.FromHours(1);
internal static void Update() {
UpdateTimer.Change(TimeSpan.Zero, UpdateFrequency);
}
private static async Task DoUpdate() {
ArgumentNullException.ThrowIfNull(ASF.WebBrowser);
Uri request = new("https://raw.githubusercontent.com/nolddor/steam-badges-db/main/data/badges.min.json");
ObjectResponse? response = await ASF.WebBrowser.UrlGetToJsonObject(request).ConfigureAwait(false);
if (response == null) {
ASF.ArchiLogger.LogGenericDebug(Strings.BadgeDataFetchFailed);
UpdateTimer.Change(TimeSpan.FromMinutes(1), UpdateFrequency);
return;
}
try {
ArgumentNullException.ThrowIfNull(response.Content);
AppIDs = response.Content.Data.Keys.Select(uint.Parse).ToHashSet();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
ASF.ArchiLogger.LogGenericError(Strings.BadgeDataParsingFailed);
return;
}
}
private sealed class Badges {
[JsonExtensionData]
[JsonInclude]
internal Dictionary Data { get; private init; } = new();
}
}
}
================================================
FILE: FreePackages/Data/PICS/ProductInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;
namespace FreePackages {
internal static class ProductInfo {
private static SemaphoreSlim ProductInfoSemaphore = new SemaphoreSlim(1, 1);
internal const int ProductInfoLimitingDelaySeconds = 10;
internal const int ItemsPerProductInfoRequest = 255;
internal async static Task?> GetProductInfo(HashSet? appIDs = null, HashSet? packageIDs = null, CancellationToken? cancellationToken = null) {
List productInfo = new();
foreach ((HashSet? batchedAppIDs, HashSet? batchedPackageIDs) in GetProductIDBatches(appIDs, packageIDs)) {
cancellationToken?.ThrowIfCancellationRequested();
List? partialProductInfo = await FetchProductInfo(batchedAppIDs, batchedPackageIDs).ConfigureAwait(false);
if (partialProductInfo == null) {
return null;
}
productInfo = productInfo.Concat(partialProductInfo).ToList();
}
return productInfo;
}
internal static IEnumerable<(HashSet?, HashSet?)> GetProductIDBatches(HashSet? appIDs = null, HashSet? packageIDs = null) {
if ((appIDs?.Count ?? 0) + (packageIDs?.Count ?? 0) <= ItemsPerProductInfoRequest) {
yield return (appIDs, packageIDs);
} else {
if (appIDs != null) {
for (int i = 0; i < Math.Ceiling((decimal) appIDs.Count / ItemsPerProductInfoRequest); i++) {
HashSet batchedAppIDs = appIDs.Skip(i * ItemsPerProductInfoRequest).Take(ItemsPerProductInfoRequest).ToHashSet();
yield return (batchedAppIDs, null);
}
}
if (packageIDs != null) {
for (int i = 0; i < Math.Ceiling((decimal) packageIDs.Count / ItemsPerProductInfoRequest); i++) {
HashSet batchedPackageIDs = packageIDs.Skip(i * ItemsPerProductInfoRequest).Take(ItemsPerProductInfoRequest).ToHashSet();
yield return (null, batchedPackageIDs);
}
}
}
}
private async static Task?> FetchProductInfo(IEnumerable? appIDs = null, IEnumerable? packageIDs = null) {
await ProductInfoSemaphore.WaitAsync().ConfigureAwait(false);
try {
Bot? bot = Bot.BotsReadOnly?.Values.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
if (bot == null) {
return null;
}
var apps = appIDs == null ? Enumerable.Empty() : appIDs.Select(x => new SteamApps.PICSRequest(x));
var packages = packageIDs == null ? Enumerable.Empty() : packageIDs.Select(x => new SteamApps.PICSRequest(x, ASF.GlobalDatabase?.PackageAccessTokensReadOnly.GetValueOrDefault(x, (ulong) 0) ?? 0));
var response = await bot.SteamApps.PICSGetProductInfo(apps, packages).ToLongRunningTask().ConfigureAwait(false);
return response.Results?.ToList();
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);
return null;
} finally {
Utilities.InBackground(
async() => {
await Task.Delay(TimeSpan.FromSeconds(ProductInfoLimitingDelaySeconds)).ConfigureAwait(false);
ProductInfoSemaphore.Release();
}
);
}
}
}
}
================================================
FILE: FreePackages/FreePackages.cs
================================================
using System;
using System.Collections.Generic;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Plugins.Interfaces;
using SteamKit2;
using System.Text.Json;
using ArchiSteamFarm.Helpers.Json;
namespace FreePackages {
[Export(typeof(IPlugin))]
public sealed class FreePackages : IASF, IBotModules, ISteamPICSChanges, IBotSteamClient, IBotConnection, IBotCommand2, IGitHubPluginUpdates {
public string Name => nameof(FreePackages);
public string RepositoryName => "Citrinate/FreePackages";
public Version Version => typeof(FreePackages).Assembly.GetName().Version ?? new Version("0");
internal static GlobalCache? GlobalCache;
public Task OnLoaded() {
ASF.ArchiLogger.LogGenericInfo("Free Packages ASF Plugin by Citrinate");
return Task.CompletedTask;
}
public async Task OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
return await Commands.Response(bot, access, steamID, message, args).ConfigureAwait(false);
}
public async Task OnASFInit(IReadOnlyDictionary? additionalConfigProperties = null) {
if (GlobalCache == null) {
GlobalCache = await GlobalCache.CreateOrLoad().ConfigureAwait(false);
}
CardApps.Update();
ASFInfo.Update();
}
public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary? additionalConfigProperties = null) {
if (additionalConfigProperties == null) {
return;
}
bool isEnabled = false;
uint? packageLimit = null;
bool pauseWhilePlaying = false;
List filterConfigs = new();
foreach (KeyValuePair configProperty in additionalConfigProperties) {
switch (configProperty.Key) {
case "EnableFreePackages" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): {
isEnabled = configProperty.Value.GetBoolean();
bot.ArchiLogger.LogGenericInfo("Enable Free Packages : " + isEnabled.ToString());
break;
}
case "PauseFreePackagesWhilePlaying" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): {
pauseWhilePlaying = configProperty.Value.GetBoolean();
bot.ArchiLogger.LogGenericInfo("Pause Free Packages While Playing : " + pauseWhilePlaying.ToString());
break;
}
case "FreePackagesPerHour" or "FreePackagesLimit" when configProperty.Value.ValueKind == JsonValueKind.Number: {
packageLimit = configProperty.Value.ToJsonObject();
bot.ArchiLogger.LogGenericInfo("Free Packages Limit : " + packageLimit.ToString());
break;
}
case "FreePackagesFilter": {
FilterConfig? filter = configProperty.Value.ToJsonObject();
if (filter != null) {
bot.ArchiLogger.LogGenericInfo("Free Packages Filter : " + filter.ToJsonText());
filterConfigs.Add(filter);
}
break;
}
case "FreePackagesFilters": {
List? filters = configProperty.Value.ToJsonObject>();
if (filters != null) {
bot.ArchiLogger.LogGenericInfo("Free Packages Filters : " + filters.ToJsonText());
filterConfigs.AddRange(filters);
}
break;
}
}
}
if (isEnabled) {
await PackageHandler.AddHandler(bot, filterConfigs, packageLimit, pauseWhilePlaying).ConfigureAwait(false);
}
}
public Task GetPreferredChangeNumberToStartFrom() {
return Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);
}
public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary appChanges, IReadOnlyDictionary packageChanges) {
PICSHandler.OnPICSChanges(currentChangeNumber, appChanges, packageChanges);
return Task.CompletedTask;
}
public async Task OnPICSChangesRestart(uint currentChangeNumber) {
await PICSHandler.OnPICSRestart(currentChangeNumber).ConfigureAwait(false);
}
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
callbackManager.Subscribe(callback => OnLicenseList(bot, callback));
return Task.CompletedTask;
}
public Task?> OnBotSteamHandlersInit(Bot bot) {
return Task.FromResult?>(new List { SteamHandler.AddHandler(bot) });
}
private static void OnLicenseList (Bot bot, SteamApps.LicenseListCallback callback) {
PackageHandler.OnLicenseList(bot, callback);
}
public async Task OnBotLoggedOn(Bot bot) {
await PackageHandler.OnBotLoggedOn(bot).ConfigureAwait(false);
}
public Task OnBotDisconnected(Bot bot, EResult reason) {
return Task.FromResult(0);
}
}
}
================================================
FILE: FreePackages/FreePackages.csproj
================================================
Citrinate
PrepareResources;$(CompileDependsOn)
================================================
FILE: FreePackages/Handlers/PICSHandler.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using SteamKit2;
namespace FreePackages {
internal static class PICSHandler {
private static SemaphoreSlim PICSChangesSemaphore = new SemaphoreSlim(1, 1);
private const int PICSChangesLimitingDelaySeconds = 10;
internal static void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary appChanges, IReadOnlyDictionary packageChanges) {
if (FreePackages.GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
if (currentChangeNumber <= FreePackages.GlobalCache.LastChangeNumber) {
return;
}
PackageHandler.AddChanges(appChanges, packageChanges);
FreePackages.GlobalCache.UpdateChangeNumber(currentChangeNumber);
return;
}
internal async static Task OnPICSRestart(uint currentChangeNumber) {
if (PackageHandler.Handlers.Count == 0) {
return;
}
if (FreePackages.GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
uint oldChangeNumber = FreePackages.GlobalCache.LastChangeNumber;
ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.PICSRestart, oldChangeNumber, currentChangeNumber));
// ASF restarts PICS if either apps or packages needs a full update. Check the old change number, as one of them might still be good.
SteamApps.PICSChangesCallback? picsChanges = await FetchPICSChanges(oldChangeNumber, sendAppChangeList: false, sendPackageChangeList: true).ConfigureAwait(false);
if (picsChanges == null) {
return;
}
if (!picsChanges.RequiresFullAppUpdate) {
PackageHandler.AddChanges(picsChanges.AppChanges, new Dictionary());
} else {
ASF.ArchiLogger.LogGenericDebug(Strings.MissedApps);
// Search for the oldest change number which is still valid for apps
var appChanges = await FindOldestPICSChanges(oldChangeNumber + 1, picsChanges.CurrentChangeNumber, findApps: true);
if (appChanges != null) {
ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.RecoveredApps, appChanges.AppChanges.Count, appChanges.LastChangeNumber + 1));
PackageHandler.AddChanges(appChanges.AppChanges, new Dictionary());
}
}
if (!picsChanges.RequiresFullPackageUpdate) {
PackageHandler.AddChanges(new Dictionary(), picsChanges.PackageChanges);
} else {
ASF.ArchiLogger.LogGenericDebug(Strings.MissedPackages);
// Search for the oldest change number which is still valid for packages
var packageChanges = await FindOldestPICSChanges(oldChangeNumber + 1, picsChanges.CurrentChangeNumber, findApps: false);
if (packageChanges != null) {
ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.RecoveredPackages, packageChanges.PackageChanges.Count, packageChanges.LastChangeNumber + 1));
PackageHandler.AddChanges(new Dictionary(), packageChanges.PackageChanges);
}
}
if (currentChangeNumber > FreePackages.GlobalCache.LastChangeNumber) {
FreePackages.GlobalCache.UpdateChangeNumber(currentChangeNumber);
}
}
private async static Task FindOldestPICSChanges(uint minValidChangeNumber, uint maxValidChangeNumber, bool findApps) {
if (minValidChangeNumber >= maxValidChangeNumber) {
return null;
}
bool sendAppChangeList = findApps;
bool sendPackageChangeList = !findApps;
uint changeNumber = maxValidChangeNumber - ((uint) Math.Floor((maxValidChangeNumber - minValidChangeNumber) / 2.0));
SteamApps.PICSChangesCallback? oldestPicsChanges = null;
do {
SteamApps.PICSChangesCallback? picsChanges = await FetchPICSChanges(changeNumber, sendAppChangeList, sendPackageChangeList).ConfigureAwait(false);
if (picsChanges == null) {
break;
}
bool isValid = (findApps && !picsChanges.RequiresFullAppUpdate) || (!findApps && !picsChanges.RequiresFullPackageUpdate);
if (isValid) {
oldestPicsChanges = picsChanges;
maxValidChangeNumber = changeNumber;
} else {
minValidChangeNumber = changeNumber;
}
changeNumber = maxValidChangeNumber - Math.Max(1, ((uint) Math.Floor((maxValidChangeNumber - minValidChangeNumber) / 2.0)));
} while (changeNumber > minValidChangeNumber);
return oldestPicsChanges;
}
private async static Task FetchPICSChanges(uint changeNumber, bool sendAppChangeList = true, bool sendPackageChangeList = true) {
await PICSChangesSemaphore.WaitAsync().ConfigureAwait(false);
try {
Bot? refreshBot = GetRefreshBot();
if (refreshBot == null) {
return null;
}
return await refreshBot.SteamApps.PICSGetChangesSince(changeNumber, sendAppChangeList, sendPackageChangeList).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);
return null;
} finally {
Utilities.InBackground(
async() => {
await Task.Delay(TimeSpan.FromSeconds(PICSChangesLimitingDelaySeconds)).ConfigureAwait(false);
PICSChangesSemaphore.Release();
}
);
}
}
private static Bot? GetRefreshBot() => Bot.BotsReadOnly?.Values.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
}
}
================================================
FILE: FreePackages/Handlers/PackageHandler.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using SteamKit2;
namespace FreePackages {
internal sealed class PackageHandler : IDisposable {
internal readonly Bot Bot;
internal readonly BotCache BotCache;
internal readonly PackageFilter PackageFilter;
private readonly ActivationQueue ActivationQueue;
private readonly RemovalQueue RemovalQueue;
private CancellationTokenSource? RemovalCancellation;
ConcurrentHashSet PackagesToRemove = new(new PackageComparer());
internal static ConcurrentDictionary Handlers = new();
private readonly Timer UserDataRefreshTimer;
private static SemaphoreSlim AddHandlerSemaphore = new SemaphoreSlim(1, 1);
private static SemaphoreSlim ProcessChangesSemaphore = new SemaphoreSlim(1, 1);
private PackageHandler(Bot bot, BotCache botCache, List filterConfigs, uint? packageLimit, bool pauseWhilePlaying) {
Bot = bot;
BotCache = botCache;
PackageFilter = new PackageFilter(botCache, filterConfigs);
ActivationQueue = new ActivationQueue(bot, botCache, pauseWhilePlaying, packageLimit, PackageFilter);
RemovalQueue = new RemovalQueue(bot, botCache, pauseWhilePlaying);
UserDataRefreshTimer = new Timer(async e => await FetchUserData().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
}
public void Dispose() {
ActivationQueue.Dispose();
UserDataRefreshTimer.Dispose();
}
internal static async Task AddHandler(Bot bot, List filterConfigs, uint? packageLimit, bool pauseWhilePlaying) {
if (Handlers.ContainsKey(bot.BotName)) {
Handlers[bot.BotName].Dispose();
Handlers.TryRemove(bot.BotName, out PackageHandler? _);
}
await AddHandlerSemaphore.WaitAsync().ConfigureAwait(false);
try {
if (filterConfigs.Any(filterConfig => filterConfig.PlaytestMode != EPlaytestMode.None)) {
// Only allow 1 bot to request playtests
int numBotsThatIncludePlaytests = Handlers.Values.Where(x => x.PackageFilter.FilterConfigs.Any(filterConfig => filterConfig.PlaytestMode != EPlaytestMode.None)).Count();
if (numBotsThatIncludePlaytests > 0) {
filterConfigs.ForEach(filterConfig => filterConfig.PlaytestMode = EPlaytestMode.None);
bot.ArchiLogger.LogGenericInfo(Strings.PlaytestConfigLimitTriggered);
}
}
string cacheFilePath = Bot.GetFilePath(String.Format("{0}_{1}", bot.BotName, nameof(FreePackages)), Bot.EFileType.Database);
BotCache? botCache = await BotCache.CreateOrLoad(cacheFilePath).ConfigureAwait(false);
if (botCache == null) {
bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorDatabaseInvalid, cacheFilePath));
botCache = new(cacheFilePath);
}
Handlers.TryAdd(bot.BotName, new PackageHandler(bot, botCache, filterConfigs, packageLimit, pauseWhilePlaying));
} finally {
AddHandlerSemaphore.Release();
}
}
internal static void OnLicenseList(Bot bot, SteamApps.LicenseListCallback callback) {
if (!Handlers.ContainsKey(bot.BotName)) {
return;
}
Handlers[bot.BotName].HandleLicenseList(callback);
}
internal static async Task OnBotLoggedOn(Bot bot) {
if (!Handlers.ContainsKey(bot.BotName)) {
return;
}
await Handlers[bot.BotName].FetchUserData().ConfigureAwait(false);
Handlers[bot.BotName].ActivationQueue.Start();
Handlers[bot.BotName].RemovalQueue.Start();
}
private void UpdateUserData() {
UserDataRefreshTimer.Change(TimeSpan.Zero, TimeSpan.FromMinutes(15));
}
private async Task FetchUserData() {
if (!Bot.IsConnectedAndLoggedOn) {
UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));
return;
}
Steam.UserData? userData = await WebRequest.GetUserData(Bot).ConfigureAwait(false);
if (userData == null) {
UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));
Bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(userData)));
return;
}
Steam.UserInfo? userInfo = await WebRequest.GetUserInfo(Bot).ConfigureAwait(false);
if (userInfo == null) {
UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));
Bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(userInfo)));
return;
}
PackageFilter.UpdateUserDetails(userData, userInfo);
UserDataRefreshTimer.Change(TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15));
}
internal static void AddChanges(IReadOnlyDictionary appChanges, IReadOnlyDictionary packageChanges) {
if (Handlers.Count == 0) {
return;
}
// It's possible for a PICS change to effect thousands of apps and packages, Ex: https://steamdb.info/changelist/20445399/ (47,074 apps total, 31,529 packages total)
// Store a list of changed apps/packages so that we can guarantee they'll all be processed eventually
// Each bot has its own list, so that if any bots are offline, they'll be able to get caught up
HashSet appIDs = appChanges.Select(x => x.Key).ToHashSet();
HashSet packageIDs = packageChanges.Select(x => x.Key).ToHashSet();
Handlers.Values.ToList().ForEach(x => x.BotCache.AddChanges(appIDs, packageIDs, ignoreFailedApps: true));
Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
}
private async static Task IsReady(uint maxWaitTimeSeconds = 120) {
DateTime timeoutTime = DateTime.Now.AddSeconds(maxWaitTimeSeconds);
do {
bool ready = Handlers.Values.Where(x => x.Bot.BotConfig.Enabled && !x.PackageFilter.Ready).Count() == 0;
if (ready) {
return true;
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
} while (maxWaitTimeSeconds == 0 || DateTime.Now < timeoutTime);
return false;
}
internal async static Task HandleChanges() {
if (!await ProcessChangesSemaphore.WaitAsync(0).ConfigureAwait(false)) {
return;
}
try {
await IsReady().ConfigureAwait(false);
HashSet appIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.ChangedApps).ToHashSet();
HashSet packageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.ChangedPackages).ToHashSet();
HashSet newOwnedPackageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.NewOwnedPackages).ToHashSet();
packageIDs.UnionWith(newOwnedPackageIDs);
if (appIDs.Count == 0 && packageIDs.Count == 0) {
return;
}
foreach ((HashSet? batchedAppIDs, HashSet? batchedPackageIDs) in ProductInfo.GetProductIDBatches(appIDs, packageIDs)) {
var productInfo = await ProductInfo.GetProductInfo(batchedAppIDs, batchedPackageIDs).ConfigureAwait(false);
if (productInfo == null) {
continue;
}
await HandleProductInfo(productInfo).ConfigureAwait(false);
}
} finally {
ProcessChangesSemaphore.Release();
}
}
private async static Task HandleProductInfo(List productInfos) {
{ // Add wanted apps to the queue
List? apps = await FilterableApp.GetFilterables(
productInfos,
app => {
Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(appID: app.ID));
return true;
}
).ConfigureAwait(false);
if (apps == null) {
ASF.ArchiLogger.LogGenericError(Strings.ProductInfoFetchFailed);
return;
}
if (apps.Count > 0) {
apps.ForEach(app => {
if (app.Type == EAppType.Beta) {
Handlers.Values.ToList().ForEach(x => x.HandlePlaytest(app));
} else {
Handlers.Values.ToList().ForEach(x => x.HandleFreeApp(app));
}
});
}
}
{ // Add wanted packages to the queue or check new packages for free DLC
HashSet newOwnedPackageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.NewOwnedPackages).ToHashSet();
List? packages = await FilterablePackage.GetFilterables(
productInfos,
package => {
Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(packageID: package.ID));
return !newOwnedPackageIDs.Contains(package.ID);
}
).ConfigureAwait(false);
if (packages == null) {
ASF.ArchiLogger.LogGenericError(Strings.ProductInfoFetchFailed);
return;
}
if (packages.Count > 0) {
packages.ForEach(package => {
if (newOwnedPackageIDs.Contains(package.ID)) {
Handlers.Values.ToList().ForEach(x => x.HandleNewPackage(package));
} else {
Handlers.Values.ToList().ForEach(x => x.HandleFreePackage(package));
}
});
}
}
// Remove invalid apps from the app change list
foreach (uint unknownAppID in productInfos.SelectMany(static result => result.UnknownApps)) {
Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(appID: unknownAppID));
}
// Remove invalid packages from the package change list
foreach (uint unknownPackageID in productInfos.SelectMany(static result => result.UnknownPackages)) {
Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(packageID: unknownPackageID));
Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(newOwnedPackageID: unknownPackageID));
}
// Save changes to the app/package change lists
Handlers.Values.ToList().ForEach(x => x.BotCache.SaveChanges());
}
private void HandleFreeApp(FilterableApp app) {
if (!BotCache.ChangedApps.Contains(app.ID)) {
return;
}
if (!PackageFilter.Ready) {
return;
}
try {
if (!PackageFilter.IsRedeemableApp(app)) {
return;
}
if (!PackageFilter.IsWantedApp(app)) {
return;
}
BotCache.AddPackage(new Package(EPackageType.App, app.ID, filterHash: PackageFilter.Hash));
} finally {
BotCache.RemoveChange(appID: app.ID);
}
}
private void HandleFreePackage(FilterablePackage package) {
if (!BotCache.ChangedPackages.Contains(package.ID)) {
return;
}
if (!PackageFilter.Ready) {
return;
}
try {
if (!PackageFilter.IsRedeemablePackage(package)) {
return;
}
if (!PackageFilter.IsWantedPackage(package)) {
return;
}
if (BotCache.AddPackage(new Package(EPackageType.Sub, package.ID, package.StartTime, filterHash: PackageFilter.Hash))) {
// Remove duplicates.
// Whenever we're trying to activate an app and also an package for that app, get rid of the app.
// This is because the error messages for activating packages are more descriptive and useful.
BotCache.RemoveAppPackages(package.PackageContentIDs);
}
} finally {
BotCache.RemoveChange(packageID: package.ID);
}
}
private void HandlePlaytest(FilterableApp app) {
if (!BotCache.ChangedApps.Contains(app.ID)) {
return;
}
if (!PackageFilter.Ready) {
return;
}
try {
if (app.Parent == null) {
return;
}
if (!PackageFilter.IsRedeemablePlaytest(app)) {
return;
}
if (!PackageFilter.IsWantedPlaytest(app)) {
return;
}
BotCache.AddPackage(new Package(EPackageType.Playtest, app.Parent.ID, filterHash: PackageFilter.Hash));
} finally {
BotCache.RemoveChange(appID: app.ID);
}
}
private void HandleNewPackage(FilterablePackage package) {
if (!BotCache.NewOwnedPackages.Contains(package.ID)) {
return;
}
try {
if (package.PackageContents.Count == 0) {
return;
}
// Check for free DLC on newly added packages
HashSet dlcAppIDs = new();
foreach (FilterableApp app in package.PackageContents) {
if (String.IsNullOrEmpty(app.ListOfDLC)) {
continue;
}
foreach (string dlcAppIDString in app.ListOfDLC.Split(",", StringSplitOptions.RemoveEmptyEntries)) {
if (!uint.TryParse(dlcAppIDString, out uint dlcAppID) || (dlcAppID == 0)) {
continue;
}
dlcAppIDs.Add(dlcAppID);
}
}
if (dlcAppIDs.Count != 0) {
BotCache.AddChanges(appIDs: dlcAppIDs);
Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
}
} finally {
BotCache.RemoveChange(newOwnedPackageID: package.ID);
}
}
internal void HandleLicenseList(SteamApps.LicenseListCallback callback) {
List newLicenses = callback.LicenseList.Where(license => !BotCache.SeenPackages.Contains(license.PackageID)).ToList();
if (newLicenses.Count == 0) {
return;
}
UpdateUserData();
// Initialize SeenPackages
if (BotCache.SeenPackages.Count == 0) {
BotCache.UpdateSeenPackages(newLicenses);
return;
}
BotCache.AddChanges(newOwnedPackageIDs: newLicenses.Select(license => license.PackageID).ToHashSet());
BotCache.UpdateSeenPackages(newLicenses);
Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
}
internal string GetStatus() {
HashSet responses = new HashSet();
// x packages queued. y activations used
int activationsPastPeriod = Math.Min(BotCache.NumActivationsPastPeriod(), (int)ActivationQueue.MaxActivationsPerPeriod);
responses.Add(String.Format(Strings.QueueStatus, ActivationQueue.ActivationsRemaining, activationsPastPeriod, ActivationQueue.ActivationsPerPeriod));
// activations are paused
if (ActivationQueue.PauseWhilePlaying && !Bot.IsPlayingPossible) {
responses.Add(Strings.QueuePausedWhileIngame);
}
// activations will resume when
if (activationsPastPeriod >= ActivationQueue.ActivationsPerPeriod) {
DateTime resumeTime = BotCache.GetLastActivation()!.Value.AddMinutes(ActivationQueue.ActivationPeriodMinutes + 1);
responses.Add(String.Format(Strings.QueueLimitedUntil, String.Format("{0:T}", resumeTime)));
}
// x apps and y packages discovered but not processed
if (BotCache.ChangedApps.Count > 0 || BotCache.ChangedPackages.Count > 0) {
responses.Add(String.Format(Strings.QueueDiscoveryStatus, BotCache.ChangedApps.Count, BotCache.ChangedPackages.Count));
}
// removing x packages
if (RemovalQueue.RemovalsRemaining > 0) {
responses.Add(String.Format(Strings.RemovingPackages, RemovalQueue.RemovalsRemaining));
}
return String.Join(" ", responses);
}
internal string ClearQueue() {
int numPackages = BotCache.Packages.Where(package => ActivationQueue.ActivationTypes.Contains(package.Type)).Count();
int numChangedApps = BotCache.ChangedApps.Count;
int numChangedPackages = BotCache.ChangedPackages.Count;
if (numPackages == 0 && numChangedApps == 0 && numChangedPackages == 0) {
return Strings.QueueEmpty;
}
BotCache.ClearQueue();
List responses = new List();
if (numPackages > 0) {
responses.Add(String.Format(Strings.PackagesRemoved, numPackages));
}
if (numChangedApps > 0) {
responses.Add(String.Format(Strings.DiscoveredAppsRemoved, numChangedApps));
}
if (numChangedPackages > 0) {
responses.Add(String.Format(Strings.DiscoveredPackagesRemoved, numChangedPackages));
}
return String.Join(" ", responses);
}
internal string AddPackage(EPackageType type, uint id, bool useFilter) {
if (useFilter) {
if (type == EPackageType.App) {
BotCache.AddChanges(appIDs: new HashSet { id });
return String.Format(Strings.DiscoveredAppsAdded, String.Format("app/{0}", id));
} else {
BotCache.AddChanges(packageIDs: new HashSet { id });
return String.Format(Strings.DiscoveredPackagesAdded, String.Format("sub/{0}", id));
}
}
BotCache.AddPackage(new Package(type, id));
if (type == EPackageType.App) {
return String.Format(Strings.AppsQueued, String.Format("app/{0}", id));
} else {
return String.Format(Strings.PackagesQueued, String.Format("sub/{0}", id));
}
}
internal void AddPackages(HashSet? appIDs, HashSet? packageIDs, bool useFilter) {
if (useFilter) {
BotCache.AddChanges(appIDs, packageIDs);
return;
}
HashSet packages = new();
if (appIDs != null) {
packages.UnionWith(appIDs.Select(static id => new Package(EPackageType.App, id)));
}
if (packageIDs != null) {
packages.UnionWith(packageIDs.Select(static id => new Package(EPackageType.Sub, id)));
}
BotCache.AddPackages(packages);
}
internal async Task ScanRemovables(Dictionary removeablePackages, bool excludePlayed, bool removeAll, StatusReporter statusReporter) {
if (RemovalCancellation != null) {
statusReporter.Report(Bot, Strings.RemovalScanAlreadyRunning);
return;
}
RemovalCancellation = new CancellationTokenSource();
try {
await ProcessChangesSemaphore.WaitAsync(RemovalCancellation.Token).ConfigureAwait(false);
try {
await IsReady().ConfigureAwait(false);
Dictionary? ownedGameDetails = null;
if (excludePlayed) {
ownedGameDetails = await SteamHandler.Handlers[Bot.BotName].GetOwnedGames(Bot.SteamID).ConfigureAwait(false);
if (ownedGameDetails == null) {
statusReporter.Report(Bot, Strings.PlaytimeFetchFailed);
return;
}
}
var productInfos = await ProductInfo.GetProductInfo(packageIDs: removeablePackages.Keys.ToHashSet(), cancellationToken: RemovalCancellation.Token).ConfigureAwait(false);
if (productInfos == null) {
statusReporter.Report(Bot, Strings.ProductInfoFetchFailed);
return;
}
List? packages = await FilterablePackage.GetFilterables(productInfos, cancellationToken: RemovalCancellation.Token, onNonFreePackage: x => !removeAll).ConfigureAwait(false);
if (packages == null) {
statusReporter.Report(Bot, Strings.ProductInfoFetchFailed);
return;
}
if (packages.Count == 0) {
statusReporter.Report(Bot, Strings.RemovingNoPackages);
return;
}
RemovalCancellation.Token.ThrowIfCancellationRequested();
PackagesToRemove.Clear();
List previewResponses = [];
var ownedPackageIDs = Bot.OwnedPackages.Keys.ToHashSet();
foreach (FilterablePackage package in packages) {
if (!removeAll) {
if (!PackageFilter.IsRedeemablePackage(package, ignoreAlreadyOwned: true)) {
continue;
}
if (PackageFilter.IsWantedPackage(package, ignoreAgeFilters: true)) {
continue;
}
}
if (excludePlayed) {
if (package.PackageContents.Any(app => {
return (ownedGameDetails!.ContainsKey(app.ID) && ownedGameDetails![app.ID].playtime_forever > 0)
|| (app.Type != EAppType.Demo && app.ParentID != null && ownedGameDetails!.ContainsKey(app.ParentID.Value) && ownedGameDetails![app.ParentID.Value].playtime_forever > 0);
})) {
continue;
}
}
// Attempt to remove the app directly, which uses an API with a more generous rate limit
if (package.PackageContents.Count == 1) {
FilterableApp app = package.PackageContents.First();
// Apparently the API for removing apps doesn't care which package is removed? (Haven't tested this) https://github.com/JustArchiNET/ArchiSteamFarm/issues/3434#issuecomment-2954303590
// Only remove by app if this is the only package that can be removed
int ownedPackagesWithApp = ASF.GlobalDatabase!.PackagesDataReadOnly.Where(x => ownedPackageIDs.Contains(x.Key) && x.Value.AppIDs != null && x.Value.AppIDs.Contains(app.ID)).Count();
if (ownedPackagesWithApp <= 1) {
PackagesToRemove.Add(new Package(EPackageType.RemoveApp, app.ID));
previewResponses.Add(String.Format("app/{0} ({1})", app.ID, removeablePackages[package.ID]));
continue;
}
}
PackagesToRemove.Add(new Package(EPackageType.RemoveSub, package.ID));
previewResponses.Add(String.Format("sub/{0} ({1})", package.ID, removeablePackages[package.ID]));
}
if (PackagesToRemove.Count == 0) {
statusReporter.Report(Bot, Strings.RemovingNoUnwatedPackages);
return;
}
statusReporter.Report(Bot, String.Format(Strings.RemovablePackagesFound, new object?[] {
PackagesToRemove.Count,
String.Join(PackagesToRemove.Count > 100 ? ", " : Environment.NewLine, previewResponses),
String.Format("!cancelremove {0}", Bot.BotName),
String.Format("!confirmremove {0}", Bot.BotName),
String.Format("!dontremove {0} ", Bot.BotName)
}));
} finally {
ProcessChangesSemaphore.Release();
}
} catch (OperationCanceledException) {
statusReporter.Report(Bot, Strings.RemovalScanCancelled);
} finally {
RemovalCancellation?.Dispose();
RemovalCancellation = null;
}
}
internal string ConfirmRemoval() {
if (PackagesToRemove.Count == 0) {
return String.Format(Strings.RemovalScanNeeded, String.Format("!removefreepackages {0}", Bot.BotName));
}
int numRemovalsDiscovered = PackagesToRemove.Count;
BotCache.AddPackages(PackagesToRemove);
PackagesToRemove.Clear();
return String.Format(Strings.RemovingPackages, numRemovalsDiscovered);
}
internal string ModifyRemovables(EPackageType type, uint id) {
if (PackagesToRemove.Count == 0) {
return String.Format(Strings.RemovalScanNeeded, String.Format("!removefreepackages {0}", Bot.BotName));
}
Package? package = PackagesToRemove.FirstOrDefault(package => package.Type == type && package.ID == id);
if (package == null) {
if (type == EPackageType.RemoveApp) {
return String.Format(Strings.RemovalPackageNotFound, String.Format("app/{0}", id)) + " :steamthumbsdown:";
} else {
return String.Format(Strings.RemovalPackageNotFound, String.Format("sub/{0}", id)) + " :steamthumbsdown:";
}
}
PackagesToRemove.Remove(package);
if (type == EPackageType.RemoveApp) {
return String.Format(Strings.RemovalPackageCancelled, String.Format("app/{0}", id));
} else {
return String.Format(Strings.RemovalPackageCancelled, String.Format("sub/{0}", id));
}
}
internal string CancelRemoval() {
bool stoppingScan = RemovalCancellation != null;
int numRemovalsDiscovered = PackagesToRemove.Count;
int numRemovals = BotCache.Packages.Where(package => RemovalQueue.RemovalTypes.Contains(package.Type)).Count();
if (!stoppingScan && numRemovalsDiscovered == 0 && numRemovals == 0) {
return Strings.RemovalQueueEmpty;
}
RemovalCancellation?.Cancel();
PackagesToRemove.Clear();
BotCache.CancelRemoval();
List responses = new List();
if (numRemovals > 0) {
responses.Add(String.Format(Strings.RemovalsCancelled, numRemovals));
}
if (numRemovalsDiscovered > 0) {
responses.Add(String.Format(Strings.RemovalScanCancelled));
}
if (stoppingScan) {
responses.Add(String.Format(Strings.RemovalScanCanceling));
}
return String.Join(" ", responses);
}
}
}
================================================
FILE: FreePackages/Handlers/SteamHandler.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;
using SteamKit2.Internal;
namespace FreePackages {
internal sealed class SteamHandler : ClientMsgHandler {
internal static ConcurrentDictionary Handlers = new();
internal static SteamHandler AddHandler(Bot bot) {
if (Handlers.ContainsKey(bot.BotName)) {
Handlers.TryRemove(bot.BotName, out SteamHandler? _);
}
SteamHandler handler = new();
Handlers.TryAdd(bot.BotName, handler);
return handler;
}
public override void HandleMsg(IPacketMsg packetMsg) { }
public async Task?> GetOwnedGames(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
if (Client == null) {
throw new InvalidOperationException(nameof(Client));
}
if (!Client.IsConnected) {
return null;
}
SteamUnifiedMessages steamUnifiedMessages = Client.GetHandler() ?? throw new InvalidOperationException(nameof(SteamUnifiedMessages));
Player unifiedPlayerService = steamUnifiedMessages.CreateService();
CPlayer_GetOwnedGames_Request request = new() {
steamid = steamID,
include_appinfo = true,
include_extended_appinfo = true,
include_free_sub = true,
include_played_free_games = true,
skip_unvetted_apps = false
};
SteamUnifiedMessages.ServiceMethodResponse response;
try {
response = await unifiedPlayerService.GetOwnedGames(request).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);
return null;
}
return response.Result == EResult.OK ? response.Body.games.ToDictionary(static game => (uint)game.appid, static game => game) : null;
}
}
}
================================================
FILE: FreePackages/Helpers/DeterministicHasher.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace FreePackages {
internal static class DeterministicHasher {
private const int FnvOffsetBias = unchecked((int) 2166136261);
private const int FnvPrime = 16777619;
internal static int Hash(int value) => Hash(FnvOffsetBias, value);
internal static int Hash(uint value) => Hash(FnvOffsetBias, value);
internal static int Hash(bool value) => Hash(FnvOffsetBias, value);
internal static int Hash(string? str) => Hash(FnvOffsetBias, str);
internal static int Hash(IEnumerable? collection) => Hash(FnvOffsetBias, collection);
internal static int Hash(IEnumerable? collection) => Hash(FnvOffsetBias, collection);
internal static int Hash(IEnumerable? collection) => Hash(FnvOffsetBias, collection);
internal static int Hash(int hash, int value) => unchecked((hash ^ value) * FnvPrime);
internal static int Hash(int hash, uint value) => Hash(hash, (int) value);
internal static int Hash(int hash, bool value) => Hash(hash, value ? 1 : 0);
internal static int Hash(int hash, string? str) {
if (str == null) {
return hash;
}
foreach (char c in str) {
hash = Hash(hash, c);
}
return hash;
}
internal static int Hash(int hash, IEnumerable? collection) {
if (collection == null) {
return hash;
}
foreach (string item in collection.OrderBy(static x => x, StringComparer.Ordinal)) {
hash = Hash(hash, item);
}
return hash;
}
internal static int Hash(int hash, IEnumerable? collection) {
if (collection == null) {
return hash;
}
foreach (uint item in collection.OrderBy(static x => x)) {
hash = Hash(hash, item);
}
return hash;
}
internal static int Hash(int hash, IEnumerable? collection) {
if (collection == null) {
return hash;
}
foreach (FilterConfig item in collection.OrderBy(static x => x.GetHashCode())) {
hash = Hash(hash, item.GetHashCode());
}
return hash;
}
}
}
================================================
FILE: FreePackages/Helpers/StatusReporter.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;
// For when long-running commands are issued through Steam chat, this is used to send status reports from the bot the command was sent to, to the user who issued the command
// If the commands weren't issued through Steam chat, this just logs the status reports
namespace FreePackages {
internal sealed class StatusReporter {
[JsonInclude]
[JsonRequired]
private ulong SenderSteamID; // When we send status reports, they'll come from this SteamID
[JsonInclude]
[JsonRequired]
private ulong RecipientSteamID; // When we send status reports, they'll go to this SteamID
private ConcurrentDictionary> Reports = new();
private ConcurrentDictionary> PreviousReports = new();
private uint ReportDelaySeconds;
private uint ReportMaxDelaySeconds;
private const uint DefaultReportDelaySeconds = 5;
private Timer? ReportTimer;
private DateTime? ReportMaxDelayTime = null;
private SemaphoreSlim ReportSemaphore = new SemaphoreSlim(1, 1);
internal StatusReporter(Bot? sender = null, ulong recipientSteamID = 0, uint reportDelaySeconds = DefaultReportDelaySeconds, uint? reportMaxDelaySeconds = null) {
SenderSteamID = sender?.SteamID ?? 0;
RecipientSteamID = recipientSteamID;
ReportDelaySeconds = reportDelaySeconds;
ReportMaxDelaySeconds = reportMaxDelaySeconds ?? reportDelaySeconds * 5;
}
[JsonConstructor]
internal StatusReporter(ulong senderSteamID = 0, ulong recipientSteamID = 0) {
SenderSteamID = senderSteamID;
RecipientSteamID = recipientSteamID;
}
internal static StatusReporter StatusLogger() {
// Create a status reporter that doesn't send messages through chat, it just logs everything
return new StatusReporter(0, 0);
}
internal void Report(Bot reportingBot, string report, bool suppressDuplicateMessages = false, bool log = false) {
if (log || SenderSteamID == 0 || RecipientSteamID == 0) {
reportingBot.ArchiLogger.LogGenericInfo(report);
return;
}
ReportSemaphore.Wait();
try {
if (suppressDuplicateMessages) {
bool existsInReports = Reports.TryGetValue(reportingBot, out var reports) && reports.Contains(report);
bool existsInPreviousReports = PreviousReports.TryGetValue(reportingBot, out var previousReports) && previousReports.Contains(report);
if (existsInReports || existsInPreviousReports) {
return;
}
}
Reports.TryAdd(reportingBot, new List());
Reports[reportingBot].Add(report);
// I prefer to send all reports in as few messages as possible
// As long as reports continue to come in, we wait (until some limit, to avoid possibly waiting forever)
double delayCorrectionSeconds = 0;
if (ReportMaxDelayTime != null) {
if (ReportMaxDelayTime <= DateTime.Now) {
return;
}
delayCorrectionSeconds = Math.Max(0, (DateTime.Now.AddSeconds(ReportDelaySeconds) - ReportMaxDelayTime.Value).TotalSeconds);
}
if (ReportTimer != null) {
ReportTimer.Change(Timeout.Infinite, Timeout.Infinite);
ReportTimer.Dispose();
}
ReportTimer = new Timer(async _ => await Send().ConfigureAwait(false), null, TimeSpan.FromSeconds(ReportDelaySeconds - delayCorrectionSeconds), Timeout.InfiniteTimeSpan);
if (ReportMaxDelayTime == null) {
ReportMaxDelayTime = DateTime.Now.AddSeconds(ReportMaxDelaySeconds);
}
} finally {
ReportSemaphore.Release();
}
}
internal void ForceSend() {
Utilities.InBackground(async() => await Send().ConfigureAwait(false));
}
private async Task Send() {
await ReportSemaphore.WaitAsync().ConfigureAwait(false);
try {
ReportTimer?.Dispose();
ReportMaxDelayTime = null;
List messages = new List();
List bots = Reports.Keys.OrderBy(bot => bot.BotName).ToList();
foreach (Bot bot in bots) {
messages.Add(Commands.FormatBotResponse(bot, String.Join(Environment.NewLine, Reports[bot])));
if (Reports[bot].Count > 1) {
// Add an extra line if there's more than 1 message from a bot
messages.Add("");
}
if (Reports.TryRemove(bot, out List? previousReports)) {
if (previousReports != null) {
PreviousReports[bot] = previousReports;
}
}
}
if (messages.Count == 0) {
return;
}
Bot? sender = SenderSteamID == 0 ? null : Bot.BotsReadOnly?.Values.FirstOrDefault(bot => bot.SteamID == SenderSteamID);
if (sender == null
|| RecipientSteamID == 0
|| !new SteamID(RecipientSteamID).IsIndividualAccount
|| sender.SteamFriends.GetFriendRelationship(RecipientSteamID) != EFriendRelationship.Friend
) {
// Can't send a chat message through Steam, just log the report
ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));
return;
}
try {
if (!await sender.SendMessage(RecipientSteamID, String.Join(Environment.NewLine, messages)).ConfigureAwait(false)) {
ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));
}
} catch (Exception) {
ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));
}
} finally {
ReportSemaphore.Release();
}
}
}
}
================================================
FILE: FreePackages/IPC/Api/FreePackagesController.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SteamKit2;
using SteamKit2.Internal;
namespace FreePackages.IPC {
[Route("Api/FreePackages")]
public sealed class FreePackagesController : ArchiController {
[HttpGet("{botNames:required}/GetChangesSince/{changeNumber:required}")]
[EndpointSummary("Request changes for apps and packages since a given change number")]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task> GetChangesSince(string botNames, uint changeNumber, bool showAppChanges = true, bool showPackageChanges = true) {
if (string.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
}
Bot? bot = bots.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
if (bot == null) {
return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
}
SteamApps.PICSChangesCallback picsChanges;
try {
picsChanges = await bot.SteamApps.PICSGetChangesSince(changeNumber, showAppChanges, showPackageChanges).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return BadRequest(new GenericResponse(false, e.Message));
}
return Ok(new GenericResponse(true, picsChanges));
}
[HttpGet("{botNames:required}/GetProductInfo")]
[EndpointSummary("Request product information for a list of apps or packages")]
[ProducesResponseType(typeof(GenericResponse>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(byte[]), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task> GetProductInfo(string botNames, string? appIDs, string? packageIDs, bool returnFirstRaw = false) {
if (string.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
}
Bot? bot = bots.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
if (bot == null) {
return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
}
IEnumerable productInfos;
try {
List apps = appIDs == null ? new() : appIDs.Split(",").Select(x => new SteamApps.PICSRequest(uint.Parse(x))).ToList();
List packages = packageIDs == null ? new() : packageIDs.Split(",").Select(x => new SteamApps.PICSRequest(uint.Parse(x), ASF.GlobalDatabase?.PackageAccessTokensReadOnly.GetValueOrDefault(uint.Parse(x), (ulong) 0) ?? 0)).ToList();
var response = await bot.SteamApps.PICSGetProductInfo(apps, packages).ToLongRunningTask().ConfigureAwait(false);
if (response.Results == null) {
return BadRequest(new GenericResponse(false));
}
productInfos = response.Results;
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return BadRequest(new GenericResponse(false, e.Message));
}
if (returnFirstRaw) {
var results = productInfos.SelectMany(static result => result.Apps.Values).Concat(productInfos.SelectMany(static result => result.Packages.Values));
if (results.Count() == 0) {
return File(Array.Empty(), "text/plain; charset=utf-8");
}
try {
await using var kvMemory = new MemoryStream();
results.First().KeyValues.SaveToStream(kvMemory, false);
return File(kvMemory.ToArray(), "text/plain; charset=utf-8");
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return BadRequest(new GenericResponse(false, e.Message));
}
}
return Ok(new GenericResponse>(true, productInfos));
}
[HttpGet("{botName:required}/RequestFreeAppLicense")]
[HttpPost("{botName:required}/RequestFreeAppLicense")]
[EndpointSummary("Request a free license for given appids")]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task> RequestFreeAppLicense(string botName, [FromQuery] string appIDs) {
if (string.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
}
if (!bot.IsConnectedAndLoggedOn) {
return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
}
HashSet apps = new();
foreach (string appIDString in appIDs.Split(",", StringSplitOptions.RemoveEmptyEntries)) {
if (!uint.TryParse(appIDString, out uint appID)) {
return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(appIDString))));
}
apps.Add(appID);
}
if (apps.Count == 0) {
return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(appIDs))));
}
SteamApps.FreeLicenseCallback response;
try {
response = await bot.SteamApps.RequestFreeLicense(apps).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return BadRequest(new GenericResponse(false, e.Message));
}
return Ok(new GenericResponse(true, response));
}
[HttpGet("{botName:required}/RequestFreeSubLicense")]
[HttpPost("{botName:required}/RequestFreeSubLicense")]
[EndpointSummary("Request a free license for given subid")]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task> RequestFreeSubLicense(string botName, [FromQuery] uint subID) {
if (string.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
}
if (!bot.IsConnectedAndLoggedOn) {
return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
}
EResult result;
EPurchaseResultDetail purchaseResult;
try {
(result, purchaseResult) = await bot.Actions.AddFreeLicensePackage(subID).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
return BadRequest(new GenericResponse(false, e.Message));
}
return Ok(new GenericResponse(true, new FreeSubResponse(result, purchaseResult)));
}
[HttpGet("{botName:required}/GetOwnedPackages")]
[EndpointSummary("Retrieves all packages owned by the given bot")]
[ProducesResponseType(typeof(GenericResponse>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public ActionResult GetOwnedPackages(string botName) {
if (string.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
}
if (bot.OwnedPackages.Count == 0) {
return BadRequest(new GenericResponse(false, Strings.NoPackagesFound));
}
return Ok(new GenericResponse>(true, bot.OwnedPackages.Keys));
}
[HttpGet("{botName:required}/GetOwnedApps")]
[EndpointSummary("Retrieves all apps owned by the given bot")]
[ProducesResponseType(typeof(GenericResponse>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse>), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public async Task> GetOwnedApps(string botName, bool showDetails = false) {
if (string.IsNullOrEmpty(botName)) {
throw new ArgumentNullException(nameof(botName));
}
Bot? bot = Bot.GetBot(botName);
if (bot == null) {
return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
}
if (bot.OwnedPackages.Count == 0) {
return BadRequest(new GenericResponse(false, Strings.NoAppsFound));
}
if (ASF.GlobalDatabase == null) {
return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(ASF.GlobalDatabase))));
}
var ownedPackageIDs = bot.OwnedPackages.Keys.ToHashSet();
var ownedAppIDs = ASF.GlobalDatabase!.PackagesDataReadOnly.Where(x => ownedPackageIDs.Contains(x.Key) && x.Value.AppIDs != null).SelectMany(x => x.Value.AppIDs!).ToHashSet().ToList();
ownedAppIDs.Sort();
if (showDetails) {
Dictionary? detailsList;
try {
detailsList = await SteamHandler.Handlers[bot.BotName].GetOwnedGames(bot.SteamID).ConfigureAwait(false);
if (detailsList == null) {
return BadRequest(new GenericResponse(false, Strings.AppListFetchFailed));
}
}
catch (Exception e) {
return BadRequest(new GenericResponse(false, e.Message));
}
return Ok(new GenericResponse>(true, ownedAppIDs.ToDictionary(appID => appID, appID => {
if (detailsList.TryGetValue(appID, out CPlayer_GetOwnedGames_Response.Game? game)) {
return game;
}
return null;
})));
}
return Ok(new GenericResponse>(true, ownedAppIDs));
}
[Consumes("application/json")]
[HttpPost("{botNames:required}/QueueLicenses")]
[EndpointSummary("Adds the provided appids and subids to the given bot's package queue")]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
public ActionResult QueueLicenses(string botNames, [FromBody] QueueLicensesRequest request) {
if (string.IsNullOrEmpty(botNames)) {
throw new ArgumentNullException(nameof(botNames));
}
HashSet? bots = Bot.GetBots(botNames);
if (bots == null || bots.Count == 0) {
return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
}
if (PackageHandler.Handlers.Keys.Union(bots.Select(x => x.BotName)).Count() == 0) {
return BadRequest(new GenericResponse(false, Strings.PluginNotEnabled));
}
foreach (Bot bot in bots) {
if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
continue;
}
PackageHandler.Handlers[bot.BotName].AddPackages(request.AppIDs, request.PackageIDs, request.UseFilter);
}
Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
return Ok(new GenericResponse(true));
}
}
}
================================================
FILE: FreePackages/IPC/Requests/QueueLicensesRequest.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FreePackages.IPC {
public sealed class QueueLicensesRequest {
[JsonInclude]
public HashSet? AppIDs { get; private init; } = null;
[JsonInclude]
public HashSet? PackageIDs { get; private init; } = null;
[JsonInclude]
public bool UseFilter { get; private init; } = true;
[JsonConstructor]
private QueueLicensesRequest() { }
}
}
================================================
FILE: FreePackages/IPC/Responses/FreeSubResponse.cs
================================================
using System.Text.Json.Serialization;
using SteamKit2;
namespace FreePackages.IPC {
public sealed class FreeSubResponse {
[JsonInclude]
[JsonPropertyName("Result")]
public EResult Result { get; private init; }
[JsonInclude]
[JsonPropertyName("PurchaseResultDetail")]
public EPurchaseResultDetail PurchaseResultDetail { get; private init; }
public FreeSubResponse(EResult result, EPurchaseResultDetail purchaseResultDetail) {
Result = result;
PurchaseResultDetail = purchaseResultDetail;
}
}
}
================================================
FILE: FreePackages/Json.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FreePackages {
internal static class Steam {
internal sealed class PlaytestAccessResponse {
[JsonInclude]
[JsonPropertyName("granted")]
[JsonRequired]
internal int? Granted { get; private init; } = null;
[JsonInclude]
[JsonPropertyName("success")]
[JsonRequired]
internal int Success { get; private init; } = 0;
[JsonConstructor]
internal PlaytestAccessResponse() {}
}
internal sealed class UserData {
[JsonInclude]
[JsonPropertyName("rgOwnedPackages")]
[JsonRequired]
internal HashSet OwnedPackages { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgOwnedApps")]
[JsonRequired]
internal HashSet OwnedApps { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgIgnoredApps")]
[JsonRequired]
[JsonConverter(typeof(EmptyArrayOrDictionaryConverter))]
internal Dictionary IgnoredApps { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgExcludedTags")]
[JsonRequired]
internal List ExcludedTags { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgExcludedContentDescriptorIDs")]
[JsonRequired]
internal HashSet ExcludedContentDescriptorIDs { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgWishlist")]
[JsonRequired]
internal HashSet WishlistedApps { get; private init; } = new();
[JsonInclude]
[JsonPropertyName("rgFollowedApps")]
[JsonRequired]
internal HashSet FollowedApps { get; private init; } = new();
[JsonExtensionData]
[JsonInclude]
internal Dictionary AdditionalData { get; private init; } = new();
[JsonConstructor]
internal UserData() {}
}
internal sealed class Tag {
[JsonInclude]
[JsonPropertyName("tagid")]
[JsonRequired]
internal uint TagID = 0;
[JsonInclude]
[JsonPropertyName("name")]
[JsonRequired]
internal string Name = "";
[JsonInclude]
[JsonPropertyName("timestamp_added")]
[JsonRequired]
internal uint TimestampAdded = 0;
[JsonConstructor]
internal Tag() {}
}
internal sealed class UserInfo {
[JsonInclude]
[JsonPropertyName("logged_in")]
public bool LoggedIn;
[JsonInclude]
[JsonPropertyName("steamid")]
public string SteamID = "";
[JsonInclude]
[JsonPropertyName("accountid")]
public int AccountID;
[JsonInclude]
[JsonPropertyName("account_name")]
public string AccountName = "";
[JsonInclude]
[JsonPropertyName("is_support")]
public bool IsSupport;
[JsonInclude]
[JsonPropertyName("is_limited")]
public bool IsLimited;
[JsonInclude]
[JsonPropertyName("is_partner_member")]
public bool IsPartnerMember;
[JsonInclude]
[JsonPropertyName("is_valve_email")]
public bool IsValveEmail;
[JsonInclude]
[JsonPropertyName("country_code")]
[JsonRequired]
public string CountryCode = "";
[JsonInclude]
[JsonPropertyName("excluded_content_descriptors")]
public HashSet ExcludedContentDescriptors = new();
[JsonConstructor]
internal UserInfo() {}
}
// https://stackoverflow.com/questions/12221950/how-to-deserialize-object-that-can-be-an-array-or-a-dictionary-with-newtonsoft
public class EmptyArrayOrDictionaryConverter : JsonConverter> {
public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType == JsonTokenType.StartObject) {
var dictionary = JsonSerializer.Deserialize>(ref reader, options);
if (dictionary == null) {
throw new JsonException();
}
return dictionary;
} else if (reader.TokenType == JsonTokenType.StartArray) {
reader.Read();
if (reader.TokenType == JsonTokenType.EndArray) {
return new Dictionary();
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) {
throw new NotImplementedException();
}
}
}
}
================================================
FILE: FreePackages/Localization/README.md
================================================
If you'd like to help translate this plugin you can do so here: https://crowdin.com/project/freepackages
Contact me on Crowdin if your language isn't currently listed
================================================
FILE: FreePackages/Localization/Strings.de-DE.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Abruf der Abzeichendaten für kostenlose Pakete fehlgeschlagen
Parsen der Abzeichendaten für kostenlose Pakete fehlgeschlagen
PlaytestMode auf 0 (keiner) geändert, nur 1 Bot darf diesen Filter verwenden
Warteschlange ist leer
{0} kostenlose Pakete entfernt.
{0} will be replaced by a number
{0} entdeckte Apps entfernt.
{0} will be replaced by a number
{0} entdeckte Pakete entfernt.
{0} will be replaced by a number
{0} zur Warteschlange für entdeckte Apps hinzugefügt
{0} will be replaced by an appID
{0} zur Warteschlange für entdeckte Pakete hinzugefügt
{0} will be replaced by a subID
{0} zur Warteschlange für kostenlose Pakete hinzugefügt
{0} will be replaced by an appID
{0} zur Warteschlange für kostenlose Pakete hinzugefügt
{0} will be replaced by a subID
Aktivierung kostenloser Pakete pausiert bis {0}
{0} will be replaced by a time
Aktivierungslimit überschritten
Ersetzt durch {0}
{0} will be replaced by a number
Unbekannt
Ungültig
Fehlgeschlagen
Auf der Warteliste
{0} kostenlose Pakete in der Warteschlange. {1}/{2} Aktivierungen verwendet.
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
Die Aktivierung wurde pausiert, da das Konto zum Spielen eines Spiels verwendet wird.
Die Aktivierung wird um {0} fortgesetzt.
{0} will be replaced by a time
{0} Apps und {1} Pakete entdeckt, aber noch nicht bearbeitet.
{0} will be replaced by a number, {1} will be replaced by a number
PICS neu gestartet, überspringe von der Nummer {0} bis {1}
{0} will be replaced by a number, {1} will be replaced by a number
Möglicherweise wurden einige kostenlose Apps aufgrund eines PICS-Neustarts verpasst
{0} Appänderungen bei Änderungsnummer {1} wiederhergestellt
{0} will be replaced by a number, {1} will be replaced by a number
Möglicherweise wurden einige kostenlose Pakete aufgrund eines PICS-Neustarts verpasst
{0} Paketänderungen bei Änderungsnummer {1} wiederhergestellt
{0} will be replaced by a number, {1} will be replaced by a number
Keine Pakete gefunden
Keine Apps gefunden
App-Liste konnte nicht abgerufen werden
Plugin Free Packages nicht aktiviert
Fehler beim Parsen der Daten von ASFInfo
================================================
FILE: FreePackages/Localization/Strings.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Failed to fetch badge data for free packages
Failed to parse badge data for free packages
Changed PlaytestMode to 0 (None), only 1 bot is allowed to use this filter
Queue is empty
{0} free packages removed.
{0} will be replaced by a number
{0} discovered apps removed.
{0} will be replaced by a number
{0} discovered packages removed.
{0} will be replaced by a number
Added {0} to discovered apps queue
{0} will be replaced by an appID
Added {0} to discovered packages queue
{0} will be replaced by a subID
Added {0} to free packages queue
{0} will be replaced by an appID
Added {0} to free packages queue
{0} will be replaced by a subID
Pausing free package activations until {0}
{0} will be replaced by a time
Free Package rate limit exceeded
Replaced with {0}
{0} will be replaced by a number
Unknown
Invalid
Failed
Waitlisted
{0} free packages queued. {1}/{2} activations used.
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
Activations are now paused as the account is being used to play a game.
Activations will resume at {0}.
{0} will be replaced by a time
{0} apps and {1} packages discovered but not processed yet.
{0} will be replaced by a number, {1} will be replaced by a number
PICS restarted, skipping from change number {0} to {1}
{0} will be replaced by a number, {1} will be replaced by a number
Possibly missed some free apps due to PICS restart
Recovered {0} app changes at change number {1}
{0} will be replaced by a number, {1} will be replaced by a number
Possibly missed some free packages due to PICS restart
Recovered {0} package changes at change number {1}
{0} will be replaced by a number, {1} will be replaced by a number
No packages found
No apps found
Failed to get app list
Free Packages plugin not enabled
Failed to parse data from ASFInfo
Removing {0} packages.
{0} will be replaced by a number
Failed to fetch licenses page
Failed to find any removable packages
Didn't find any free packages to remove
Failed to fetch product info
No packages are being removed
Cancelled {0} removals.
{0} will be replaced by a number
Looking for free packages to remove, this scan will take ~{0} minutes. Cancel at any time using: {1}
{0} will be replaced by a number, {1} will be replaced with a command
Found {0} packages to remove:
{1}
To cancel, use: {2}
To continue with removal, use: {3}
To remove items from the list above before continuing, use: {4}
{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command
Didn't find any unwanted free packages to remove
Scan for unwanted free packages cancelled.
Already scanning for unwanted free packages
{0} was not in the list of scanned packages
{0} will be replaced by a packageID
{0} removed
{0} will be replaced by a packageID
Cancelling scan for unwanted free packages...
You must first scan for unwanted free packages using the command: {0}
{0} will be replaced by a command
Pausing package removals until {0}
{0} will be replaced by a time
Failed to get playtime information
Unwanted
================================================
FILE: FreePackages/Localization/Strings.ru-RU.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Не удалось получить данные о значках для бесплатных пакетов
Не удалось собрать данные о значках для бесплатных пакетов
Значение фильтра PlaytestMode было автоматически заменено на "0" (игнорировать всё), так как его может использовать только один бот
Очередь пуста
Бесплатных пакетов удалено: {0}.
{0} will be replaced by a number
Обнаруженных приложений удалено: {0}.
{0} will be replaced by a number
Обнаруженных пакетов удалено: {0}.
{0} will be replaced by a number
Добавлено в очередь обнаруженных приложений: {0}
{0} will be replaced by an appID
Добавлено в очередь обнаруженных пакетов: {0}
{0} will be replaced by a subID
Добавлено в очередь бесплатных пакетов: {0}
{0} will be replaced by an appID
Добавлено в очередь бесплатных пакетов: {0}
{0} will be replaced by a subID
Активация бесплатных пакетов приостановлена до {0}
{0} will be replaced by a time
Плагин "Free Package" превысил лимит активаций
Заменено на {0}
{0} will be replaced by a number
Неизвестно
Некорректно
Неудачно
Ожидание
Бесплатных пакетов в очереди: {0}. Активаций использовано: {1}/{2}.
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
Активации приостановлены, поскольку аккаунт используется для игры.
Активации продолжатся в {0}.
{0} will be replaced by a time
Обнаружены, но ещё не обработаны приложения в количестве "{0}" и пакеты в количестве "{1}".
{0} will be replaced by a number, {1} will be replaced by a number
PICS перезапущен. Переход с номера изменения {0} на {1}
{0} will be replaced by a number, {1} will be replaced by a number
Возможно, пропущены некоторые бесплатные приложения из-за перезагрузки PICS
Восстановлены изменения приложений в количестве: "{0}" под изменённым номером {1}
{0} will be replaced by a number, {1} will be replaced by a number
Возможно, пропущены некоторые бесплатные пакеты из-за перезагрузки PICS
Восстановлены изменения пакетов в количестве: "{0}" под изменённым номером {1}
{0} will be replaced by a number, {1} will be replaced by a number
Пакеты не обнаружены
Приложения не найдены
Не удалось получить список приложений
Плагин "Free Packages" не включён
Не удалось собрать данные из ASFInfo
Удалено пакетов: {0}.
{0} will be replaced by a number
Не удалось получить страницу лицензий
Не удалось найти ни одного удаляемого пакета
Не найдены бесплатные пакеты для удаления
Не удалось получить информацию о товаре
Ни один пакет не был удалён
Отменённых удалений: {0}.
{0} will be replaced by a number
Поиск бесплатных пакетов для удаления, сканирование займёт минут примерно: {0}. Можно отменить в любой момент командой: {1}
{0} will be replaced by a number, {1} will be replaced with a command
Найдено пакетов для удаления - {0}:
{1}
Для отмены, используйте команду: {2}
Для продолжения удаления, используйте: {3}
Для удаления элементов из вышестоящего списка, прежде чем продолжить, используйте команду: {4}
{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command
Не найдены нежелательные бесплатные пакеты для удаления
Отменено сканирование нежелательных бесплатных пакетов.
Уже идёт сканирование нежелательных бесплатных пакетов
Отсутствует в списке отсканированных пакетов: {0}
{0} will be replaced by a packageID
Удалено: {0}
{0} will be replaced by a packageID
Отмена сканирования нежелательных бесплатных пакетов...
Сначала необходимо просканировать нежелательные бесплатные пакеты с помощью команды: {0}
{0} will be replaced by a command
Удаление пакетов приостановлено до {0}
{0} will be replaced by a time
Не удалось получить информацию о наигранном времени
================================================
FILE: FreePackages/Localization/Strings.tr-TR.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Ücretsiz paketlerin rozet verileri alınamadı
Ücretsiz paketlerin rozet verileri çözümlenemedi
PlaytestMode 0 (Yok) olarak değiştirildi, bu filtreyi yalnızca 1 bot kullanabilir
Kuyruk boş
{0} ücretsiz paket kaldırıldı.
{0} will be replaced by a number
{0} keşfedilen uygulama kaldırıldı.
{0} will be replaced by a number
{0} keşfedilen paket kaldırıldı.
{0} will be replaced by a number
{0} keşfedilen uygulamalar kuyruğuna eklendi
{0} will be replaced by an appID
{0} keşfedilen paketler kuyruğuna eklendi
{0} will be replaced by a subID
{0} ücretsiz paket kuyruğuna eklendi
{0} will be replaced by an appID
{0} ücretsiz paket kuyruğuna eklendi
{0} will be replaced by a subID
Ücretsiz paket etkinleştirmeleri {0} tarihine kadar duraklatıldı
{0} will be replaced by a time
Ücretsiz Paket hız limiti aşıldı
{0} ile değiştirildi
{0} will be replaced by a number
Bilinmiyor
Geçersiz
Başarısız
Beklemeye alındı
{0} ücretsiz paket kuyrukta. {1}/{2} etkinleştirme kullanıldı.
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
Etkinleştirmeler, hesap oyun oynarken duraklatıldı.
Etkinleştirmeler {0} tarihinde yeniden başlayacak.
{0} will be replaced by a time
{0} uygulama ve {1} paket keşfedildi ancak henüz işlenmedi.
{0} will be replaced by a number, {1} will be replaced by a number
PICS yeniden başlatıldı, değişiklik numarası {0}'den {1}'e atlanıyor
{0} will be replaced by a number, {1} will be replaced by a number
PICS yeniden başlatması nedeniyle bazı ücretsiz uygulamalar kaçırılmış olabilir
{0} uygulama değişikliği, değişiklik numarası {1}'de kurtarıldı
{0} will be replaced by a number, {1} will be replaced by a number
PICS yeniden başlatması nedeniyle bazı ücretsiz paketler kaçırılmış olabilir
{0} paket değişikliği, değişiklik numarası {1}'de kurtarıldı
{0} will be replaced by a number, {1} will be replaced by a number
Paket bulunamadı
Uygulama bulunamadı
Uygulama listesi alınamadı
Ücretsiz Paketler eklentisi etkin değil
ASFInfo'dan gelen veriler çözümlenemedi
{0} paket kaldırılıyor.
{0} will be replaced by a number
Lisanslar sayfası alınamadı
Kaldırılabilir paket bulunamadı
Kaldırılacak ücretsiz paket bulunamadı
Ürün bilgileri alınamadı
Kaldırılan paket yok
{0} kaldırma işlemi iptal edildi.
{0} will be replaced by a number
Kaldırılacak ücretsiz paketler aranıyor, bu tarama ~{0} dakika sürecek. Herhangi bir zamanda şu komutla iptal edin: {1}
{0} will be replaced by a number, {1} will be replaced with a command
Kaldırılacak {0} paket bulundu:
{1}
İptal etmek için: {2}
Kaldırmaya devam etmek için: {3}
Devam etmeden önce listedeki öğeleri kaldırmak için: {4}
{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command
Kaldırılacak istenmeyen ücretsiz paket bulunamadı
İstenmeyen ücretsiz paketler için tarama iptal edildi.
Zaten istenmeyen ücretsiz paketler taranıyor
{0} taranan paketler listesinde yoktu
{0} will be replaced by a packageID
{0} kaldırıldı
{0} will be replaced by a packageID
İstenmeyen ücretsiz paketler için tarama iptal ediliyor...
Önce şu komutla ücretsiz paketleri taramanız gerekiyor: {0}
{0} will be replaced by a command
Paket kaldırma işlemleri {0} tarihine kadar duraklatıldı
{0} will be replaced by a time
Oynama süresi bilgisi alınamadı
================================================
FILE: FreePackages/Localization/Strings.uk-UA.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Не вдалося отримати дані о значках для безплатних пакетів
Не вдалося зібрати дані о значках для безплатних пакетів
Значення фільтра PlaytestMode було автоматично замінено на "0" (ігнорувати все), оскільки його може використовувати тільки один бот
Черга порожня
Безплатних пакетів видалено: {0}.
{0} will be replaced by a number
Виявлених застосунків видалено: {0}.
{0} will be replaced by a number
Виявлених пакетів видалено: {0}.
{0} will be replaced by a number
Додано {0} до черги виявлених застосунків
{0} will be replaced by an appID
Додано {0} до черги виявлених пакетів
{0} will be replaced by a subID
Додано {0} до черги безплатних пакетів
{0} will be replaced by an appID
Додано {0} до черги безплатних пакетів
{0} will be replaced by a subID
Призупинення активації безплатних пакетів до {0}
{0} will be replaced by a time
Перевищено ліміт безплатних пакетів
Замінено на {0}
{0} will be replaced by a number
Невідомо
Некоректно
Невдало
У списку очікування
Безплатних пакетів у черзі: {0}. Активацій використано: {1}/{2}.
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
Активації призупинені, оскільки обліковий запис використовується для гри.
Активації поновляться в {0}.
{0} will be replaced by a time
Виявлено {0} застосунків та {1} пакетів, але ще не оброблено.
{0} will be replaced by a number, {1} will be replaced by a number
PICS перезапущено, перейшовши від номера зміни {0} до {1}
{0} will be replaced by a number, {1} will be replaced by a number
Можливо, пропущено деякі безплатні застосунки через перезапуск PICS
Відновлено {0} змін застосунку з номером зміни {1}
{0} will be replaced by a number, {1} will be replaced by a number
Можливо, пропущено деякі безплатні пакети через перезапуск PICS
Відновлено {0} змін пакетів з номером зміни {1}
{0} will be replaced by a number, {1} will be replaced by a number
Пакети не знайдено
Застосунки не знайдено
Не вдалося отримати список застосунків
Плагін Free Packages не ввімкнено
Не вдалося обробити дані з ASFInfo
================================================
FILE: FreePackages/Localization/Strings.zh-Hans.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
获取免费包徽章数据失败
解析免费包徽章数据失败
已将 PlaytestMode 更改为 0 (None),只有一个机器人可使用此过滤器
队列为空
{0} 免费包已移除。
{0} will be replaced by a number
{0} 已发现应用已移除。
{0} will be replaced by a number
{0} 已发现包已移除。
{0} will be replaced by a number
添加 {0} 至已发现应用队列
{0} will be replaced by an appID
添加 {0} 至已发现包队列
{0} will be replaced by a subID
添加 {0} 至免费包队列
{0} will be replaced by an appID
添加 {0} 至免费包队列
{0} will be replaced by a subID
暂停免费包激活,直到 {0}
{0} will be replaced by a time
Free Package 达到速率上限
替换为 {0}
{0} will be replaced by a number
未知
无效
失败
等待列表
{0} 个免费包已添加至队列,{1}/{2} 已激活。
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
因为账户被游戏占用,激活任务已暂停。
激活任务将在 {0} 恢复。
{0} will be replaced by a time
{0} 应用和 {1} 包已发现但尚未处理。
{0} will be replaced by a number, {1} will be replaced by a number
PICS 已重启,跳过更改号 {0} 至 {1}
{0} will be replaced by a number, {1} will be replaced by a number
由于PICS重启,可能忽略了一些免费应用
已恢复 {0} 应用更改,更改编号 {1}
{0} will be replaced by a number, {1} will be replaced by a number
由于PICS重启,可能忽略了一些免费包
已恢复 {0} 包更改,更改号 {1}
{0} will be replaced by a number, {1} will be replaced by a number
未找到包
未找到应用
获取应用列表失败
Free Packages 插件未启用
解析来自 ASFInfo 的数据失败
================================================
FILE: FreePackages/Localization/Strings.zh-Hant.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
獲取免費包徽章資料失敗
解析免費包徽章資料失敗
已將遊戲測試模式更改為 0 (無),只有 1 個機器人可使用此過濾器
佇列為空
{0} 免費包已移除。
{0} will be replaced by a number
{0} 已發現應用已移除。
{0} will be replaced by a number
{0} 已發現包已移除。
{0} will be replaced by a number
新增 {0} 至已發現應用佇列
{0} will be replaced by an appID
新增 {0} 至已發現包佇列
{0} will be replaced by a subID
新增 {0} 至免費包佇列
{0} will be replaced by an appID
新增 {0} 至免費包佇列
{0} will be replaced by a subID
暫停免費包啟用,直到 {0}
{0} will be replaced by a time
FreePackages 的啟用已達到上限
替換為 {0}
{0} will be replaced by a number
未知
無效
失敗
等待清單
{0} 個免費包已新增至佇列,{1}/{2} 已啟用。
{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number
因為帳號被遊戲佔用,啟用任務已暫停。
啟用任務將在 {0} 恢復。
{0} will be replaced by a time
{0} 應用和 {1} 包已發現但尚未處理。
{0} will be replaced by a number, {1} will be replaced by a number
PICS 已重啟,跳過更改編號 {0} 至 {1}
{0} will be replaced by a number, {1} will be replaced by a number
由於 PICS 重啟,可能忽略了一些免費應用
已恢復 {0} 應用更改,更改編號 {1}
{0} will be replaced by a number, {1} will be replaced by a number
由於 PICS 重啟,可能忽略了一些免費包
已恢復 {0} 包更改,更改編號 {1}
{0} will be replaced by a number, {1} will be replaced by a number
未找到包
未找到應用
獲取應用清單失敗
外掛程式 FreePackages 未啟用
解析來自 ASFInfo 的資料失敗
================================================
FILE: FreePackages/PackageFilter/FilterConfig.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FreePackages {
internal sealed class FilterConfig : IJsonOnDeserialized {
[JsonInclude]
internal bool ImportStoreFilters { get; set; } = false;
[JsonInclude]
internal HashSet Types { get; set; } = new();
[JsonInclude]
internal HashSet Categories { get; set; } = new();
[JsonInclude]
internal HashSet Tags { get; set; } = new();
[JsonInclude]
internal HashSet IgnoredTypes { get; set; } = new() {"Demo"};
[JsonInclude]
internal HashSet IgnoredTags { get; set; } = new();
[JsonInclude]
internal HashSet IgnoredCategories { get; set; } = new();
[JsonInclude]
internal HashSet IgnoredContentDescriptors { get; set; } = new();
[JsonInclude]
internal HashSet IgnoredAppIDs { get; set; } = new();
[JsonInclude]
internal bool IgnoreFreeWeekends { get; set; } = false;
[JsonInclude]
internal uint MinReviewScore { get; set; } = 0;
[JsonInclude]
internal HashSet Languages { get; set; } = new();
[JsonInclude]
internal EPlaytestMode PlaytestMode { get; set; } = EPlaytestMode.None;
[JsonInclude]
internal bool RequireAllTags { get; set; } = false;
[JsonInclude]
internal bool RequireAllCategories { get; set; } = false;
[JsonInclude]
internal bool NoCostOnly { get; set; } = false;
[JsonInclude]
internal HashSet Systems { get; set; } = new();
[JsonInclude]
internal bool WishlistOnly { get; set; } = false;
[JsonInclude]
internal uint MinDaysOld { get; set; } = 0; // Not used, only exists as a typo of MaxDaysOld, and is only here to support old configs
[JsonInclude]
internal uint MaxDaysOld { get; set; } = 0;
[JsonConstructor]
internal FilterConfig() { }
public void OnDeserialized() {
// Handles filter config changes made in V1.5.4.10
if (Types.Contains("Demo") && IgnoredTypes.Contains("Demo")) {
IgnoredTypes.Remove("Demo");
}
// Handles filter config changes made in V1.5.5.0
if (MaxDaysOld == 0 && MinDaysOld > 0) {
MaxDaysOld = MinDaysOld;
}
}
public override int GetHashCode() {
int hash = DeterministicHasher.Hash(ImportStoreFilters);
hash = DeterministicHasher.Hash(hash, Types);
hash = DeterministicHasher.Hash(hash, Categories);
hash = DeterministicHasher.Hash(hash, Tags);
hash = DeterministicHasher.Hash(hash, IgnoredTypes);
hash = DeterministicHasher.Hash(hash, IgnoredTags);
hash = DeterministicHasher.Hash(hash, IgnoredCategories);
hash = DeterministicHasher.Hash(hash, IgnoredContentDescriptors);
hash = DeterministicHasher.Hash(hash, IgnoredAppIDs);
hash = DeterministicHasher.Hash(hash, IgnoreFreeWeekends);
hash = DeterministicHasher.Hash(hash, MinReviewScore);
hash = DeterministicHasher.Hash(hash, Languages);
hash = DeterministicHasher.Hash(hash, (int) PlaytestMode);
hash = DeterministicHasher.Hash(hash, RequireAllTags);
hash = DeterministicHasher.Hash(hash, RequireAllCategories);
hash = DeterministicHasher.Hash(hash, NoCostOnly);
hash = DeterministicHasher.Hash(hash, Systems);
hash = DeterministicHasher.Hash(hash, WishlistOnly);
hash = DeterministicHasher.Hash(hash, MaxDaysOld);
return hash;
}
}
[Flags]
internal enum EPlaytestMode : byte {
None = 0,
Unlimited = 1,
Limited = 2,
All = Unlimited | Limited
}
}
================================================
FILE: FreePackages/PackageFilter/Filterables/FilterableApp.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using SteamKit2;
namespace FreePackages {
internal sealed class FilterableApp {
internal FilterableApp? Parent = null;
internal uint? ParentID = null;
internal bool ParentInfoRequired = false; // Whether or not the product info of the parent is needed to apply filters
internal uint ID;
internal EAppType Type;
internal bool IsFreeApp;
internal string? ReleaseState;
internal string? State;
internal uint MustOwnAppToPurchase;
internal List? RestrictedCountries;
internal List? PurchaseRestrictedCountries;
internal bool AllowPurchaseFromRestrictedCountries;
internal List AppTags;
internal List Category;
internal List ContentDescriptors;
internal List SupportedLanguages;
internal uint ReviewScore;
internal string? ListOfDLC;
internal uint PlayTestType;
internal List? OSList;
internal uint DeckCompatibility;
internal DateTime SteamReleaseDate;
internal bool Hidden;
internal FilterableApp(SteamApps.PICSProductInfoCallback.PICSProductInfo productInfo) : this(productInfo.ID, productInfo.KeyValues) {}
internal FilterableApp(KeyValue kv) : this(kv["appid"].AsUnsignedInteger(), kv) {}
internal FilterableApp(uint id, KeyValue kv) {
ID = id;
try {
Type = Enum.Parse(kv["common"]["type"].AsString() ?? EAppType.Invalid.ToString(), true);
} catch {
Type = EAppType.Invalid;
}
IsFreeApp = kv["extended"]["isfreeapp"].AsBoolean();
ReleaseState = kv["common"]["releasestate"].AsString();
State = kv["extended"]["state"].AsString();
MustOwnAppToPurchase = kv["extended"]["mustownapptopurchase"].AsUnsignedInteger();
RestrictedCountries = kv["common"]["restricted_countries"].AsString()?.ToUpper().Split(",").ToList();
PurchaseRestrictedCountries = kv["extended"]["purchaserestrictedcountries"].AsString()?.ToUpper().Split(" ").ToList();
AllowPurchaseFromRestrictedCountries = kv["extended"]["allowpurchasefromrestrictedcountries"].AsBoolean();
AppTags = kv["common"]["store_tags"].Children.Select(tag => tag.AsUnsignedInteger()).ToList();
Category = kv["common"]["category"].Children.Select(category => UInt32.Parse(category.Name!.Substring(9))).ToList(); // category numbers are stored in the name as "category_##"
ContentDescriptors = kv["common"]["content_descriptors"].Children.Select(content_descriptor => content_descriptor.AsUnsignedInteger()).ToList();
SupportedLanguages = kv["common"]["supported_languages"].Children.Select(supported_language => supported_language.Name!).ToList();
ReviewScore = kv["common"]["review_score"].AsUnsignedInteger();
ListOfDLC = kv["extended"]["listofdlc"].AsString();
PlayTestType = kv["extended"]["playtest_type"].AsUnsignedInteger();
OSList = kv["common"]["oslist"].AsString()?.ToUpper().Split(",").ToList();
DeckCompatibility = kv["common"]["steam_deck_compatibility"]["category"].AsUnsignedInteger();
SteamReleaseDate = DateTimeOffset.FromUnixTimeSeconds(kv["common"]["steam_release_date"].AsUnsignedInteger()).UtcDateTime;
Hidden = kv["common"] == KeyValue.Invalid;
// Fix the category for games which do have trading cards, but which don't have the trading card category, Ex: https://steamdb.info/app/316260/
if (CardApps.AppIDs.Contains(ID) && !Category.Contains(29)) {
Category.Add(29);
}
{
uint parentID = 0;
if (Type == EAppType.Beta) {
// This is generally less reliable than ["common"]["parent"] (Ex: https://steamdb.info/app/2420490/ on Oct 17 2023 has "parent" and is redeemable, but doesn't have "betaforappid")
parentID = kv["extended"]["betaforappid"].AsUnsignedInteger();
}
if (parentID == 0) {
parentID = kv["common"]["parent"].AsUnsignedInteger();
}
if (parentID > 0) {
ParentID = parentID;
}
// I only want product info for parents of playtests and demos (because they share a store page with their parents and so should inherit some of their parents properties)
if (Type == EAppType.Beta || Type == EAppType.Demo) {
ParentInfoRequired = true;
}
}
}
internal static async Task?> GetFilterables(List productInfos, Func? onNonFreeApp = null) {
var appProductInfos = productInfos.SelectMany(static result => result.Apps.Values);
if (appProductInfos.Count() == 0) {
return [];
}
List apps = appProductInfos.Select(x => new FilterableApp(x)).ToList();
// Filter out non-free apps
apps.RemoveAll(app => {
if (!app.IsFree() || !app.IsAvailable()) {
if (onNonFreeApp?.Invoke(app) == false) {
return false;
}
return true;
}
return false;
});
// Get the parents of the free apps
HashSet parentIDs = apps.Where(app => app.ParentInfoRequired && app.ParentID != null).Select(app => app.ParentID!.Value).ToHashSet();
var parentProductInfos = (await ProductInfo.GetProductInfo(appIDs: parentIDs).ConfigureAwait(false))?.SelectMany(static result => result.Apps.Values);
if (parentProductInfos == null) {
ASF.ArchiLogger.LogNullError(parentProductInfos);
return null;
}
if (parentProductInfos.Count() > 0) {
apps.ForEach(app => {
if (app.ParentInfoRequired && app.ParentID != null) {
app.AddParent(parentProductInfos.FirstOrDefault(parent => parent.ID == app.ParentID));
}
});
}
return apps;
}
internal void AddParent(SteamApps.PICSProductInfoCallback.PICSProductInfo? productInfo) => AddParent(productInfo?.ID, productInfo?.KeyValues);
internal void AddParent(KeyValue? kv) => AddParent(kv?["appid"].AsUnsignedInteger(), kv);
internal void AddParent(uint? id, KeyValue? kv) {
if (!ParentInfoRequired || id == null || kv == null) {
return;
}
Parent = new FilterableApp(id.Value, kv);
}
internal bool IsFree() {
if (IsFreeApp) {
return true;
}
if (Type == EAppType.Demo) {
return true;
}
// Playtest
if (Type == EAppType.Beta) {
return true;
}
return false;
}
internal bool IsAvailable() {
string[] availableReleaseStates = ["released", "preloadonly"];
string[] availableStates = ["eStateAvailable"];
if (!availableReleaseStates.Contains(ReleaseState) && !availableStates.Contains(State)) {
// App not released yet
// Note: There's another seemingly relevant field: kv["common"]["steam_release_date"]
// steam_release_date is not checked because an app can be "released", still have a future release date, and still be redeemed
// Example: https://steamdb.info/changelist/20505012/
return false;
}
return true;
}
internal bool HasID(IEnumerable ids) {
if (ids.Count() == 0) {
return false;
}
if (ids.Contains(ID)) {
return true;
}
// Parent IDs are also used for filtering as only playtests and demos have parents right now
// I figure if someone doesn't want a certain app, then they also don't want the demo or playtest version of that app
if (Parent != null && ids.Contains(Parent.ID)) {
return true;
}
return false;
}
internal bool HasType(IEnumerable types) {
if (types.Count() == 0) {
return false;
}
return types.Contains(Type.ToString(), StringComparer.OrdinalIgnoreCase);
}
internal bool HasTag(IEnumerable tags, bool requireAll = false) {
if (tags.Count() == 0) {
return false;
}
if ((!requireAll && AppTags.Any(tag => tags.Contains(tag)))
|| (requireAll && tags.All(tag => AppTags.Contains(tag)))
) {
return true;
}
// Also check parent app, because parents can have additional tags defined
if (Parent != null && (
(!requireAll && Parent.AppTags.Any(tag => tags.Contains(tag)))
|| (requireAll && tags.All(tag => Parent.AppTags.Contains(tag)))
)) {
return true;
}
return false;
}
internal bool HasCategory(IEnumerable categories, bool requireAll = false) {
if (categories.Count() == 0) {
return false;
}
if ((!requireAll && Category.Any(category => categories.Contains(category)))
|| (requireAll && categories.All(category => Category.Contains(category)))
) {
return true;
}
// Only use parent categories if the app has no categories of its own. Ex: Tekken 8 playtest (https://steamdb.info/app/2385860/).
// This may lead to unintended fitlering, but not doing it may also lead to unintended filtering.
// Don't use parent categories if the app has categories of its own defined, but the parent has more.
// It could be that the parent naturally has more categories, for example a demo without achievement and a parent with achievements.
if (Category.Count == 0 && Parent != null && (
(!requireAll && Parent.Category.Any(category => categories.Contains(category)))
|| (requireAll && categories.All(category => Parent.Category.Contains(category)))
)) {
return true;
}
return false;
}
internal bool HasContentDescriptor(IEnumerable content_descriptors) {
if (content_descriptors.Count() == 0) {
return false;
}
if (ContentDescriptors.Any(content_descriptor => content_descriptors.Contains(content_descriptor))) {
return true;
}
// Also check parent app, because parents may have additional descriptors defined
if (Parent != null && Parent.ContentDescriptors.Any(content_descriptor => content_descriptors.Contains(content_descriptor))) {
return true;
}
return false;
}
internal bool HasLanguage(IEnumerable languages) {
if (languages.Count() == 0) {
return false;
}
if (SupportedLanguages.Any(language => languages.Contains(language, StringComparer.OrdinalIgnoreCase))) {
return true;
}
// Only check the parent's languages if the app has no languages of its own
// Most playtests don't list supported languages, in which case we do want to use the parent's languages (ex: Tekken 8 playtest https://steamdb.info/app/2385860/)
// Don't check the parent's langauge if the app has languages of its own, but the parent has more.
// It could be that the parent app naturally has more language support, in demos for example (ex: Grounded Demo supports only English while the full release supports more languages https://steamdb.info/app/1316010/ , https://steamcommunity.com/app/962130/discussions/0/2440336502396337163/)
if (SupportedLanguages.Count == 0 && Parent != null && Parent.SupportedLanguages.Any(language => languages.Contains(language, StringComparer.OrdinalIgnoreCase))) {
return true;
}
return false;
}
internal bool HasSystem(IEnumerable systems) {
if (systems.Count() == 0) {
return false;
}
if (OSList != null && OSList.Any(system => systems.Contains(system, StringComparer.OrdinalIgnoreCase))) {
return true;
}
if (DeckCompatibility == 3 && systems.Contains("DeckVerified", StringComparer.OrdinalIgnoreCase)) {
return true;
}
if (DeckCompatibility == 2 && systems.Contains("DeckPlayable", StringComparer.OrdinalIgnoreCase)) {
return true;
}
if (DeckCompatibility == 1 && systems.Contains("DeckUnsupported", StringComparer.OrdinalIgnoreCase)) {
return true;
}
if (DeckCompatibility == 0 && systems.Contains("DeckUnknown", StringComparer.OrdinalIgnoreCase)) {
return true;
}
return false;
}
}
}
================================================
FILE: FreePackages/PackageFilter/Filterables/FilterablePackage.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using SteamKit2;
namespace FreePackages {
internal sealed class FilterablePackage {
internal List PackageContents = new();
internal HashSet PackageContentIDs;
internal HashSet PackageContentParentIDs = new();
internal uint ID;
internal EBillingType BillingType;
internal EPackageStatus Status;
internal ELicenseType LicenseType;
internal bool DeactivatedDemo;
internal ulong ExpiryTime;
internal ulong StartTime;
internal uint DontGrantIfAppIDOwned;
internal uint MustOwnAppToPurchase;
internal List? RestrictedCountries;
internal bool OnlyAllowRestrictedCountries;
internal List? PurchaseRestrictedCountries;
internal bool AllowPurchaseFromRestrictedCountries;
internal bool FreeWeekend;
internal bool BetaTesterPackage;
internal FilterablePackage(SteamApps.PICSProductInfoCallback.PICSProductInfo productInfo) : this(productInfo.ID, productInfo.KeyValues) {}
internal FilterablePackage(KeyValue kv) : this(Convert.ToUInt32(kv.Name), kv) {}
internal FilterablePackage(uint id, KeyValue kv) {
ID = id;
PackageContentIDs = kv["appids"].Children.Select(x => x.AsUnsignedInteger()).ToHashSet();
BillingType = (EBillingType) kv["billingtype"].AsInteger();
Status = (EPackageStatus) kv["status"].AsInteger();
LicenseType = (ELicenseType) kv["licensetype"].AsInteger();
DeactivatedDemo = kv["extended"]["deactivated_demo"].AsBoolean();
ExpiryTime = kv["extended"]["expirytime"].AsUnsignedLong();
StartTime = kv["extended"]["starttime"].AsUnsignedLong();
DontGrantIfAppIDOwned = kv["extended"]["dontgrantifappidowned"].AsUnsignedInteger();
MustOwnAppToPurchase = kv["extended"]["mustownapptopurchase"].AsUnsignedInteger();
RestrictedCountries = kv["extended"]["restrictedcountries"].AsString()?.ToUpper().Split(" ").ToList();
OnlyAllowRestrictedCountries = kv["extended"]["onlyallowrestrictedcountries"].AsBoolean();
PurchaseRestrictedCountries = kv["extended"]["purchaserestrictedcountries"].AsString()?.ToUpper().Split(" ").ToList();
AllowPurchaseFromRestrictedCountries = kv["extended"]["allowpurchasefromrestrictedcountries"].AsBoolean();
FreeWeekend = kv["extended"]["freeweekend"].AsBoolean();
BetaTesterPackage = kv["extended"]["betatesterpackage"].AsBoolean();
}
internal static async Task?> GetFilterables(List productInfos, Func? onNonFreePackage = null, CancellationToken? cancellationToken = null) {
var packageProductInfos = productInfos.SelectMany(static result => result.Packages.Values);
if (packageProductInfos.Count() == 0) {
return [];
}
List packages = packageProductInfos.Select(x => new FilterablePackage(x)).ToList();
// Filter out non-free, non-new packages
packages.RemoveAll(package => {
if (!package.IsFree() || !package.IsAvailable()) {
if (onNonFreePackage?.Invoke(package) == false) {
return false;
}
return true;
}
return false;
});
// Get the apps contained in each package
HashSet packageContentsIDs = packages.SelectMany(package => package.PackageContentIDs).ToHashSet();
var packageContentProductInfos = (await ProductInfo.GetProductInfo(appIDs: packageContentsIDs, cancellationToken: cancellationToken).ConfigureAwait(false))?.SelectMany(static result => result.Apps.Values);
if (packageContentProductInfos == null) {
ASF.ArchiLogger.LogNullError(packageContentProductInfos);
return null;
}
packages.ForEach(package => package.AddPackageContents(packageContentProductInfos.Where(x => package.PackageContentIDs.Contains(x.ID))));
// Filter out any packages which contain unavailable apps
packages.RemoveAll(package => {
if (!package.IsAvailablePackageContents() && package.BillingType != EBillingType.NoCost) {
// Ignore this check for NoCost packages; assume that everything is available
// Ex: https://steamdb.info/sub/1011710 is redeemable even though it contains https://steamdb.info/app/235901/ (which as of Feb 12 2024 is some unknown app)
if (onNonFreePackage?.Invoke(package) == false) {
return false;
}
return true;
}
return false;
});
// Get the parents for the apps in each package
HashSet parentIDs = packages.SelectMany(package => package.PackageContentParentIDs).ToHashSet();
var parentProductInfos = (await ProductInfo.GetProductInfo(appIDs: parentIDs, cancellationToken: cancellationToken).ConfigureAwait(false))?.SelectMany(static result => result.Apps.Values);
if (parentProductInfos == null) {
ASF.ArchiLogger.LogNullError(parentProductInfos);
return null;
}
if (parentProductInfos.Count() > 0) {
packages.ForEach(package => {
if (package.PackageContentParentIDs.Count != 0) {
package.AddPackageContentParents(parentProductInfos.Where(parent => package.PackageContentParentIDs.Contains(parent.ID)));
}
});
}
return packages;
}
internal void AddPackageContents(IEnumerable productInfos) => AddPackageContents(productInfos.Select(productInfo => (productInfo.ID, productInfo.KeyValues)));
internal void AddPackageContents(IEnumerable kvs) => AddPackageContents(kvs.Select(kv => (kv["appid"].AsUnsignedInteger(), kv)));
internal void AddPackageContents(IEnumerable<(uint id, KeyValue kv)> packageContents) {
PackageContents = packageContents.Select(packageContent => new FilterableApp(packageContent.id, packageContent.kv)).ToList();
PackageContentParentIDs = PackageContents.Where(app => app.ParentInfoRequired && app.ParentID != null).Select(app => app.ParentID!.Value).ToHashSet();
}
internal void AddPackageContentParents(IEnumerable productInfos) => AddPackageContentParents(productInfos.Select(productInfo => (productInfo.ID, productInfo.KeyValues)));
internal void AddPackageContentParents(IEnumerable kvs) => AddPackageContentParents(kvs.Select(kv => (kv["appid"].AsUnsignedInteger(), kv)));
internal void AddPackageContentParents(IEnumerable<(uint id, KeyValue kv)> parents) {
PackageContents.ForEach(app => {
if (app.ParentInfoRequired && app.ParentID != null) {
try {
var parent = parents.First(parent => parent.id == app.ParentID);
app.AddParent(parent.id, parent.kv);
} catch (Exception) {
// Ignore missing parent exception
}
}
});
}
internal bool IsFree() {
if (BillingType == EBillingType.FreeOnDemand || BillingType == EBillingType.NoCost) {
return true;
}
return false;
}
internal bool IsAvailable() {
if (PackageContentIDs.Count == 0) {
// Package has no apps
return false;
}
if (Status != EPackageStatus.Available) {
// Package is unavailable
return false;
}
if (LicenseType != ELicenseType.SinglePurchase) {
// Wrong license type
return false;
}
if (ExpiryTime > 0 && ExpiryTime < DateUtils.DateTimeToUnixTime(DateTime.UtcNow)) {
// Package was only available for a limited time and is no longer available
return false;
}
if (DeactivatedDemo) {
// Demo package has been disabled
return false;
}
if (BetaTesterPackage) {
// Playtests can't be activated through packages
return false;
}
if (ID == 17906) {
// Special case: Anonymous Dedicated Server Comp (https://steamdb.info/sub/17906/)
// This always returns AccessDenied/InvalidPackage
return false;
}
return true;
}
internal bool IsAvailablePackageContents() {
if (PackageContentIDs.Count != PackageContents.Count) {
// Could not find all of the apps for this package
return false;
}
if (PackageContents.Any(app => !app.IsAvailable())) {
// At least one of the apps in this package isn't available
return false;
}
return true;
}
}
}
================================================
FILE: FreePackages/PackageFilter/PackageFilter.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using SteamKit2;
namespace FreePackages {
internal sealed class PackageFilter {
private readonly BotCache BotCache;
internal readonly List FilterConfigs;
internal readonly int Hash;
private HashSet? OwnedAppIDs = null;
private Steam.UserData? UserData = null;
private HashSet ImportedIgnoredAppIDs = new();
private HashSet ImportedIgnoredTags = new();
private HashSet ImportedIgnoredContentDescriptors = new();
internal string? Country = null;
internal bool Ready { get { return OwnedAppIDs != null && Country != null && UserData != null; }}
internal PackageFilter(BotCache botCache, List