├── .deployer.template.yaml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── docs ├── defs │ ├── app.v2.200.yaml │ ├── default-error.yaml │ ├── dlcinfo.v2.200.yaml │ ├── early-access.v1.yaml │ ├── market.cards.average-prices.v2.200.yaml │ ├── market.cards.v2.200.yaml │ ├── prices.v2.200.yaml │ ├── profile.background.games.v2.200.yaml │ ├── profile.background.list.v2.200.yaml │ ├── profile.v2.200.yaml │ ├── rates.v1.200.yaml │ ├── similar.v2.200.yaml │ └── twitch-stream.v2.yaml ├── examples │ ├── app.v2.success.json │ ├── dlcinfo.v2.success.json │ ├── early-access.v1.success.json │ ├── market.cards.average-prices.v2.success.json │ ├── market.cards.v2.success.json │ ├── prices.v2.success.json │ ├── profile.background.games.v2.success.json │ ├── profile.background.list.v2.success.json │ ├── rates.v1.success.json │ └── similar.v2.success.json ├── openapi.yaml └── schemas │ ├── obj.currency-code.yaml │ ├── obj.drm.yaml │ ├── obj.platform.yaml │ ├── obj.price.yaml │ └── obj.shop.yaml ├── install.sql ├── migration.sql ├── package-lock.json ├── package.json ├── phpstan.neon ├── routing.php └── src ├── Config ├── BrightDataConfig.php └── CoreConfig.php ├── Controllers ├── AppController.php ├── Controller.php ├── DLCController.php ├── EarlyAccessController.php ├── MarketController.php ├── PricesController.php ├── ProfileController.php ├── ProfileManagementController.php ├── RatesController.php ├── SimilarController.php └── TwitchController.php ├── Cron ├── CronJob.php └── CronJobFactory.php ├── Data ├── Interfaces │ ├── AppData │ │ ├── AppDataProviderInterface.php │ │ ├── HLTBProviderInterface.php │ │ ├── PlayersProviderInterface.php │ │ ├── ReviewsProviderInterface.php │ │ ├── SteamPeekProviderInterface.php │ │ └── WSGFProviderInterface.php │ ├── EarlyAccessProviderInterface.php │ ├── ExfglsProviderInterface.php │ ├── PricesProviderInterface.php │ ├── RatesProviderInterface.php │ ├── SteamRepProviderInterface.php │ └── TwitchProviderInterface.php ├── Managers │ ├── Market │ │ ├── ERarity.php │ │ ├── EType.php │ │ ├── MarketIndex.php │ │ └── MarketManager.php │ ├── SteamRepManager.php │ └── UserManager.php ├── Objects │ ├── DSteamRep.php │ ├── HLTB.php │ ├── Players.php │ ├── Prices.php │ ├── Reviews │ │ ├── Review.php │ │ └── Reviews.php │ ├── SteamPeak │ │ ├── SteamPeekGame.php │ │ └── SteamPeekResults.php │ ├── TwitchStream.php │ └── WSGF.php ├── Providers │ ├── EarlyAccessProvider.php │ ├── ExfglsProvider.php │ ├── HLTBProvider.php │ ├── PlayersProvider.php │ ├── PricesProvider.php │ ├── RatesProvider.php │ ├── ReviewsProvider.php │ ├── SteamPeekProvider.php │ ├── SteamRepProvider.php │ ├── TwitchProvider.php │ └── WSGFProvider.php └── Updaters │ ├── Market │ └── MarketCrawler.php │ └── Rates │ └── RatesUpdater.php ├── Database ├── DBadges.php ├── DCurrency.php ├── DDlcCategories.php ├── DMarketData.php ├── DMarketIndex.php ├── DSession.php ├── DUsersProfiles.php ├── TBadges.php ├── TCache.php ├── TCurrency.php ├── TDlcCategories.php ├── TGameDlc.php ├── THLTB.php ├── TMarketData.php ├── TMarketIndex.php ├── TSessions.php ├── TSteamRep.php ├── TUsersBadges.php └── TUsersProfiles.php ├── Endpoints ├── EndpointBuilder.php ├── EndpointsConfig.php └── KeysConfig.php ├── Environment ├── Container.php └── Lock.php ├── Exceptions ├── ApiException.php ├── InvalidValueException.php └── MissingParameterException.php ├── Lib ├── Cache │ ├── Cache.php │ ├── CacheInterface.php │ ├── DCache.php │ └── ECacheKey.php ├── Http │ ├── BoolParam.php │ ├── IntParam.php │ ├── ListParam.php │ └── StringParam.php ├── Loader │ ├── Crawler.php │ ├── Item.php │ ├── Loader.php │ ├── Proxy │ │ ├── BrightDataProxy.php │ │ ├── ProxyFactory.php │ │ ├── ProxyFactoryInterface.php │ │ └── ProxyInterface.php │ └── SimpleLoader.php ├── Logging │ ├── LoggerFactory.php │ ├── LoggerFactoryInterface.php │ └── LoggingConfig.php ├── Money │ └── CurrencyConverter.php ├── OpenId │ ├── OpenId.php │ ├── OpenIdProvider.php │ └── Session.php └── Redis │ ├── ERedisKey.php │ ├── RedisClient.php │ └── RedisConfig.php ├── Routing ├── Middleware │ ├── AccessLogMiddleware.php │ └── IpThrottleMiddleware.php ├── Router.php └── Strategy │ └── ApiStrategy.php ├── bootstrap.php └── job.php /.deployer.template.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | repository: "" 3 | hosts: 4 | prod: 5 | hostname: "" 6 | port: 0 7 | remote_user: "" 8 | identity_file: "" 9 | deploy_path: "" 10 | branch: master 11 | ssh_multiplexing: false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .configs/ 3 | bin/ 4 | vendor/ 5 | logs/ 6 | temp/ 7 | .nginx* 8 | deployer.yaml 9 | node_modules/ 10 | dist/ 11 | build/ 12 | cron.table 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 IsThereAnyDeal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Augmented Steam API Server 2 | 3 | An Augmented Steam server, providing data via API for Augmented Steam extension. 4 | 5 | Some API endpoints require data from 3rd party services, you may need to provide your own configuration for these, 6 | see Config objects for required config schema. 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs": "redocly preview-docs docs/openapi.yaml", 4 | "build": "vendor/bin/deby --config build/build.php build", 5 | "deploy": "vendor/bin/deby --config build/build.php deploy@server", 6 | "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 1G" 7 | }, 8 | "autoload": { 9 | "psr-4": { 10 | "AugmentedSteam\\Server\\": "src" 11 | } 12 | }, 13 | "require": { 14 | "ext-json": "*", 15 | "ext-simplexml": "*", 16 | "ext-curl": "*", 17 | "ext-dom": "*", 18 | "ext-libxml": "*", 19 | "ext-openssl": "*", 20 | "itad/config": "*@dev", 21 | "itad/db": "*@dev", 22 | "league/route": "^6.2", 23 | "guzzlehttp/guzzle": "^7.8", 24 | "monolog/monolog": "^3.5", 25 | "laminas/laminas-diactoros": "^3.3", 26 | "laminas/laminas-httphandlerrunner": "^2.1", 27 | "php-di/php-di": "^7.0", 28 | "nette/schema": "^1.2", 29 | "league/uri": "^7.3", 30 | "league/uri-components": "^7.4", 31 | "filp/whoops": "^2.15", 32 | "sentry/sdk": "^4.0", 33 | "predis/predis": "^2.2", 34 | "php-sage/sage": "^1.5" 35 | }, 36 | "require-dev": { 37 | "phpstan/phpstan": "^1.7", 38 | "phpstan/phpstan-deprecation-rules": "^1.1", 39 | "itad/deby": "*@dev" 40 | }, 41 | "repositories": [ 42 | { 43 | "type": "vcs", 44 | "url": "git@github.com:IsThereAnyDeal/Config.git" 45 | }, 46 | { 47 | "type": "vcs", 48 | "url": "git@github.com:IsThereAnyDeal/Database.git" 49 | }, 50 | { 51 | "type": "vcs", 52 | "url": "git@github.com:IsThereAnyDeal/Deby.git" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /docs/defs/app.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | required: 7 | - family_sharing 8 | - players 9 | - wsgf 10 | - hltb 11 | - reviews 12 | properties: 13 | family_sharing: 14 | type: boolean 15 | players: 16 | type: object 17 | required: [recent, peak_today, peak_all] 18 | properties: 19 | recent: 20 | type: 21 | - integer 22 | peak_today: 23 | type: 24 | - integer 25 | peak_all: 26 | type: 27 | - integer 28 | wsgf: 29 | type: 30 | - object 31 | - 'null' 32 | required: [url, wide, ultrawide, multi_monitor, 4k] 33 | properties: 34 | url: 35 | type: string 36 | wide: 37 | type: string 38 | ultrawide: 39 | type: string 40 | multi_monitor: 41 | type: string 42 | 4k: 43 | type: string 44 | hltb: 45 | type: 46 | - object 47 | - 'null' 48 | required: [story, extras, complete, url] 49 | properties: 50 | story: 51 | type: 52 | - integer 53 | - 'null' 54 | extras: 55 | type: 56 | - integer 57 | - 'null' 58 | complete: 59 | type: 60 | - integer 61 | - 'null' 62 | url: 63 | type: 64 | - string 65 | reviews: 66 | type: object 67 | required: 68 | - metauser 69 | - opencritic 70 | properties: 71 | metauser: 72 | type: [object, 'null'] 73 | required: [score, verdict, url] 74 | properties: 75 | score: 76 | type: [integer, 'null'] 77 | verdict: 78 | type: [string, 'null'] 79 | url: 80 | type: string 81 | opencritic: 82 | type: [object, 'null'] 83 | required: [score, verdict, url] 84 | properties: 85 | score: 86 | type: [integer, 'null'] 87 | verdict: 88 | type: [string, 'null'] 89 | url: 90 | type: string 91 | examples: 92 | success: 93 | value: 94 | $ref: "../examples/app.v2.success.json" 95 | -------------------------------------------------------------------------------- /docs/defs/default-error.yaml: -------------------------------------------------------------------------------- 1 | description: Error response 2 | content: 3 | "application/json": 4 | schema: 5 | type: object 6 | required: [status_code, reason_phrase] 7 | properties: 8 | result: 9 | type: string 10 | enum: 11 | - error 12 | error: 13 | type: string 14 | enum: 15 | - missing_param 16 | - invalid_value 17 | error_description: 18 | type: string 19 | required: false 20 | status_code: 21 | type: integer 22 | reason_phrase: 23 | type: string 24 | -------------------------------------------------------------------------------- /docs/defs/dlcinfo.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: array 6 | items: 7 | type: object 8 | required: [id, name, description, icon] 9 | properties: 10 | id: 11 | type: integer 12 | name: 13 | type: string 14 | description: 15 | type: string 16 | icon: 17 | type: string 18 | examples: 19 | success: 20 | summary: "Example success response" 21 | value: 22 | $ref: "../examples/dlcinfo.v2.success.json" 23 | -------------------------------------------------------------------------------- /docs/defs/early-access.v1.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: array 6 | items: 7 | type: integer 8 | examples: 9 | success: 10 | value: 11 | $ref: "../examples/early-access.v1.success.json" 12 | -------------------------------------------------------------------------------- /docs/defs/market.cards.average-prices.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | propertyNames: 7 | pattern: ^\d+$ 8 | additionalProperties: 9 | type: object 10 | required: [regular, foil] 11 | properties: 12 | regular: 13 | type: number 14 | foil: 15 | type: number 16 | examples: 17 | success: 18 | value: 19 | $ref: "../examples/market.cards.average-prices.v2.success.json" 20 | -------------------------------------------------------------------------------- /docs/defs/market.cards.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | propertyNames: 7 | type: string 8 | additionalProperties: 9 | type: object 10 | required: [img, url, price] 11 | properties: 12 | img: 13 | type: string 14 | url: 15 | type: string 16 | price: 17 | type: number 18 | examples: 19 | success: 20 | value: 21 | $ref: "../examples/market.cards.v2.success.json" 22 | -------------------------------------------------------------------------------- /docs/defs/prices.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: OK 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | required: 7 | - prices 8 | - bundles 9 | properties: 10 | prices: 11 | type: object 12 | propertyNames: 13 | type: string 14 | required: [current, lowest, bundled, urls] 15 | properties: 16 | current: 17 | type: [object, 'null'] 18 | required: 19 | - shop 20 | - price 21 | - regular 22 | - cut 23 | - voucher 24 | - flag 25 | - drm 26 | - platforms 27 | - timestamp 28 | - expiry 29 | - url 30 | properties: 31 | shop: 32 | $ref: "../schemas/obj.shop.yaml" 33 | price: 34 | $ref: "../schemas/obj.price.yaml" 35 | regular: 36 | $ref: "../schemas/obj.price.yaml" 37 | cut: 38 | type: integer 39 | voucher: 40 | type: [string, 'null'] 41 | flag: 42 | type: [string, null] 43 | drm: 44 | type: array 45 | items: 46 | $ref: "../schemas/obj.drm.yaml" 47 | platforms: 48 | type: array 49 | items: 50 | $ref: "../schemas/obj.drm.yaml" 51 | timestamp: 52 | type: string 53 | expiry: 54 | type: [string, 'null'] 55 | url: 56 | type: string 57 | lowest: 58 | type: [object, 'null'] 59 | required: 60 | - shop 61 | - price 62 | - regular 63 | - cut 64 | - timestamp 65 | - expiry 66 | - url 67 | properties: 68 | shop: 69 | $ref: "../schemas/obj.shop.yaml" 70 | price: 71 | $ref: "../schemas/obj.price.yaml" 72 | regular: 73 | $ref: "../schemas/obj.price.yaml" 74 | cut: 75 | type: integer 76 | timestamp: 77 | type: string 78 | expiry: 79 | type: [string, 'null'] 80 | url: 81 | type: string 82 | bundled: 83 | type: number 84 | min: 0 85 | urls: 86 | type: [object, 'null'] 87 | required: 88 | - info 89 | - history 90 | properties: 91 | info: 92 | type: string 93 | history: 94 | type: string 95 | bundles: 96 | type: array 97 | items: 98 | type: object 99 | required: 100 | - id 101 | - title 102 | - page 103 | - url 104 | - details 105 | - isMature 106 | - publish 107 | - expiry 108 | - counts 109 | - tiers 110 | properties: 111 | id: 112 | type: number 113 | title: 114 | type: string 115 | page: 116 | type: object 117 | required: 118 | - id 119 | - name 120 | properties: 121 | id: 122 | type: integer 123 | name: 124 | type: string 125 | url: 126 | type: string 127 | details: 128 | type: string 129 | isMature: 130 | type: boolean 131 | publish: 132 | type: string 133 | expiry: 134 | type: [string, 'null'] 135 | counts: 136 | type: object 137 | required: [games, media] 138 | properties: 139 | games: 140 | type: integer 141 | media: 142 | type: integer 143 | tiers: 144 | type: array 145 | items: 146 | type: object 147 | required: 148 | - price 149 | - games 150 | properties: 151 | price: 152 | $ref: "../schemas/obj.price.yaml" 153 | games: 154 | type: array 155 | items: 156 | type: object 157 | required: [id, slug, title, type, mature] 158 | properties: 159 | id: 160 | type: string 161 | slug: 162 | type: string 163 | title: 164 | type: string 165 | type: 166 | type: string 167 | mature: 168 | type: boolean 169 | examples: 170 | success: 171 | value: 172 | $ref: "../examples/prices.v2.success.json" 173 | -------------------------------------------------------------------------------- /docs/defs/profile.background.games.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: array 6 | items: 7 | type: array 8 | minItems: 2 9 | maxItems: 2 10 | items: 11 | type: 12 | - string 13 | - integer 14 | examples: 15 | success: 16 | value: 17 | $ref: "../examples/profile.background.games.v2.success.json" 18 | -------------------------------------------------------------------------------- /docs/defs/profile.background.list.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: array 6 | items: 7 | type: array 8 | minItems: 2 9 | maxItems: 2 10 | items: 11 | type: string 12 | examples: 13 | success: 14 | value: 15 | $ref: "../examples/profile.background.list.v2.success.json" 16 | -------------------------------------------------------------------------------- /docs/defs/profile.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | required: 7 | - badges 8 | - steamrep 9 | - style 10 | - bg 11 | properties: 12 | badges: 13 | type: array 14 | items: 15 | type: object 16 | required: 17 | - title 18 | - img 19 | properties: 20 | title: 21 | type: string 22 | img: 23 | type: string 24 | steamrep: 25 | type: array 26 | items: 27 | type: string 28 | style: 29 | type: 30 | - 'null' 31 | - string 32 | bg: 33 | type: object 34 | required: 35 | - img 36 | - appid 37 | properties: 38 | img: 39 | type: 40 | - 'null' 41 | - string 42 | appid: 43 | type: 44 | - 'null' 45 | - integer 46 | -------------------------------------------------------------------------------- /docs/defs/rates.v1.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | propertyNames: 7 | pattern: "^[A-Z]{3}$" 8 | additionalProperties: 9 | type: object 10 | propertyNames: 11 | pattern: ^[A-Z]{3}$ 12 | additionalProperties: 13 | type: number 14 | examples: 15 | success: 16 | summary: "Rates response example" 17 | value: 18 | $ref: "../examples/rates.v1.success.json" 19 | -------------------------------------------------------------------------------- /docs/defs/similar.v2.200.yaml: -------------------------------------------------------------------------------- 1 | description: Success 2 | content: 3 | application/json: 4 | schema: 5 | type: array 6 | items: 7 | type: object 8 | required: [title, appid, sprating, score] 9 | properties: 10 | title: 11 | type: string 12 | appid: 13 | type: integer 14 | sprating: 15 | type: number 16 | score: 17 | type: number 18 | examples: 19 | success: 20 | value: 21 | $ref: "../examples/similar.v2.success.json" 22 | -------------------------------------------------------------------------------- /docs/defs/twitch-stream.v2.yaml: -------------------------------------------------------------------------------- 1 | description: OK 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | properties: 7 | user_name: 8 | type: string 9 | game: 10 | type: string 11 | view_count: 12 | type: integer 13 | thumbnail_url: 14 | type: string 15 | -------------------------------------------------------------------------------- /docs/examples/app.v2.success.json: -------------------------------------------------------------------------------- 1 | { 2 | "family_sharing": true, 3 | "players": { 4 | "recent": 26069, 5 | "peak_today": 27665, 6 | "peak_all": 1051810 7 | }, 8 | "wsgf": null, 9 | "hltb": { 10 | "story": 1534, 11 | "extras": 3822, 12 | "complete": 6455, 13 | "url": "https:\/\/howlongtobeat.com\/game\/2127" 14 | }, 15 | "reviews": { 16 | "metauser": { 17 | "score": 71, 18 | "verdict": "Mixed or average", 19 | "url": "https:\/\/metacritic.com\/game\/cyberpunk-2077\/user-reviews\/?platform=pc" 20 | }, 21 | "opencritic": { 22 | "score": 76, 23 | "verdict": "Strong", 24 | "url": "https:\/\/opencritic.com\/game\/8525\/cyberpunk-2077" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/examples/dlcinfo.v2.success.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "name": "New Maps or Game Areas", 5 | "description": "This DLC adds new maps or areas to your game.", 6 | "icon": "dlc_maps.png" 7 | }, 8 | { 9 | "id": 8, 10 | "name": "Multiplayer Only Content", 11 | "description": "This DLC only affects multi-player gameplay.", 12 | "icon": "dlc_multiplayeronly.png" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /docs/examples/early-access.v1.success.json: -------------------------------------------------------------------------------- 1 | [ 2 | 15540, 3 | 16900, 4 | 108600, 5 | 217120, 6 | 223490, 7 | 224440, 8 | 229560, 9 | 230290, 10 | 232810, 11 | 236370 12 | ] 13 | -------------------------------------------------------------------------------- /docs/examples/market.cards.average-prices.v2.success.json: -------------------------------------------------------------------------------- 1 | { 2 | "220": { 3 | "regular": 0.087143, 4 | "foil": 0.707143 5 | }, 6 | "440": { 7 | "foil": 0.905714, 8 | "regular": 0.051429 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/examples/market.cards.v2.success.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alyx Vance": { 3 | "img": "IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsNxGVIwXpaL3_Z0W11LtNtNsjDhgz2oOWIQXDzMG-WKXeNHVs5HLBXNG6IqDCitO6TSjGbEukkQAoCfKMH-mFBaJyXf0xqwtVUuWG9hXt0Excvd5gXJFfjzndKN74gkHAUcchSynbwIMaPjQplahRtWbCxX72TOdPxmXwlQ1o5SLZcaYl9HTjTpg", 4 | "url": "220-Alyx%20Vance", 5 | "price": 0.09 6 | }, 7 | "Alyx Vance (Foil)": { 8 | "img": "IzMF03bk9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NsNxGVIwXpaL3_Z0W11LtNtNsjDhgz2oOWIQXDzMG-WKXeNHVs5HLBXNG6IqDCitO6TSjGbEukkQAoCfKMH-mFBaJyXf0xqwtVUuWG9hXt0Excvd5gWKF-6kiwXab8kkHZAJpMGnCT3I5eIgllgbEI8XurmVryUa4P3wS11Q1o5SLZcaYlIj14tyA", 9 | "url": "220-Alyx%20Vance%20%28Foil%29", 10 | "price": 0.82 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/examples/prices.v2.success.json: -------------------------------------------------------------------------------- 1 | { 2 | "prices": { 3 | "app\/403190": { 4 | "current": { 5 | "shop": { 6 | "id": 37, 7 | "name": "Humble Store" 8 | }, 9 | "price": { 10 | "amount": 6.24, 11 | "amountInt": 624, 12 | "currency": "EUR" 13 | }, 14 | "regular": { 15 | "amount": 12.49, 16 | "amountInt": 1249, 17 | "currency": "EUR" 18 | }, 19 | "cut": 50, 20 | "voucher": null, 21 | "flag": "S", 22 | "drm": [ 23 | { 24 | "id": 61, 25 | "name": "Steam" 26 | } 27 | ], 28 | "platforms": [ 29 | { 30 | "id": 1, 31 | "name": "Windows" 32 | }, 33 | { 34 | "id": 2, 35 | "name": "Mac" 36 | } 37 | ], 38 | "timestamp": "2024-01-21T01:55:52+01:00", 39 | "expiry": "2024-02-05T19:00:00+01:00", 40 | "url": "https:\/\/isthereanydeal.com\/link\/018d2789-0f1b-7056-9c23-934949c59fc3\/" 41 | }, 42 | "lowest": { 43 | "shop": { 44 | "id": 39, 45 | "name": "Chrono.gg" 46 | }, 47 | "price": { 48 | "amount": 5.54, 49 | "amountInt": 554, 50 | "currency": "EUR" 51 | }, 52 | "regular": { 53 | "amount": 17.02, 54 | "amountInt": 1702, 55 | "currency": "EUR" 56 | }, 57 | "cut": 67, 58 | "timestamp": "2017-08-18T18:11:12+02:00" 59 | }, 60 | "bundled": 0, 61 | "urls": { 62 | "info": "https:\/\/isthereanydeal.com\/game\/id:018d2785-b38f-7200-8f41-5d86ca45ebe3\/info\/", 63 | "history": "https:\/\/isthereanydeal.com\/game\/id:018d2785-b38f-7200-8f41-5d86ca45ebe3\/history\/" 64 | } 65 | } 66 | }, 67 | "bundles": [ 68 | { 69 | "id": 3882, 70 | "title": "Humble Survive This Bundle ", 71 | "page": { 72 | "id": 1, 73 | "name": "Humble Bundle" 74 | }, 75 | "url": "https:\/\/www.humblebundle.com\/survive-this-bundle", 76 | "details": "https:\/\/isthereanydeal.com\/bundles\/3882\/", 77 | "isMature": false, 78 | "publish": "2016-08-10T02:05:00+02:00", 79 | "expiry": "2016-08-23T20:00:00+02:00", 80 | "counts": { 81 | "games": 7, 82 | "media": 0 83 | }, 84 | "tiers": [ 85 | { 86 | "price": { 87 | "amount": 1, 88 | "amountInt": 100, 89 | "currency": "USD" 90 | }, 91 | "games": [ 92 | { 93 | "id": "018d2785-9f4b-7319-b47b-8cfeaaaff5dc", 94 | "slug": "tharsis", 95 | "title": "Tharsis", 96 | "type": "game", 97 | "mature": false 98 | }, 99 | { 100 | "id": "018d2785-a584-739a-a511-5de9b234ed7f", 101 | "slug": "kholat", 102 | "title": "Kholat", 103 | "type": "game", 104 | "mature": false 105 | }, 106 | { 107 | "id": "018d2785-a6b7-7117-82aa-7331e28f467b", 108 | "slug": "savage-lands", 109 | "title": "Savage Lands", 110 | "type": "game", 111 | "mature": false 112 | } 113 | ] 114 | }, 115 | { 116 | "price": null, 117 | "games": [ 118 | { 119 | "id": "018d2785-9898-731d-8204-5ae9eb6da870", 120 | "slug": "space-engineers", 121 | "title": "Space Engineers", 122 | "type": "game", 123 | "mature": false 124 | }, 125 | { 126 | "id": "018d2785-9bd1-71b9-8f39-ef720854b2c3", 127 | "slug": "rust", 128 | "title": "Rust", 129 | "type": "game", 130 | "mature": true 131 | }, 132 | { 133 | "id": "018d2785-a7a6-72ef-a248-2088e3447395", 134 | "slug": "shelter-2", 135 | "title": "Shelter 2", 136 | "type": "game", 137 | "mature": false 138 | } 139 | ] 140 | }, 141 | { 142 | "price": { 143 | "amount": 14, 144 | "amountInt": 1400, 145 | "currency": "USD" 146 | }, 147 | "games": [ 148 | { 149 | "id": "018d2785-b38f-7200-8f41-5d86ca45ebe3", 150 | "slug": "planetbase", 151 | "title": "Planetbase", 152 | "type": "game", 153 | "mature": false 154 | } 155 | ] 156 | } 157 | ] 158 | } 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /docs/examples/profile.background.games.v2.success.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | 603770, 4 | "- Arcane RERaise -" 5 | ], 6 | [ 7 | 449940, 8 | "! That Bastard Is Trying To Steal Our Gold !" 9 | ], 10 | [ 11 | 866510, 12 | "!Anyway!" 13 | ], 14 | [ 15 | 467530, 16 | ".atorb." 17 | ] 18 | ] 19 | -------------------------------------------------------------------------------- /docs/examples/profile.background.list.v2.success.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "i0CoZ81Ui0m-9KwlBY1L_18myuGuq1wfhWSIYhY_9XEDYOMNRBsMoGuuOgceXob50kaxV_PHjMO1MHaEqgQnpMiivV2wFR-km8Ox-HQD7PT-O_Y_daeXX2LJx-0utuNoSS_qwx92smXX1IXpJvdAtK9T", 4 | "Gordon & Alyx" 5 | ], 6 | [ 7 | "i0CoZ81Ui0m-9KwlBY1L_18myuGuq1wfhWSIYhY_9XEDYOMNRBsMoGuuOgceXob50kaxV_PHjMO1MHaEqgQnpMijuF3iE0inzJKx-HoD7KKoaaduIvXDW2PFmLgn5-M7GivnzRtx5mSG1IXpJk_KsXuB", 8 | "Gordon & Alyx 2" 9 | ] 10 | ] 11 | -------------------------------------------------------------------------------- /docs/examples/rates.v1.success.json: -------------------------------------------------------------------------------- 1 | { 2 | "AED": { 3 | "USD": 0.27225 4 | }, 5 | "AFN": { 6 | "USD": 0.009711 7 | }, 8 | "ALL": { 9 | "USD": 0.009228 10 | }, 11 | "AMD": { 12 | "USD": 0.002073 13 | }, 14 | "ANG": { 15 | "USD": 0.557916 16 | }, 17 | "AOA": { 18 | "USD": 0.00189 19 | }, 20 | "ARS": { 21 | "USD": 0.009559 22 | }, 23 | "AUD": { 24 | "USD": 0.6987 25 | }, 26 | "AWG": { 27 | "USD": 0.555401 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/examples/similar.v2.success.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Cyberpunk Detective", 4 | "appid": 1324400, 5 | "sprating": 0, 6 | "score": 3.42 7 | }, 8 | { 9 | "title": "Cyber City", 10 | "appid": 1061930, 11 | "sprating": 0.97, 12 | "score": 3.306 13 | }, 14 | { 15 | "title": "CyberCity: SEX Saga", 16 | "appid": 2407350, 17 | "sprating": 2.86, 18 | "score": 3.235 19 | }, 20 | { 21 | "title": "Cloudpunk", 22 | "appid": 746850, 23 | "sprating": 8.31, 24 | "score": 2.988 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /docs/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Augmented Steam Server 4 | description: Augmented Steam server for Augmented Steam browser extension 5 | version: 2.0.0 6 | license: 7 | name: MIT 8 | identifier: MIT 9 | servers: 10 | - url: https://api.augmentedsteam.com 11 | paths: 12 | /rates/v1: 13 | get: 14 | operationId: rates-v1 15 | tags: 16 | - global 17 | summary: Currency Conversion Rates 18 | description: Get current conversion rates 19 | security: [] 20 | parameters: 21 | - name: to 22 | in: query 23 | description: List of currency codes 24 | required: true 25 | style: simple 26 | schema: 27 | type: array 28 | items: 29 | $ref: './schemas/obj.currency-code.yaml' 30 | responses: 31 | 200: 32 | $ref: "./defs/rates.v1.200.yaml" 33 | default: 34 | $ref: "./defs/default-error.yaml" 35 | 36 | /early-access/v1: 37 | get: 38 | operationId: earlyaccess-v1 39 | tags: 40 | - global 41 | summary: Early Access Games 42 | responses: 43 | 200: 44 | $ref: "./defs/early-access.v1.yaml" 45 | 46 | /prices/v2: 47 | post: 48 | operationId: prices-v2 49 | tags: 50 | - app 51 | summary: Prices 52 | security: [] 53 | requestBody: 54 | content: 55 | application/json: 56 | schema: 57 | type: object 58 | properties: 59 | country: 60 | type: string 61 | shops: 62 | type: array 63 | items: 64 | type: integer 65 | apps: 66 | type: array 67 | items: 68 | type: integer 69 | subs: 70 | type: array 71 | items: 72 | type: integer 73 | bundles: 74 | type: array 75 | items: 76 | type: integer 77 | voucher: 78 | type: boolean 79 | 80 | responses: 81 | 200: 82 | $ref: "./defs/prices.v2.200.yaml" 83 | default: 84 | $ref: "./defs/default-error.yaml" 85 | 86 | /app/{appid}/v2: 87 | parameters: 88 | - schema: 89 | type: integer 90 | name: appid 91 | in: path 92 | required: true 93 | get: 94 | operationId: get-v1-app-appid 95 | tags: 96 | - app 97 | summary: App Info 98 | security: [] 99 | responses: 100 | 200: 101 | $ref: "./defs/app.v2.200.yaml" 102 | default: 103 | $ref: "./defs/default-error.yaml" 104 | 105 | /dlc/{appid}/v2: 106 | get: 107 | operationId: dlcinfo-v2 108 | tags: 109 | - app 110 | summary: DLC Info 111 | security: [] 112 | parameters: 113 | - $ref: "#/components/parameters/AppId" 114 | responses: 115 | 200: 116 | $ref: "./defs/dlcinfo.v2.200.yaml" 117 | default: 118 | $ref: "./defs/default-error.yaml" 119 | 120 | /similar/{appid}/v2: 121 | parameters: 122 | - schema: 123 | type: integer 124 | name: appid 125 | in: path 126 | required: true 127 | get: 128 | operationId: get-v2-similar-appid 129 | tags: 130 | - app 131 | parameters: 132 | - schema: 133 | type: integer 134 | example: 5 135 | in: query 136 | name: count 137 | - schema: 138 | type: boolean 139 | example: '1' 140 | in: query 141 | name: shuffle 142 | summary: Similar Games 143 | responses: 144 | 200: 145 | $ref: "./defs/similar.v2.200.yaml" 146 | default: 147 | $ref: "./defs/default-error.yaml" 148 | 149 | /market/cards/v2: 150 | get: 151 | operationId: market-cards-v2 152 | tags: 153 | - market 154 | summary: Cards 155 | security: [] 156 | parameters: 157 | - $ref: "#/components/parameters/Currency" 158 | - $ref: "#/components/parameters/AppId" 159 | responses: 160 | 200: 161 | $ref: "./defs/market.cards.v2.200.yaml" 162 | default: 163 | $ref: "./defs/default-error.yaml" 164 | 165 | /market/cards/average-prices/v2: 166 | get: 167 | operationId: market-cards-averageprices-v2 168 | tags: 169 | - market 170 | summary: Average Card Prices 171 | security: [] 172 | parameters: 173 | - $ref: "#/components/parameters/Currency" 174 | - name: appids 175 | in: query 176 | required: true 177 | style: simple 178 | schema: 179 | type: array 180 | items: 181 | type: integer 182 | responses: 183 | 200: 184 | $ref: "./defs/market.cards.average-prices.v2.200.yaml" 185 | default: 186 | $ref: "./defs/default-error.yaml" 187 | 188 | /twitch/{channel}/stream/v2: 189 | parameters: 190 | - schema: 191 | type: string 192 | name: channel 193 | in: path 194 | required: true 195 | get: 196 | operationId: twitch-stream-v2 197 | summary: Current Stream of Twitch Channel 198 | tags: 199 | - community 200 | responses: 201 | '200': 202 | $ref: "./defs/twitch-stream.v2.yaml" 203 | 204 | /profile/{steamId}/v2: 205 | get: 206 | operationId: profile-v2 207 | tags: 208 | - profile 209 | summary: Profile Info 210 | security: [] 211 | parameters: 212 | - schema: 213 | type: integer 214 | name: steamId 215 | in: path 216 | required: true 217 | responses: 218 | 200: 219 | $ref: "./defs/profile.v2.200.yaml" 220 | default: 221 | $ref: "./defs/default-error.yaml" 222 | 223 | /profile/background/list/v2: 224 | get: 225 | operationId: background-list-v2 226 | tags: 227 | - profile 228 | summary: List of Available Backgrounds 229 | security: [] 230 | parameters: 231 | - $ref: '#/components/parameters/AppId' 232 | responses: 233 | 200: 234 | $ref: "./defs/profile.background.list.v2.200.yaml" 235 | default: 236 | $ref: "./defs/default-error.yaml" 237 | 238 | /profile/background/games/v1: 239 | get: 240 | operationId: background-games-v1 241 | tags: 242 | - profile 243 | summary: List of Games with Backgrounds 244 | security: [] 245 | responses: 246 | 200: 247 | $ref: "./defs/profile.background.games.v2.200.yaml" 248 | default: 249 | $ref: "./defs/default-error.yaml" 250 | 251 | /profile/background/delete/v2: 252 | get: 253 | operationId: delete-background-v2 254 | tags: 255 | - profile 256 | summary: Delete Background 257 | security: [] 258 | parameters: 259 | - $ref: '#/components/parameters/SteamId' 260 | responses: 261 | 302: 262 | description: Found 263 | 264 | /profile/background/save/v2: 265 | get: 266 | operationId: save-background-v2 267 | tags: 268 | - profile 269 | summary: Save Background 270 | security: [] 271 | parameters: 272 | - $ref: '#/components/parameters/AppId' 273 | - schema: 274 | type: string 275 | in: query 276 | name: img 277 | required: true 278 | - $ref: '#/components/parameters/SteamId' 279 | responses: 280 | 302: 281 | description: Found 282 | 283 | /profile/style/delete/v2: 284 | get: 285 | operationId: delete-style-v2 286 | tags: 287 | - profile 288 | summary: Delete Profile Style 289 | security: [] 290 | parameters: 291 | - $ref: '#/components/parameters/SteamId' 292 | responses: 293 | 302: 294 | description: Found 295 | 296 | /profile/style/save/v2: 297 | get: 298 | operationId: save-style-v2 299 | tags: 300 | - profile 301 | summary: Save Profile Style 302 | security: [] 303 | parameters: 304 | - schema: 305 | type: string 306 | in: query 307 | name: style 308 | required: true 309 | - $ref: '#/components/parameters/SteamId' 310 | responses: 311 | 302: 312 | description: Found 313 | 314 | components: 315 | parameters: 316 | AppId: 317 | name: appid 318 | in: query 319 | required: true 320 | schema: 321 | type: integer 322 | Currency: 323 | name: currency 324 | in: query 325 | required: true 326 | schema: 327 | $ref: '#/components/schemas/CurrencyCode' 328 | SteamId: 329 | name: profile 330 | in: query 331 | required: false 332 | schema: 333 | type: integer 334 | description: Steam profile ID 335 | schemas: 336 | CurrencyCode: 337 | type: string 338 | pattern: ^[A-Z]{3}$ 339 | securitySchemes: {} 340 | -------------------------------------------------------------------------------- /docs/schemas/obj.currency-code.yaml: -------------------------------------------------------------------------------- 1 | $schema: "https://json-schema.org/draft/2020-12/schema" 2 | title: Currency Code 3 | type: string 4 | pattern: "^[A-Z]{3}$" 5 | -------------------------------------------------------------------------------- /docs/schemas/obj.drm.yaml: -------------------------------------------------------------------------------- 1 | $schema: "https://json-schema.org/draft/2020-12/schema" 2 | title: DRM 3 | type: object 4 | required: 5 | - id 6 | - name 7 | properties: 8 | id: 9 | type: integer 10 | name: 11 | type: string 12 | 13 | -------------------------------------------------------------------------------- /docs/schemas/obj.platform.yaml: -------------------------------------------------------------------------------- 1 | $schema: "https://json-schema.org/draft/2020-12/schema" 2 | title: Platform 3 | type: object 4 | required: 5 | - id 6 | - name 7 | properties: 8 | id: 9 | type: integer 10 | name: 11 | type: string 12 | 13 | -------------------------------------------------------------------------------- /docs/schemas/obj.price.yaml: -------------------------------------------------------------------------------- 1 | $schema: "https://json-schema.org/draft/2020-12/schema" 2 | title: Prioce 3 | type: object 4 | required: 5 | - amount 6 | - amountInt 7 | - currency 8 | properties: 9 | amount: 10 | type: float 11 | amountInt: 12 | type: integer 13 | currency: 14 | type: string 15 | 16 | -------------------------------------------------------------------------------- /docs/schemas/obj.shop.yaml: -------------------------------------------------------------------------------- 1 | $schema: "https://json-schema.org/draft/2020-12/schema" 2 | title: Shop 3 | type: object 4 | required: 5 | - id 6 | - name 7 | properties: 8 | id: 9 | type: integer 10 | name: 11 | type: string 12 | 13 | -------------------------------------------------------------------------------- /install.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE `currency` ( 3 | `from` char(3) NOT NULL, 4 | `to` char(3) NOT NULL, 5 | `rate` float NOT NULL, 6 | `timestamp` int unsigned NOT NULL, 7 | UNIQUE(`to`, `from`), 8 | INDEX(`timestamp`) 9 | ) ENGINE=InnoDB; 10 | 11 | CREATE TABLE `dlc_categories` ( 12 | `id` tinyint NOT NULL, 13 | `name` varchar(25) NOT NULL, 14 | `icon` varchar(25) NOT NULL, 15 | `description` varchar(180) NOT NULL, 16 | PRIMARY KEY(`id`) 17 | ) ENGINE=InnoDB; 18 | 19 | CREATE TABLE `game_dlc` ( 20 | `appid` int NOT NULL, 21 | `dlc_category` tinyint NOT NULL, 22 | `score` int NOT NULL, 23 | PRIMARY KEY (`appid`, `dlc_category`), 24 | FOREIGN KEY (dlc_category) REFERENCES dlc_categories(id) ON UPDATE CASCADE ON DELETE RESTRICT 25 | ) ENGINE=InnoDB; 26 | 27 | CREATE TABLE `market_index` ( 28 | `appid` int NOT NULL, 29 | `last_update` int NOT NULL DEFAULT 0, 30 | `last_request` int NOT NULL, 31 | `request_counter` int NOT NULL DEFAULT 0, 32 | PRIMARY KEY (`appid`), 33 | INDEX (`last_update` DESC, `appid`) 34 | ) ENGINE=InnoDB; 35 | 36 | CREATE TABLE `market_data` ( 37 | `hash_name` varchar(255) NOT NULL, 38 | `appid` int NOT NULL, 39 | `appname` varchar(255) NOT NULL, 40 | `name` varchar(255) NOT NULL, 41 | `sell_listings` int NOT NULL DEFAULT 0, 42 | `sell_price_usd` int NOT NULL, 43 | `img` text NOT NULL, 44 | `url` text NOT NULL, 45 | `type` enum('unknown', 'background', 'booster', 'card', 'emoticon', 'item') NOT NULL, 46 | `rarity` enum('normal', 'uncommon', 'foil', 'rare'), 47 | `timestamp` int unsigned NOT NULL, 48 | PRIMARY KEY (`hash_name`), 49 | INDEX (`appid`) 50 | ) ENGINE=InnoDB; 51 | 52 | CREATE TABLE `users_profiles` ( 53 | `steam64` bigint unsigned NOT NULL, 54 | `bg_img` text, 55 | `bg_appid` int, 56 | `style` varchar(20), 57 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 58 | PRIMARY KEY(`steam64`) 59 | ) ENGINE=InnoDB; 60 | 61 | CREATE TABLE `badges` ( 62 | `id` tinyint unsigned NOT NULL, 63 | `title` varchar(40) NOT NULL, 64 | `img` varchar(30) NOT NULL, 65 | PRIMARY KEY (`id`) 66 | ) ENGINE=InnoDB; 67 | 68 | CREATE TABLE `users_badges` ( 69 | `steam64` bigint unsigned NOT NULL, 70 | `badge_id` tinyint unsigned NOT NULL, 71 | PRIMARY KEY (`steam64`, `badge_id`), 72 | FOREIGN KEY (badge_id) REFERENCES badges(id) ON UPDATE CASCADE ON DELETE CASCADE 73 | ) ENGINE=InnoDB; 74 | 75 | CREATE TABLE `steamrep` ( 76 | `steam64` bigint unsigned NOT NULL, 77 | `rep` varchar(100), 78 | `timestamp` int unsigned NOT NULL, 79 | `checked` tinyint unsigned NOT NULL, 80 | PRIMARY KEY (`steam64`), 81 | INDEX (`checked`, `timestamp`) 82 | ) ENGINE=InnoDB; 83 | 84 | CREATE TABLE `cache` ( 85 | `key` int NOT NULL, 86 | `field` varchar(50) NOT NULL, 87 | `data` longblob, 88 | `expiry` int unsigned NOT NULL, 89 | PRIMARY KEY (`key`, `field`), 90 | INDEX (`expiry`) 91 | ); 92 | 93 | CREATE TABLE `sessions` ( 94 | `token` char(10) NOT NULL, 95 | `hash` char(64) NOT NULL, 96 | `steam_id` bigint NOT NULL, 97 | `expiry` int unsigned NOT NULL, 98 | PRIMARY KEY(`token`) 99 | ) ENGINE=InnoDB; 100 | -------------------------------------------------------------------------------- /migration.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IsThereAnyDeal/AugmentedSteam_Server/966d21c5a698cc8c1a69ba2c00ffe6cb88b508aa/migration.sql -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@redocly/cli": "^1.8.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 3 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 4 | 5 | parameters: 6 | treatPhpDocTypesAsCertain: false 7 | level: max 8 | paths: 9 | - src 10 | ignoreErrors: 11 | - identifier: missingType.generics 12 | -------------------------------------------------------------------------------- /routing.php: -------------------------------------------------------------------------------- 1 | route($container); 10 | -------------------------------------------------------------------------------- /src/Config/BrightDataConfig.php: -------------------------------------------------------------------------------- 1 | config = (new Processor())->process(Expect::structure([ 25 | "url" => Expect::string()->required(), 26 | "port" => Expect::int()->required(), 27 | "user" => Expect::string()->required(), 28 | "password" => Expect::string()->required(), 29 | "zone" => Expect::string()->required() 30 | ]), $config); 31 | } 32 | 33 | public function getUrl(): string { 34 | return $this->config->url; 35 | } 36 | 37 | public function getPort(): int { 38 | return $this->config->port; 39 | } 40 | 41 | public function getUser(): string { 42 | return $this->config->user; 43 | } 44 | 45 | public function getPassword(): string { 46 | return $this->config->password; 47 | } 48 | 49 | public function getZone(): string { 50 | return $this->config->zone; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/CoreConfig.php: -------------------------------------------------------------------------------- 1 | config = (new Processor())->process(Expect::structure([ 29 | "host" => Expect::string()->required(), 30 | "dev" => Expect::bool(false), 31 | "prettyErrors" => Expect::bool(false), 32 | "sentry" => Expect::structure([ 33 | "enabled" => Expect::bool(false), 34 | "dsn" => Expect::string(), 35 | "environment" => Expect::string() 36 | ]) 37 | ]), $config); 38 | } 39 | 40 | public function getHost(): string { 41 | return $this->config->host; 42 | } 43 | 44 | public function isDev(): bool { 45 | return $this->config->dev; 46 | } 47 | 48 | public function isProduction(): bool { 49 | return !$this->isDev(); 50 | } 51 | 52 | public function usePrettyErrors(): bool { 53 | return $this->config->prettyErrors; 54 | } 55 | 56 | public function isSentryEnabled(): bool { 57 | return $this->config->sentry->enabled; 58 | } 59 | 60 | public function getSentryDsn(): ?string { 61 | return $this->config->sentry->dsn; 62 | } 63 | 64 | public function getSentryEnvironment(): string { 65 | return $this->config->sentry->environment; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Controllers/AppController.php: -------------------------------------------------------------------------------- 1 | cache->has($key, $field)) { 37 | return $this->cache->get($key, $field); 38 | } 39 | 40 | $data = $provider->fetch($appid); 41 | $this->cache->set($key, $field, $data, $ttl); 42 | return $data; 43 | } 44 | 45 | /** 46 | * @param array{appid: numeric-string} $params 47 | * @return array 48 | */ 49 | public function appInfo_v2(ServerRequestInterface $request, array $params): array { 50 | $appid = intval($params['appid']); 51 | 52 | return [ 53 | "family_sharing" => !$this->getData($this->exfgls, ECacheKey::Exfgls, $appid, 6*3600), 54 | "players" => $this->getData($this->players, ECacheKey::Players, $appid, 30*60), 55 | "wsgf" => $this->getData($this->wsgf, ECacheKey::WSGF, $appid, 3*86400), 56 | "hltb" => $this->getData($this->hltb, ECacheKey::HLTB, $appid, 86400), 57 | "reviews" => $this->getData($this->reviews, ECacheKey::Reviews, $appid, 86400) 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $data 10 | * @return list 11 | */ 12 | protected function validateIntList(array $data, string $key): array { 13 | if (isset($data[$key])) { 14 | if (!is_array($data[$key]) || !array_is_list($data[$key])) { 15 | throw new BadRequestException(); 16 | } 17 | foreach($data[$key] as $value) { 18 | if (!is_int($value)) { 19 | throw new BadRequestException(); 20 | } 21 | } 22 | } 23 | return $data[$key] ?? []; // @phpstan-ignore-line 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Controllers/DLCController.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function dlcInfo_v2(ServerRequestInterface $request, array $params): array { 23 | $appid = intval($params['appid']); 24 | if (empty($appid)) { 25 | return []; 26 | } 27 | 28 | $g = new TGameDlc(); 29 | $d = new TDlcCategories(); 30 | 31 | return $this->db->select(<<id, $d->name, $d->description, $d->icon 33 | FROM $g 34 | JOIN $d ON $g->dlc_category=$d->id 35 | WHERE $g->appid=:appid 36 | ORDER BY $g->score DESC 37 | LIMIT 3 38 | SQL 39 | )->params([ 40 | ":appid" => $appid 41 | ])->fetch(DDlcCategories::class) 42 | ->toArray(fn(DDlcCategories $o) => [ 43 | "id" => $o->getId(), 44 | "name" => $o->getName(), 45 | "description" => $o->getDescription(), 46 | "icon" => $o->getIcon() 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/EarlyAccessController.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function appids_v1(ServerRequestInterface $request): array { 24 | $key = ECacheKey::EarlyAccess; 25 | $field = "ea"; 26 | 27 | if ($this->cache->has($key, $field)) { 28 | $cached = $this->cache->get($key, $field) ?? []; 29 | if (!is_array($cached) || !array_is_list($cached)) { 30 | throw new \Exception(); 31 | } 32 | return $cached; 33 | } 34 | 35 | $appids = $this->provider->fetch(); 36 | $this->cache->set($key, $field, $appids, self::TTL); 37 | return $appids; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controllers/MarketController.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function averageCardPrices_v2(ServerRequestInterface $request): array { 25 | /** @var string $currency */ 26 | $currency = (new StringParam($request, "currency"))->value(); 27 | 28 | /** @var list $appids */ 29 | $appids = (new ListParam($request, "appids"))->value(); 30 | 31 | $appids = array_values(array_filter( 32 | array_map(fn($id) => intval($id), $appids), 33 | fn($id) => $id > 0 34 | )); 35 | 36 | if (count($appids) == 0) { 37 | throw new InvalidValueException("appids"); 38 | } 39 | 40 | $conversion = $this->converter->getConversion("USD", $currency); 41 | if (!is_float($conversion)) { 42 | throw new \Exception(); 43 | } 44 | 45 | $this->index->recordRequest(...$appids); 46 | return $this->manager->getAverageCardPrices($appids, $conversion); 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function cards_v2(ServerRequestInterface $request): array { 53 | /** @var string $currency */ 54 | $currency = (new StringParam($request, "currency"))->value(); 55 | 56 | /** @var int $appid */ 57 | $appid = (new IntParam($request, "appid"))->value(); 58 | 59 | $conversion = $this->converter->getConversion("USD", $currency); 60 | if (!is_float($conversion)) { 61 | throw new \Exception(); 62 | } 63 | 64 | $this->index->recordRequest($appid); 65 | return $this->manager->getCards($appid, $conversion); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Controllers/PricesController.php: -------------------------------------------------------------------------------- 1 | |JsonSerializable 17 | */ 18 | public function prices_v2(ServerRequestInterface $request): array|JsonSerializable { 19 | $data = $request->getBody()->getContents(); 20 | if (!json_validate($data)) { 21 | throw new BadRequestException(); 22 | } 23 | 24 | $params = json_decode($data, true, flags: JSON_THROW_ON_ERROR); 25 | if (!is_array($params) || !isset($params['country']) || !is_string($params['country'])) { 26 | throw new BadRequestException(); 27 | } 28 | 29 | $country = $params['country']; 30 | $shops = $this->validateIntList($params, "shops"); 31 | $apps = $this->validateIntList($params, "apps"); 32 | $subs = $this->validateIntList($params, "subs"); 33 | $bundles = $this->validateIntList($params, "bundles"); 34 | $voucher = filter_var($params['voucher'] ?? true, FILTER_VALIDATE_BOOLEAN); 35 | 36 | $ids = array_merge( 37 | array_map(fn($id) => "app/$id", array_filter($apps)), 38 | array_map(fn($id) => "sub/$id", array_filter($subs)), 39 | array_map(fn($id) => "bundle/$id", array_filter($bundles)), 40 | ); 41 | 42 | if (count($ids) == 0) { 43 | throw new BadRequestException(); 44 | } 45 | 46 | $overview = $this->pricesProvider->fetch($ids, $shops, $country, $voucher); 47 | return $overview ?? []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function profile_v2(ServerRequestInterface $request, array $params): array { 23 | $steamId = (int)$params['steamId']; 24 | 25 | $info = $this->userManager->getProfileInfo($steamId); 26 | $badges = $this->userManager->getBadges($steamId) 27 | ->toArray(fn(DBadges $badge) => [ 28 | "title" => $badge->getTitle(), 29 | "img" => $badge->getImg() 30 | ]); 31 | 32 | return [ 33 | "badges" => $badges, 34 | "steamrep" => $this->steamRepManager->getReputation($steamId), 35 | "style" => $info?->getStyle(), 36 | "bg" => [ 37 | "img" => $info?->getBgImg(), 38 | "appid" => $info?->getBgAppid(), 39 | ], 40 | ]; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Controllers/ProfileManagementController.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | public function backgrounds_v2(ServerRequestInterface $request): array { 35 | /** @var int $appid */ 36 | $appid = (new IntParam($request, "appid"))->value(); 37 | 38 | return $this->marketManager 39 | ->getBackgrounds($appid); 40 | } 41 | 42 | /** 43 | * @return list 44 | */ 45 | public function games_v1(ServerRequestInterface $request): array { 46 | return $this->marketManager 47 | ->getGamesWithBackgrounds(); 48 | } 49 | 50 | public function deleteBackground_v2(ServerRequestInterface $request): RedirectResponse { 51 | /** @var ?int $profile */ 52 | $profile = (new IntParam($request, "profile", default: null, nullable: true))->value(); 53 | 54 | $authResponse = $this->session->authorize( 55 | $request, 56 | "/profile/background/delete/v2", 57 | self::ReturnUrl.self::Failure, 58 | $profile 59 | ); 60 | if ($authResponse instanceof RedirectResponse) { 61 | return $authResponse; 62 | } 63 | 64 | $steamId = $authResponse; 65 | $this->userManager 66 | ->deleteBackground($steamId); 67 | 68 | return new RedirectResponse(self::ReturnUrl.self::Success); 69 | } 70 | 71 | public function saveBackground_v2(ServerRequestInterface $request): RedirectResponse { 72 | try { 73 | /** @var int $appid */ 74 | $appid = (new IntParam($request, "appid"))->value(); 75 | 76 | /** @var string $img */ 77 | $img = (new StringParam($request, "img"))->value(); 78 | 79 | /** @var ?int $profile */ 80 | $profile = (new IntParam($request, "profile", default: null, nullable: true))->value(); 81 | } catch(Throwable) { 82 | return new RedirectResponse(self::ReturnUrl.self::Error_BadRequest); 83 | } 84 | 85 | if (!$this->marketManager->doesBackgroundExist($appid, $img)) { 86 | return new RedirectResponse(self::ReturnUrl.self::Error_NotFound); 87 | } 88 | 89 | $authResponse = $this->session->authorize( 90 | $request, 91 | "/profile/background/save/v2?appid=$appid&img=$img", 92 | self::ReturnUrl.self::Failure, 93 | $profile 94 | ); 95 | if ($authResponse instanceof RedirectResponse) { 96 | return $authResponse; 97 | } 98 | 99 | $steamId = $authResponse; 100 | $this->userManager 101 | ->saveBackground($steamId, $appid, $img); 102 | 103 | return new RedirectResponse(self::ReturnUrl.self::Success); 104 | } 105 | 106 | public function deleteStyle_v2(ServerRequestInterface $request): RedirectResponse { 107 | /** @var ?int $profile */ 108 | $profile = (new IntParam($request, "profile", default: null, nullable: true))->value(); 109 | 110 | $authResponse = $this->session->authorize( 111 | $request, 112 | "/profile/style/delete/v2", 113 | self::ReturnUrl.self::Failure, 114 | $profile 115 | ); 116 | if ($authResponse instanceof RedirectResponse) { 117 | return $authResponse; 118 | } 119 | 120 | $steamId = $authResponse; 121 | $this->userManager 122 | ->deleteStyle($steamId); 123 | 124 | return new RedirectResponse(self::ReturnUrl.self::Success); 125 | } 126 | 127 | public function saveStyle_v2(ServerRequestInterface $request): RedirectResponse { 128 | try { 129 | /** @var string $style */ 130 | $style = (new StringParam($request, "style"))->value(); 131 | 132 | /** @var ?int $profile */ 133 | $profile = (new IntParam($request, "profile", default: null, nullable: true))->value(); 134 | } catch(Throwable) { 135 | return new RedirectResponse(self::ReturnUrl.self::Error_BadRequest); 136 | } 137 | 138 | $authResponse = $this->session->authorize( 139 | $request, 140 | "/profile/style/save/v2?style=$style", 141 | self::ReturnUrl.self::Failure, 142 | $profile 143 | ); 144 | if ($authResponse instanceof RedirectResponse) { 145 | return $authResponse; 146 | } 147 | 148 | $steamId = $authResponse; 149 | $this->userManager 150 | ->saveStyle($steamId, $style); 151 | 152 | return new RedirectResponse(self::ReturnUrl.self::Success); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Controllers/RatesController.php: -------------------------------------------------------------------------------- 1 | > 17 | */ 18 | public function rates_v1(ServerRequestInterface $request): array { 19 | /** @var list $currencies */ 20 | $currencies = (new ListParam($request, "to"))->value(); 21 | 22 | if (count($currencies) == 0 || count($currencies) > 2) { 23 | throw new InvalidValueException("to"); 24 | } 25 | 26 | return $this->converter->getAllConversionsTo($currencies); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Controllers/SimilarController.php: -------------------------------------------------------------------------------- 1 | |\JsonSerializable 24 | */ 25 | public function similar_v2(ServerRequestInterface $request, array $params): array|\JsonSerializable { 26 | $appid = intval($params['appid']); 27 | /** @var int $count */ 28 | $count = (new IntParam($request, "count", 5))->value(); 29 | /** @var bool $shuffle */ 30 | $shuffle = (new BoolParam($request, "shuffle", false))->value(); 31 | 32 | $key = ECacheKey::SteamPeek; 33 | $field = (string)$appid; 34 | 35 | if ($this->cache->has($key, $field)) { 36 | $games = $this->cache->get($key, $field); 37 | if (!is_null($games) && !($games instanceof SteamPeekResults)) { 38 | throw new \Exception(); 39 | } 40 | } else { 41 | $games = $this->steamPeek->fetch($appid); 42 | $this->cache->set($key, $field, $games, 10*86400); 43 | } 44 | 45 | if (is_null($games)) { 46 | return []; 47 | } 48 | 49 | return $games 50 | ->shuffle($shuffle) 51 | ->limit($count); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Controllers/TwitchController.php: -------------------------------------------------------------------------------- 1 | |JsonSerializable 23 | */ 24 | public function stream_v2(ServerRequestInterface $request, array $params): array|JsonSerializable { 25 | $channel = $params['channel']; 26 | 27 | $key = ECacheKey::Twitch; 28 | $field = $channel; 29 | 30 | if ($this->cache->has($key, $field)) { 31 | $stream = $this->cache->get($key, $field); 32 | if (!is_null($stream) && !($stream instanceof TwitchStream)) { 33 | throw new \Exception(); 34 | } 35 | } else { 36 | $stream = $this->twitch->fetch($channel); 37 | $this->cache->set($key, $field, $stream, 1800); 38 | } 39 | 40 | return $stream ?? []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Cron/CronJob.php: -------------------------------------------------------------------------------- 1 | lock = new Lock("temp/locks/{$name}.lock"); 17 | $this->lock->tryLock($durationMin * 60); 18 | return $this; 19 | } 20 | 21 | public function callable(callable $callable): self { 22 | $this->callable = $callable; 23 | return $this; 24 | } 25 | 26 | public function execute(): void { 27 | 28 | try { 29 | call_user_func($this->callable); 30 | } catch(\Throwable $e) { 31 | if (!is_null($this->lock)) { 32 | $this->lock->unlock(); 33 | } 34 | 35 | throw $e; // rethrow exception so we know about it 36 | } 37 | 38 | if (!is_null($this->lock)) { 39 | $this->lock->unlock(); 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/Cron/CronJobFactory.php: -------------------------------------------------------------------------------- 1 | loggerFactory = $this->container->get(LoggerFactoryInterface::class); 27 | $this->db = $this->container->get(DbDriver::class); 28 | } 29 | 30 | private function createRatesJob(): CronJob { 31 | return (new CronJob()) 32 | ->lock("rates", 5) 33 | ->callable(function(){ 34 | $container = Container::getInstance(); 35 | $logger = $this->loggerFactory->logger("rates"); 36 | 37 | $provider = $container->get(RatesProviderInterface::class); 38 | $updater = new RatesUpdater($this->db, $provider, $logger); 39 | $updater->update(); 40 | }); 41 | } 42 | 43 | private function createMarketJob(): CronJob { 44 | return (new CronJob()) 45 | ->lock("market", 60) 46 | ->callable(function(){ 47 | $logger = $this->loggerFactory->logger("market"); 48 | $guzzle = $this->container->get(Client::class); 49 | $proxy = $this->container->get(ProxyFactoryInterface::class) 50 | ->createProxy(); 51 | 52 | $loader = new Loader($logger, $guzzle); 53 | $updater = new MarketCrawler($this->db, $loader, $logger, $proxy); 54 | $updater->update(); 55 | }); 56 | } 57 | 58 | private function createCacheMaintenanceJob(): CronJob { 59 | return (new CronJob()) 60 | ->callable(function(){ 61 | $db = $this->container->get(DbDriver::class); 62 | 63 | $c = new TCache(); 64 | $db->delete(<<expiry < UNIX_TIMESTAMP() 67 | SQL 68 | )->delete(); 69 | }); 70 | } 71 | 72 | public function getJob(string $job): CronJob { 73 | 74 | return match($job) { 75 | "rates" => $this->createRatesJob(), 76 | "market" => $this->createMarketJob(), 77 | "cache-maintenance" => $this->createCacheMaintenanceJob(), 78 | default => throw new InvalidArgumentException() 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Data/Interfaces/AppData/AppDataProviderInterface.php: -------------------------------------------------------------------------------- 1 | */ 8 | public function fetch(): array; 9 | } 10 | -------------------------------------------------------------------------------- /src/Data/Interfaces/ExfglsProviderInterface.php: -------------------------------------------------------------------------------- 1 | $steamIds 12 | * @param list $shops 13 | */ 14 | public function fetch(array $steamIds, array $shops, string $country, bool $withVouchers): ?Prices; 15 | } 16 | -------------------------------------------------------------------------------- /src/Data/Interfaces/RatesProviderInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function fetch(): array; 16 | } 17 | -------------------------------------------------------------------------------- /src/Data/Interfaces/SteamRepProviderInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | public function getReputation(int $steamId): ?array; 12 | } 13 | -------------------------------------------------------------------------------- /src/Data/Interfaces/TwitchProviderInterface.php: -------------------------------------------------------------------------------- 1 | db->insert($i) 21 | ->columns($i->appid, $i->last_request) 22 | ->onDuplicateKeyUpdate($i->last_request) 23 | ->onDuplicateKeyExpression($i->request_counter, "{$i->request_counter->name}+1"); 24 | 25 | foreach($appids as $appid) { 26 | $insert->stack( 27 | (new DMarketIndex()) 28 | ->setAppid($appid) 29 | ->setLastRequest(time()) 30 | ); 31 | } 32 | $insert->persist(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Data/Managers/Market/MarketManager.php: -------------------------------------------------------------------------------- 1 | $appids 18 | * @return array> 19 | */ 20 | public function getAverageCardPrices(array $appids, float $conversion): array { 21 | 22 | $d = new TMarketData(); 23 | // @phpstan-ignore-next-line 24 | $select = $this->db->select(<<appid, $d->rarity=:foil as foil, AVG($d->sell_price_usd) as `average`, count(*) as `count` 26 | FROM $d 27 | WHERE $d->type=:card 28 | AND $d->appid IN :appids 29 | GROUP BY $d->appid, $d->rarity=:foil 30 | SQL 31 | )->params([ 32 | ":appids" => $appids, 33 | ":foil" => ERarity::Foil, 34 | ":card" => EType::Card 35 | ])->fetch(); 36 | 37 | $result = []; 38 | /** @var \stdClass $o */ 39 | foreach($select as $o) { 40 | $appid = (int)$o->appid; 41 | $isFoil = $o->foil; 42 | $avg = ($o->average/100)*$conversion; 43 | 44 | $result[$appid][$isFoil ? "foil" : "regular"] = $avg; 45 | } 46 | return $result; 47 | } 48 | 49 | /** 50 | * @return array 55 | */ 56 | public function getCards(int $appid, float $conversion): array { 57 | 58 | $d = new TMarketData(); 59 | $select = $this->db->select(<<name, $d->img, $d->url, $d->sell_price_usd 61 | FROM $d 62 | WHERE $d->appid=:appid 63 | AND $d->type=:card 64 | SQL 65 | )->params([ 66 | ":appid" => $appid, 67 | ":card" => EType::Card 68 | ])->fetch(DMarketData::class); 69 | 70 | $result = []; 71 | /** @var DMarketData $o */ 72 | foreach($select as $o) { 73 | $result[$o->getName()] = [ 74 | "img" => $o->getImg(), 75 | "url" => $o->getUrl(), 76 | "price" => ($o->getSellPriceUsd() / 100) * $conversion, 77 | ]; 78 | } 79 | return $result; 80 | } 81 | 82 | public function doesBackgroundExist(int $appid, string $img): bool { 83 | 84 | $d = new TMarketData(); 85 | return $this->db->select(<<appid=:appid 89 | AND $d->img=:img 90 | AND $d->type=:background 91 | SQL 92 | )->exists([ 93 | ":appid" =>$appid, 94 | ":img" => $img, 95 | ":background" => EType::Background 96 | ]); 97 | } 98 | 99 | /** 100 | * @param int $appid 101 | * @return list 102 | */ 103 | public function getBackgrounds(int $appid): iterable { 104 | 105 | $d = new TMarketData(); 106 | // @phpstan-ignore-next-line 107 | return $this->db->select(<<name, $d->img 109 | FROM $d 110 | WHERE $d->appid=:appid 111 | AND $d->type=:background 112 | ORDER BY $d->name ASC 113 | SQL 114 | )->params([ 115 | ":appid" => $appid, 116 | ":background" => EType::Background 117 | ])->fetch(DMarketData::class) 118 | ->toArray(fn(DMarketData $o) => [ 119 | $o->getImg(), 120 | preg_replace("#\s*\(Profile Background\)#", "", $o->getName()), 121 | ]); 122 | } 123 | 124 | /** 125 | * @return list 126 | */ 127 | public function getGamesWithBackgrounds(): array { 128 | 129 | $d = new TMarketData(); 130 | return $this->db->select(<<appid, $d->appname 132 | FROM $d 133 | WHERE $d->type=:background 134 | ORDER BY $d->appname ASC 135 | SQL 136 | )->params([ 137 | ":background" => EType::Background 138 | ])->fetch(DMarketData::class) 139 | ->toArray(fn(DMarketData $o) => [$o->getAppid(), $o->getAppName()]); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Data/Managers/SteamRepManager.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function getReputation(int $steamId): array { 24 | $r = new TSteamRep(); 25 | 26 | /** @var ?DSteamRep $obj */ 27 | $obj = $this->db->select(<<rep 29 | FROM $r 30 | WHERE $r->steam64=:steamId 31 | AND (($r->checked=1 AND $r->timestamp >= :successTimestamp) 32 | OR ($r->checked=0 AND $r->timestamp >= :failureTimestamp)) 33 | SQL 34 | )->params([ 35 | ":steamId" => $steamId, 36 | ":successTimestamp" => time() - self::SuccessCacheLimit, 37 | ":failureTimestamp" => time() - self::FailureCacheLimit 38 | ])->fetch(DSteamRep::class) 39 | ->getOne(); 40 | 41 | $reputation = $obj?->getReputation(); 42 | 43 | if (is_null($reputation)) { 44 | $reputation = $this->provider->getReputation($steamId); 45 | 46 | $this->db->insert($r) 47 | ->columns($r->steam64, $r->rep, $r->timestamp, $r->checked) 48 | ->onDuplicateKeyUpdate($r->rep, $r->timestamp, $r->checked) 49 | ->persist((new DSteamRep()) 50 | ->setSteam64($steamId) 51 | ->setReputation($reputation) 52 | ->setTimestamp(time()) 53 | ); 54 | } 55 | 56 | return $reputation ?? []; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Data/Managers/UserManager.php: -------------------------------------------------------------------------------- 1 | db->delete(<<steam64=:steamId 26 | AND $p->bg_img IS NULL 27 | AND $p->style IS NULL 28 | SQL 29 | )->delete([ 30 | ":steamId" => $steamId 31 | ]); 32 | } 33 | 34 | public function deleteBackground(int $steamId): void { 35 | $p = new TUsersProfiles(); 36 | 37 | $this->db->updateObj($p) 38 | ->columns($p->bg_img, $p->bg_appid) 39 | ->where($p->steam64) 40 | ->update( 41 | (new DUsersProfiles()) 42 | ->setSteam64($steamId) 43 | ->setBgImg(null) 44 | ->setBgAppid(null) 45 | ); 46 | 47 | $this->cleanupProfile($steamId); 48 | } 49 | 50 | public function saveBackground(int $steamId, int $appid, string $img): void { 51 | $p = new TUsersProfiles(); 52 | 53 | $this->db->insert($p) 54 | ->columns($p->steam64, $p->bg_img, $p->bg_appid) 55 | ->onDuplicateKeyUpdate($p->bg_img, $p->bg_appid) 56 | ->persist( 57 | (new DUsersProfiles()) 58 | ->setSteam64($steamId) 59 | ->setBgAppid($appid) 60 | ->setBgImg($img) 61 | ); 62 | } 63 | 64 | public function deleteStyle(int $steamId): void { 65 | $p = new TUsersProfiles(); 66 | 67 | $this->db->updateObj($p) 68 | ->columns($p->style) 69 | ->where($p->steam64) 70 | ->update( 71 | (new DUsersProfiles()) 72 | ->setSteam64($steamId) 73 | ->setStyle(null) 74 | ); 75 | 76 | $this->cleanupProfile($steamId); 77 | } 78 | 79 | public function saveStyle(int $steamId, string $style): void { 80 | $p = new TUsersProfiles(); 81 | 82 | $this->db->insert($p) 83 | ->columns($p->steam64, $p->style) 84 | ->onDuplicateKeyUpdate($p->style) 85 | ->persist( 86 | (new DUsersProfiles()) 87 | ->setSteam64($steamId) 88 | ->setStyle($style) 89 | ); 90 | } 91 | 92 | /** 93 | * @param int $steamId 94 | * @return SqlResult 95 | */ 96 | public function getBadges(int $steamId): SqlResult { 97 | $b = new TBadges(); 98 | $u = new TUsersBadges(); 99 | 100 | return $this->db->select(<<title, $b->img 102 | FROM $u 103 | JOIN $b ON $u->badge_id=$b->id 104 | WHERE $u->steam64=:steamId 105 | SQL 106 | )->params([ 107 | ":steamId" => $steamId 108 | ])->fetch(DBadges::class); 109 | } 110 | 111 | public function getProfileInfo(int $steamId): ?DUsersProfiles { 112 | $p = new TUsersProfiles(); 113 | 114 | return $this->db->select(<<bg_img, $p->bg_appid, $p->style 116 | FROM $p 117 | WHERE $p->steam64=:steamId 118 | SQL 119 | )->params([ 120 | ":steamId" => $steamId 121 | ])->fetch(DUsersProfiles::class) 122 | ->getOne(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Data/Objects/DSteamRep.php: -------------------------------------------------------------------------------- 1 | steam64; 15 | } 16 | 17 | public function setSteam64(int $steam64): self { 18 | $this->steam64 = $steam64; 19 | return $this; 20 | } 21 | 22 | /** 23 | * @return list 24 | */ 25 | public function getReputation(): array { 26 | return empty($this->rep) ? [] : explode(",", $this->rep); 27 | } 28 | 29 | /** 30 | * @param ?list $reputation 31 | */ 32 | public function setReputation(?array $reputation): self { 33 | $this->rep = empty($reputation) ? null : implode(",", $reputation); 34 | $this->checked = !is_null($reputation); 35 | return $this; 36 | } 37 | 38 | public function getTimestamp(): int { 39 | return $this->timestamp; 40 | } 41 | 42 | public function setTimestamp(int $timestamp): self { 43 | $this->timestamp = $timestamp; 44 | return $this; 45 | } 46 | 47 | public function isChecked(): bool { 48 | return $this->checked; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Data/Objects/HLTB.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | #[\Override] 15 | public function jsonSerialize(): array { 16 | return [ 17 | "story" => $this->story, 18 | "extras" => $this->extras, 19 | "complete" => $this->complete, 20 | "url" => $this->url 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Data/Objects/Players.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | #[\Override] 14 | public function jsonSerialize(): array { 15 | return [ 16 | "recent" => $this->current, 17 | "peak_today" => $this->peakToday, 18 | "peak_all" => $this->peakAll, 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Data/Objects/Prices.php: -------------------------------------------------------------------------------- 1 | , 11 | * lowest: array, 12 | * urls: array{ 13 | * info: string, 14 | * history: string 15 | * } 16 | * }> 17 | */ 18 | public array $prices; 19 | 20 | /** 21 | * @var list> 22 | */ 23 | public array $bundles; 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function jsonSerialize(): array { 29 | return [ 30 | "prices" => $this->prices, 31 | "bundles" => $this->bundles 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Data/Objects/Reviews/Review.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | #[\Override] 16 | public function jsonSerialize(): array { 17 | return [ 18 | "score" => $this->score, 19 | "verdict" => $this->verdict, 20 | "url" => $this->url 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Data/Objects/Reviews/Reviews.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | #[\Override] 13 | public function jsonSerialize(): array { 14 | return [ 15 | "metauser" => $this->metauser, 16 | "opencritic" => $this->opencritic 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Data/Objects/SteamPeak/SteamPeekGame.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | #[\Override] 15 | public function jsonSerialize(): array { 16 | return [ 17 | "title" => $this->title, 18 | "appid" => $this->appid, 19 | "sprating" => $this->rating, 20 | "score" => $this->score 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Data/Objects/SteamPeak/SteamPeekResults.php: -------------------------------------------------------------------------------- 1 | */ 7 | public array $games; 8 | 9 | public function shuffle(bool $shuffle): self { 10 | if ($shuffle) { 11 | shuffle($this->games); 12 | } 13 | return $this; 14 | } 15 | 16 | public function limit(int $count): self { 17 | $this->games = array_slice($this->games, 0, $count); 18 | return $this; 19 | } 20 | 21 | /** 22 | * @return array 23 | */ 24 | #[\Override] 25 | public function jsonSerialize(): array { 26 | return $this->games; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/Objects/TwitchStream.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | #[Override] 19 | public function jsonSerialize(): array { 20 | return [ 21 | "user_name" => $this->userName, 22 | "game" => $this->game, 23 | "view_count" => $this->viewerCount, 24 | "thumbnail_url" => $this->thumbnailUrl 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Data/Objects/WSGF.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | #[\Override] 19 | public function jsonSerialize(): array { 20 | return [ 21 | "url" => $this->path, 22 | "wide" => $this->wideScreenGrade, 23 | "ultrawide" => $this->ultraWideScreenGrade, 24 | "multi_monitor" => $this->multiMonitorGrade, 25 | "4k" => $this->grade4k, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/Providers/EarlyAccessProvider.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function fetch(): array { 19 | $response = $this->loader->get($this->endpoints->getEarlyAccess()); 20 | 21 | $appids = []; 22 | if (!is_null($response)) { 23 | $appids = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 24 | if (!is_array($appids) || !array_is_list($appids)) { 25 | $appids = []; 26 | } 27 | } 28 | 29 | return $appids; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Data/Providers/ExfglsProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getExfgls(); 19 | $response = $this->loader->post($endpoint, json_encode([$appid])); 20 | 21 | if (is_null($response)) { 22 | throw new \Exception(); 23 | } 24 | 25 | /** 26 | * @var array $data 27 | */ 28 | $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 29 | if (!is_array($data)) { 30 | throw new \Exception(); 31 | } 32 | 33 | return array_key_exists((string)$appid, $data) && $data[(string)$appid]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Data/Providers/HLTBProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getHLTB($appid); 20 | $response = $this->loader->get($endpoint); 21 | 22 | if (!empty($response)) { 23 | $body = $response->getBody()->getContents(); 24 | if (!empty($body)) { 25 | $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR); 26 | 27 | if (is_array($json)) { 28 | /** 29 | * @var array{ 30 | * id: int, 31 | * main: ?int, 32 | * extra: ?int, 33 | * complete: ?int 34 | * } $json 35 | */ 36 | 37 | $hltb = new HLTB(); 38 | $hltb->story = $json['main'] === 0 ? null : (int)floor($json['main'] / 60); 39 | $hltb->extras = $json['extra'] === 0 ? null : (int)floor($json['extra'] / 60); 40 | $hltb->complete = $json['complete'] === 0 ? null : (int)floor($json['complete'] / 60); 41 | $hltb->url = "https://howlongtobeat.com/game/{$json['id']}"; 42 | return $hltb; 43 | } 44 | } 45 | } 46 | 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Data/Providers/PlayersProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getPlayers($appid); 20 | $response = $this->loader->get($endpoint); 21 | 22 | $players = new Players(); 23 | 24 | if (!is_null($response)) { 25 | $body = $response->getBody()->getContents(); 26 | $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR); 27 | 28 | if (is_array($json)) { 29 | /** 30 | * @var array{ 31 | * current: int, 32 | * day: int, 33 | * peak: int 34 | * } $json 35 | */ 36 | 37 | $players->current = $json['current']; 38 | $players->peakToday = $json['day']; 39 | $players->peakAll = $json['peak']; 40 | } 41 | } 42 | 43 | return $players; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Data/Providers/PricesProvider.php: -------------------------------------------------------------------------------- 1 | $steamIds 20 | * @return array SteamId:GID 21 | */ 22 | private function fetchIdMap(array $steamIds): array { 23 | $endpoint = $this->endpoints->getSteamIdLookup(); 24 | 25 | $response = $this->guzzle->post($endpoint, [ 26 | "body" => json_encode($steamIds), 27 | "headers" => [ 28 | "content-type" => "application/json", 29 | "accept" => "application/json" 30 | ] 31 | ]); 32 | if ($response->getStatusCode() != 200) { 33 | return []; 34 | } 35 | 36 | $json = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 37 | if (!is_array($json)) { 38 | return []; 39 | } 40 | 41 | // @phpstan-ignore-next-line 42 | return $json; 43 | } 44 | 45 | /** 46 | * @param list $shops 47 | * @param list $gids 48 | * @return array 49 | */ 50 | private function fetchOverview(string $country, array $shops, array $gids, bool $withVouchers): array { 51 | $endpoint = $this->endpoints->getPrices($country, $shops, $withVouchers); 52 | 53 | $response = $this->guzzle->post($endpoint, [ 54 | "body" => json_encode($gids), 55 | "headers" => [ 56 | "content-type" => "application/json", 57 | "accept" => "application/json" 58 | ] 59 | ]); 60 | 61 | if ($response->getStatusCode() !== 200) { 62 | return []; 63 | } 64 | 65 | $json = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 66 | if (!is_array($json)) { 67 | return []; 68 | } 69 | return $json; 70 | } 71 | 72 | /** 73 | * @param list $steamIds 74 | * @param list $shops 75 | * @return ?Prices 76 | */ 77 | public function fetch( 78 | array $steamIds, 79 | array $shops, 80 | string $country, 81 | bool $withVouchers 82 | ): ?Prices { 83 | 84 | $map = array_filter($this->fetchIdMap($steamIds)); 85 | if (empty($map)) { 86 | return null; 87 | } 88 | 89 | $gids = array_values($map); 90 | $overview = $this->fetchOverview($country, $shops, $gids, $withVouchers); 91 | if (empty($overview)) { 92 | return null; 93 | } 94 | 95 | /** 96 | * @var array{ 97 | * prices: list>, 98 | * bundles: list> 99 | * } $overview 100 | */ 101 | 102 | $gidMap = array_flip($map); 103 | 104 | $prices = new Prices(); 105 | $prices->prices = []; 106 | $prices->bundles = $overview['bundles']; 107 | 108 | /** 109 | * @var array{ 110 | * id: string, 111 | * current: array, 112 | * lowest: array, 113 | * bundled: number, 114 | * urls: array{game: string} 115 | * } $game 116 | */ 117 | foreach($overview['prices'] as $game) { 118 | $gid = $game['id']; 119 | $steamId = $gidMap[$gid]; 120 | $prices->prices[$steamId] = [ 121 | "current" => $game['current'], 122 | "lowest" => $game['lowest'], 123 | "bundled" => $game['bundled'], 124 | "urls" => [ 125 | "info" => $game['urls']['game']."info/", 126 | "history" => $game['urls']['game']."history/", 127 | ] 128 | ]; 129 | } 130 | return $prices; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Data/Providers/RatesProvider.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function fetch(): array { 25 | $endpoint = $this->endpoints->getRates(); 26 | 27 | $response = $this->loader->get($endpoint); 28 | if (!is_null($response)) { 29 | $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 30 | if (is_array($data)) { 31 | return $data; // @phpstan-ignore-line 32 | } 33 | } 34 | return []; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Data/Providers/ReviewsProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getReviews($appid); 21 | $response = $this->loader->get($endpoint); 22 | 23 | $reviews = new Reviews(); 24 | if (!is_null($response)) { 25 | $body = $response->getBody()->getContents(); 26 | $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR); 27 | 28 | if (is_array($json)) { 29 | if (!empty($json['metauser']) && is_array($json['metauser'])) { 30 | /** 31 | * @var array{ 32 | * score?: int, 33 | * verdict?: string, 34 | * url: string 35 | * } $review 36 | */ 37 | $review = $json['metauser']; 38 | $reviews->metauser = new Review( 39 | $review['score'] ?? null, 40 | $review['verdict'] ?? null, 41 | $review['url'] 42 | ); 43 | } 44 | 45 | if (!empty($json['opencritic']) && is_array($json['opencritic'])) { 46 | /** 47 | * @var array{ 48 | * score?: int, 49 | * verdict?: string, 50 | * url: string 51 | * } $review 52 | */ 53 | $review = $json['opencritic']; 54 | $reviews->opencritic = new Review( 55 | $review['score'] ?? null, 56 | $review['verdict'] ?? null, 57 | $review['url'] 58 | ); 59 | } 60 | } 61 | } 62 | 63 | return $reviews; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Data/Providers/SteamPeekProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getSteamPeek($appid); 21 | $response = $this->loader->get($endpoint); 22 | 23 | if (!is_null($response)) { 24 | $json = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); 25 | 26 | if (is_array($json) && is_array($json['response'] ?? null)) { 27 | $response = $json['response']; 28 | 29 | if (!empty($response['success']) && $response['success'] === 1 30 | && !empty($response['results']) 31 | ) { 32 | $this->logger->info((string)$appid); 33 | 34 | $results = new SteamPeekResults(); 35 | $results->games = array_values(array_map(function(array $a) { 36 | $game = new SteamPeekGame(); 37 | $game->title = $a['title']; 38 | $game->appid = intval($a['appid']); 39 | $game->rating = floatval($a['sprating']); 40 | $game->score = floatval($a['score']); 41 | return $game; 42 | }, $response['results'])); 43 | return $results; 44 | } 45 | } 46 | } 47 | 48 | $this->logger->error((string)$appid); 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Data/Providers/SteamRepProvider.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function getReputation(int $steamId): ?array { 21 | $url = $this->endpoints->getSteamRep($steamId); 22 | 23 | $response = $this->loader->get($url); 24 | if (is_null($response)) { 25 | return null; 26 | } 27 | 28 | $body = $response->getBody()->getContents(); 29 | $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR); 30 | 31 | if (is_array($json) 32 | && is_array($json['steamrep'] ?? null) 33 | && is_array($json['steamrep']['reputation'] ?? null) 34 | && is_string($json['steamrep']['reputation']['full'] ?? null) 35 | ) { 36 | $reputation = $json['steamrep']['reputation']['full']; 37 | if (!empty($reputation)) { 38 | return explode(",", $reputation); 39 | } 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Data/Providers/TwitchProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getTwitchStream($channel); 20 | 21 | $response = $this->loader->get($url); 22 | if (is_null($response)) { 23 | return null; 24 | } 25 | 26 | $body = $response->getBody()->getContents(); 27 | 28 | /** 29 | * @var array{ 30 | * user_name: string, 31 | * title: string, 32 | * thumbnail_url: string, 33 | * viewer_count: int, 34 | * game: string 35 | * } $json 36 | */ 37 | $json = json_decode($body, true, flags: JSON_THROW_ON_ERROR); 38 | if (!is_array($json)) { 39 | return null; 40 | } 41 | 42 | $stream = new TwitchStream(); 43 | $stream->userName = $json['user_name']; 44 | $stream->title = $json['title']; 45 | $stream->thumbnailUrl = $json['thumbnail_url']; 46 | $stream->viewerCount = $json['viewer_count']; 47 | $stream->game = $json['game']; 48 | return $stream; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Data/Providers/WSGFProvider.php: -------------------------------------------------------------------------------- 1 | endpoints->getWSGF($appid); 22 | $response = $this->loader->get($url); 23 | if (is_null($response)) { 24 | return null; 25 | } 26 | 27 | $data = null; 28 | 29 | $xml = simplexml_load_string($response->getBody()->getContents()); 30 | if ($xml !== false && !empty($xml->children())) { 31 | $json = json_encode($xml, flags: JSON_THROW_ON_ERROR); 32 | 33 | $obj = json_decode($json, true, flags: JSON_THROW_ON_ERROR); 34 | if (json_last_error() === JSON_ERROR_NONE && !empty($obj) && is_array($obj)) { 35 | /** 36 | * @var array{ 37 | * Title: string, 38 | * SteamID: numeric-string, 39 | * Path: string, 40 | * WideScreenGrade: string, 41 | * MultiMonitorGrade: string, 42 | * UltraWideScreenGrade: string, 43 | * Grade4k: string, 44 | * Nid: numeric-string 45 | * } $node 46 | * @var array{ 47 | * node: list> 48 | * } $obj 49 | */ 50 | $node = isset($obj['node'][0]) // check if we have multiple nodes in indexed array 51 | ? $obj['node'][count($obj['node']) - 1] // some entries have multiple nodes, not sure why 52 | : $obj['node']; 53 | 54 | $data = new WSGF(); 55 | $data->title = $node['Title']; 56 | $data->steamId = intval($node['SteamID']); 57 | $data->path = $node['Path']; 58 | $data->wideScreenGrade = $node['WideScreenGrade']; 59 | $data->multiMonitorGrade = $node['MultiMonitorGrade']; 60 | $data->ultraWideScreenGrade = $node['UltraWideScreenGrade']; 61 | $data->grade4k = $node['Grade4k']; 62 | $data->nid = intval($node['Nid']); 63 | } 64 | } 65 | 66 | if (!empty($data)) { 67 | $this->logger->info((string)$appid); 68 | } else { 69 | $this->logger->error((string)$appid); 70 | } 71 | return $data; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Data/Updaters/Market/MarketCrawler.php: -------------------------------------------------------------------------------- 1 | db = $db; 44 | $this->proxy = $proxy; 45 | 46 | $this->timestamp = time(); 47 | 48 | $d = new TMarketData(); 49 | $this->insertQuery = $this->db->insert($d) 50 | ->columns( 51 | $d->hash_name, $d->appid, 52 | $d->appname, 53 | $d->name, $d->sell_listings, $d->sell_price_usd, $d->img, $d->url, 54 | $d->type, $d->rarity, $d->timestamp 55 | ) 56 | ->onDuplicateKeyUpdate( 57 | $d->appname, 58 | $d->name, $d->sell_listings, $d->sell_price_usd, $d->img, $d->url, 59 | $d->type, $d->rarity, $d->timestamp 60 | ); 61 | } 62 | 63 | /** 64 | * @return list 65 | */ 66 | private function getAppidBatch(): array { 67 | $i = new TMarketIndex(); 68 | 69 | // @phpstan-ignore-next-line 70 | return $this->db->select(<<appid 72 | FROM $i 73 | WHERE $i->last_update <= :timestamp 74 | ORDER BY $i->last_update ASC, 75 | $i->request_counter DESC 76 | LIMIT :limit 77 | SQL 78 | )->params([ 79 | ":timestamp" => time() - self::UpdateFrequency, 80 | ":limit" => self::RequestBatchSize 81 | ])->fetchValueArray(); 82 | } 83 | 84 | /** 85 | * @param list $appids 86 | */ 87 | private function makeRequest(array $appids, int $start=0): void { 88 | $params = [ 89 | "query" => "", 90 | "start" => $start, 91 | "count" => 100, 92 | "search_description" => 0, 93 | "sort_column" => "popular", 94 | "sort_dir" => "desc", 95 | "norender" => 1, 96 | ]; 97 | foreach($appids as $appid) { 98 | $params['category_753_Game'][] = "tag_app_{$appid}"; 99 | } 100 | 101 | $url = "https://steamcommunity.com/market/search/render/?".http_build_query($params); 102 | $item = (new Item($url)) 103 | ->setData(["appids" => $appids]) 104 | ->setCurlOptions($this->proxy->getCurlOptions()); 105 | 106 | $this->enqueueRequest($item); 107 | $this->requestCounter++; 108 | } 109 | 110 | protected function successHandler(Item $request, ResponseInterface $response, string $effectiveUri): void { 111 | if (!$this->mayProcess($request, $response, self::MaxAttempts)) { 112 | return; 113 | } 114 | 115 | $data = $response->getBody()->getContents(); 116 | 117 | /** 118 | * @var array{ 119 | * success: bool, 120 | * start: int, 121 | * pagesize: int, 122 | * total_count: int, 123 | * results: list 136 | * } $json 137 | */ 138 | $json = json_decode($data, true); 139 | 140 | /** @var list $appids */ 141 | $appids = $request->getData()['appids']; 142 | if ($json['start'] === 0 && $json['start'] < $json['total_count']) { 143 | $pageSize = $json['pagesize']; 144 | 145 | if ($pageSize > 0) { 146 | for ($start = $pageSize; $start <= $json['total_count']; $start += $pageSize) { 147 | $this->makeRequest($appids, $start); 148 | } 149 | } 150 | } 151 | foreach($json['results'] as $item) { 152 | $asset = $item['asset_description']; 153 | 154 | $rarity = ERarity::Normal; 155 | $type = EType::Unknown; 156 | 157 | if ($item['app_name'] == "Steam") { 158 | if (preg_match( 159 | "#^(.+?)(?:\s+(Uncommon|Foil|Rare|))?\s+(Profile Background|Emoticon|Booster Pack|Trading Card|Sale Item)$#", 160 | $asset['type'] === "Booster Pack" ? $asset['name'] : $asset['type'], 161 | $m 162 | )) { 163 | $appName = $m[1]; 164 | 165 | $rarity = match($m[2]) { 166 | "Uncommon" => ERarity::Uncommon, 167 | "Foil" => ERarity::Foil, 168 | "Rare" => ERarity::Rare, 169 | default => ERarity::Normal 170 | }; 171 | 172 | $type = match($m[3]) { 173 | "Profile Background" => EType::Background, 174 | "Emoticon" => EType::Emoticon, 175 | "Booster Pack" => EType::Booster, 176 | "Trading Card" => EType::Card, 177 | "Sale Item" => EType::Item 178 | }; 179 | } else { 180 | $appName = $asset['type']; 181 | $this->logger->notice($appName); 182 | } 183 | } else { 184 | $appName = $item['app_name']; 185 | $type = match($asset['type']) { 186 | "Profile Background" => EType::Background, 187 | "Emoticon" => EType::Emoticon, 188 | "Booster Pack" => EType::Booster, 189 | "Trading Card" => EType::Card, 190 | "Sale Item" => EType::Item, 191 | default => EType::Unknown 192 | }; 193 | } 194 | 195 | list($appid) = explode("-", $item['hash_name'], 2); 196 | $this->insertQuery->stack( 197 | (new DMarketData()) 198 | ->setHashName($item['hash_name']) 199 | ->setAppid((int)$appid) 200 | ->setAppName($appName) 201 | ->setName($item['name']) 202 | ->setSellListings($item['sell_listings']) 203 | ->setSellPriceUsd($item['sell_price']) 204 | ->setImg($asset['icon_url']) 205 | ->setUrl($asset['appid']."/".rawurlencode($item['hash_name'])) 206 | ->setType($type) 207 | ->setRarity($rarity) 208 | ->setTimestamp(time()) 209 | ); 210 | } 211 | 212 | $this->insertQuery->persist(); 213 | --$this->requestCounter; 214 | 215 | $this->logger->info("", ["appids" => $appids, "start" => $json['start']]); 216 | } 217 | 218 | /** 219 | * @param list $appids 220 | */ 221 | private function updateIndex(array $appids): void { 222 | $i = new TMarketIndex(); 223 | $this->db->updateObj($i) 224 | ->columns($i->last_update, $i->request_counter) 225 | ->whereSql("$i->appid IN :appids", [":appids" => $appids]) 226 | ->update( 227 | (new DMarketIndex()) 228 | ->setLastUpdate($this->timestamp) 229 | ->setRequestCounter(0) 230 | ); 231 | } 232 | 233 | /** 234 | * @param list $appids 235 | */ 236 | private function cleanup(array $appids): void { 237 | if (count($appids) == 0) { return; } 238 | 239 | $d = new TMarketData(); 240 | $this->db->delete(<<appid IN :appids 243 | AND $d->timestamp < :timestamp 244 | SQL 245 | )->delete([ 246 | ":appids" => $appids, 247 | ":timestamp" => $this->timestamp 248 | ]); 249 | } 250 | 251 | public function update(): void { 252 | $this->logger->info("Update start"); 253 | 254 | for ($b = 0; $b < self::BatchCount; $b++) { 255 | $appids = $this->getAppidBatch(); 256 | if (count($appids) == 0) { break; } 257 | 258 | $this->makeRequest($appids, 0); 259 | 260 | $this->runLoader(); 261 | $this->updateIndex($appids); 262 | 263 | if ($this->requestCounter === 0) { 264 | $this->cleanup($appids); 265 | $this->logger->info("Batch done"); 266 | } else { 267 | $this->logger->notice("Batch failed to finish ({$this->requestCounter} requests left)"); 268 | } 269 | $this->requestCounter = 0; 270 | } 271 | 272 | $this->logger->info("Update done"); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/Data/Updaters/Rates/RatesUpdater.php: -------------------------------------------------------------------------------- 1 | logger->info("Start"); 21 | 22 | $rates = $this->provider->fetch(); 23 | if (empty($rates)) { 24 | throw new \Exception("No data"); 25 | } 26 | 27 | $timestamp = time(); 28 | $c = new TCurrency(); 29 | 30 | $this->db->begin(); 31 | $insert = $this->db->insert($c) 32 | ->stackSize(1000) 33 | ->columns($c->from, $c->to, $c->rate, $c->timestamp) 34 | ->onDuplicateKeyUpdate($c->rate, $c->timestamp); 35 | 36 | foreach($rates as $data) { 37 | $insert->stack((new DCurrency()) 38 | ->setFrom($data['from']) 39 | ->setTo($data['to']) 40 | ->setRate($data['rate']) 41 | ->setTimestamp($timestamp) 42 | ); 43 | } 44 | $insert->persist(); 45 | 46 | $this->db->delete(<<timestamp != :timestamp 49 | SQL 50 | )->delete([ 51 | ":timestamp" => $timestamp 52 | ]); 53 | $this->db->commit(); 54 | 55 | $this->logger->info("Done"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Database/DBadges.php: -------------------------------------------------------------------------------- 1 | id; 14 | } 15 | 16 | public function setId(int $id): self { 17 | $this->id = $id; 18 | return $this; 19 | } 20 | 21 | public function getTitle(): string { 22 | return $this->title; 23 | } 24 | 25 | public function setTitle(string $title): self { 26 | $this->title = $title; 27 | return $this; 28 | } 29 | 30 | public function getImg(): string { 31 | return $this->img; 32 | } 33 | 34 | public function setImg(string $img): self { 35 | $this->img = $img; 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Database/DCurrency.php: -------------------------------------------------------------------------------- 1 | from; 15 | } 16 | 17 | public function setFrom(string $from): self { 18 | $this->from = $from; 19 | return $this; 20 | } 21 | 22 | public function getTo(): string { 23 | return $this->to; 24 | } 25 | 26 | public function setTo(string $to): self { 27 | $this->to = $to; 28 | return $this; 29 | } 30 | 31 | public function getRate(): float { 32 | return $this->rate; 33 | } 34 | 35 | public function setRate(float $rate): self { 36 | $this->rate = $rate; 37 | return $this; 38 | } 39 | 40 | public function getTimestamp(): int { 41 | return $this->timestamp; 42 | } 43 | 44 | public function setTimestamp(int $timestamp): self { 45 | $this->timestamp = $timestamp; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Database/DDlcCategories.php: -------------------------------------------------------------------------------- 1 | id; 15 | } 16 | 17 | public function setId(int $id): self { 18 | $this->id = $id; 19 | return $this; 20 | } 21 | 22 | public function getName(): string { 23 | return $this->name; 24 | } 25 | 26 | public function setName(string $name): self { 27 | $this->name = $name; 28 | return $this; 29 | } 30 | 31 | public function getIcon(): string { 32 | return $this->icon; 33 | } 34 | 35 | public function setIcon(string $icon): self { 36 | $this->icon = $icon; 37 | return $this; 38 | } 39 | 40 | public function getDescription(): string { 41 | return $this->description; 42 | } 43 | 44 | public function setDescription(string $description): self { 45 | $this->description = $description; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Database/DMarketData.php: -------------------------------------------------------------------------------- 1 | hash_name; 25 | } 26 | 27 | public function setHashName(string $hash_name): self { 28 | $this->hash_name = $hash_name; 29 | return $this; 30 | } 31 | 32 | public function getAppid(): int { 33 | return $this->appid; 34 | } 35 | 36 | public function setAppid(int $appid): self { 37 | $this->appid = $appid; 38 | return $this; 39 | } 40 | 41 | public function getAppName(): string { 42 | return $this->appname; 43 | } 44 | 45 | public function setAppName(string $appname): self { 46 | $this->appname = $appname; 47 | return $this; 48 | } 49 | 50 | public function getName(): string { 51 | return $this->name; 52 | } 53 | 54 | public function setName(string $name): self { 55 | $this->name = $name; 56 | return $this; 57 | } 58 | 59 | public function getSellListings(): int { 60 | return $this->sell_listings; 61 | } 62 | 63 | public function setSellListings(int $sell_listings): self { 64 | $this->sell_listings = $sell_listings; 65 | return $this; 66 | } 67 | 68 | public function getSellPriceUsd(): int { 69 | return $this->sell_price_usd; 70 | } 71 | 72 | public function setSellPriceUsd(int $sell_price_usd): self { 73 | $this->sell_price_usd = $sell_price_usd; 74 | return $this; 75 | } 76 | 77 | public function getImg(): string { 78 | return $this->img; 79 | } 80 | 81 | public function setImg(string $img): self { 82 | $this->img = $img; 83 | return $this; 84 | } 85 | 86 | public function getUrl(): string { 87 | return $this->url; 88 | } 89 | 90 | public function setUrl(string $url): self { 91 | $this->url = $url; 92 | return $this; 93 | } 94 | 95 | public function getType(): EType { 96 | return $this->type; 97 | } 98 | 99 | public function setType(EType $type): self { 100 | $this->type = $type; 101 | return $this; 102 | } 103 | 104 | public function getRarity(): ERarity { 105 | return $this->rarity; 106 | } 107 | 108 | public function setRarity(ERarity $rarity): self { 109 | $this->rarity = $rarity; 110 | return $this; 111 | } 112 | 113 | public function getTimestamp(): int { 114 | return $this->timestamp; 115 | } 116 | 117 | public function setTimestamp(int $timestamp): self { 118 | $this->timestamp = $timestamp; 119 | return $this; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Database/DMarketIndex.php: -------------------------------------------------------------------------------- 1 | appid; 15 | } 16 | 17 | public function setAppid(int $appid): self { 18 | $this->appid = $appid; 19 | return $this; 20 | } 21 | 22 | public function getLastUpdate(): int { 23 | return $this->last_update; 24 | } 25 | 26 | public function setLastUpdate(int $last_update): self { 27 | $this->last_update = $last_update; 28 | return $this; 29 | } 30 | 31 | public function getLastRequest(): int { 32 | return $this->last_request; 33 | } 34 | 35 | public function setLastRequest(int $last_request): self { 36 | $this->last_request = $last_request; 37 | return $this; 38 | } 39 | 40 | public function getRequestCounter(): int { 41 | return $this->request_counter; 42 | } 43 | 44 | public function setRequestCounter(int $request_counter): self { 45 | $this->request_counter = $request_counter; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Database/DSession.php: -------------------------------------------------------------------------------- 1 | token; 15 | } 16 | 17 | public function setToken(string $token): self { 18 | $this->token = $token; 19 | return $this; 20 | } 21 | 22 | public function getHash(): string { 23 | return $this->hash; 24 | } 25 | 26 | public function setHash(string $hash): self { 27 | $this->hash = $hash; 28 | return $this; 29 | } 30 | 31 | public function getSteamId(): int { 32 | return $this->steam_id; 33 | } 34 | 35 | public function setSteamId(int $steam_id): self { 36 | $this->steam_id = $steam_id; 37 | return $this; 38 | } 39 | 40 | public function getExpiry(): int { 41 | return $this->expiry; 42 | } 43 | 44 | public function setExpiry(int $expiry): self { 45 | $this->expiry = $expiry; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Database/DUsersProfiles.php: -------------------------------------------------------------------------------- 1 | steam64; 15 | } 16 | 17 | public function setSteam64(int $steam64): self { 18 | $this->steam64 = $steam64; 19 | return $this; 20 | } 21 | 22 | public function getBgImg(): ?string { 23 | return $this->bg_img; 24 | } 25 | 26 | public function setBgImg(?string $bg_img): self { 27 | $this->bg_img = $bg_img; 28 | return $this; 29 | } 30 | 31 | public function getBgAppid(): ?int { 32 | return $this->bg_appid; 33 | } 34 | 35 | public function setBgAppid(?int $bg_appid): self { 36 | $this->bg_appid = $bg_appid; 37 | return $this; 38 | } 39 | 40 | public function getStyle(): ?string { 41 | return $this->style; 42 | } 43 | 44 | public function setStyle(?string $style): self { 45 | $this->style = $style; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Database/TBadges.php: -------------------------------------------------------------------------------- 1 | endpoints->getWSGFEndpoint(), $appid); 13 | } 14 | 15 | public function getSteamRep(int $steamId): string { 16 | return sprintf($this->endpoints->getSteamRepEndpoint(), $steamId); 17 | } 18 | 19 | public function getSteamPeek(int $appid): string { 20 | return sprintf($this->endpoints->getSteamPeekEndpoint(), $appid, $this->keys->getSteamPeekApiKey()); 21 | } 22 | 23 | public function getEarlyAccess(): string { 24 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 25 | $key = $this->keys->getIsThereAnyDealApiKey(); 26 | return $host."/internal/early-access/v1?key={$key}"; 27 | } 28 | 29 | public function getSteamIdLookup(): string { 30 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 31 | $key = $this->keys->getIsThereAnyDealApiKey(); 32 | return $host."/lookup/id/shop/61/v1?key={$key}"; 33 | } 34 | 35 | /** 36 | * @param list $shops 37 | */ 38 | public function getPrices(string $country, array $shops, bool $withVouchers): string { 39 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 40 | $key = $this->keys->getIsThereAnyDealApiKey(); 41 | return $host."/games/overview/v2?".http_build_query([ 42 | "key" => $key, 43 | "country" => $country, 44 | "shops" => implode(",", $shops), 45 | "vouchers" => $withVouchers 46 | ]); 47 | } 48 | 49 | public function getTwitchStream(string $channel): string { 50 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 51 | $key = $this->keys->getIsThereAnyDealApiKey(); 52 | return $host."/internal/twitch/stream/v1?key={$key}&channel={$channel}"; 53 | } 54 | 55 | public function getPlayers(int $appid): string { 56 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 57 | $key = $this->keys->getIsThereAnyDealApiKey(); 58 | return $host."/internal/players/v1?key={$key}&appid={$appid}"; 59 | } 60 | 61 | public function getReviews(int $appid): string { 62 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 63 | $key = $this->keys->getIsThereAnyDealApiKey(); 64 | return $host."/internal/reviews/v1?key={$key}&appid={$appid}"; 65 | } 66 | 67 | public function getHLTB(int $appid): string { 68 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 69 | $key = $this->keys->getIsThereAnyDealApiKey(); 70 | return $host."/internal/hltb/v1?key={$key}&appid={$appid}"; 71 | } 72 | 73 | public function getRates(): string { 74 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 75 | $key = $this->keys->getIsThereAnyDealApiKey(); 76 | return $host."/internal/rates/v1?key={$key}"; 77 | } 78 | 79 | public function getExfgls(): string { 80 | $host = $this->endpoints->getIsThereAnyDealApiHost(); 81 | $key = $this->keys->getIsThereAnyDealApiKey(); 82 | return $host."/internal/exfgls/v1?key={$key}"; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Endpoints/EndpointsConfig.php: -------------------------------------------------------------------------------- 1 | config = (new Processor())->process(Expect::structure([ 25 | "wsgf" => Expect::string()->required(), 26 | "steamspy" => Expect::string()->required(), 27 | "steamrep" => Expect::string()->required(), 28 | "steampeek" => Expect::string()->required(), 29 | "itad" => Expect::string()->required() 30 | ]), $config); 31 | } 32 | 33 | public function getWSGFEndpoint(): string { 34 | return $this->config->wsgf; 35 | } 36 | 37 | public function getSteamSpyEndpoint(int $appid): string { 38 | return sprintf($this->config->steamspy, $appid); 39 | } 40 | 41 | public function getSteamRepEndpoint(): string { 42 | return $this->config->steamrep; 43 | } 44 | 45 | public function getSteamPeekEndpoint(): string { 46 | return $this->config->steampeek; 47 | } 48 | 49 | public function getIsThereAnyDealApiHost(): string { 50 | return $this->config->itad; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Endpoints/KeysConfig.php: -------------------------------------------------------------------------------- 1 | config = (new Processor())->process(Expect::structure([ 22 | "itad" => Expect::string()->required(), 23 | "steampeek" => Expect::string()->required() 24 | ]), $config); 25 | } 26 | 27 | public function getIsThereAnyDealApiKey(): string { 28 | return $this->config->itad; 29 | } 30 | 31 | public function getSteamPeekApiKey(): string { 32 | return $this->config->steampeek; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Environment/Lock.php: -------------------------------------------------------------------------------- 1 | path = TEMP_DIR . "/locks/" . basename($name, ".lock") . ".lock"; 12 | } 13 | 14 | public function isLocked(int $seconds): bool { 15 | clearstatcache(); 16 | if (file_exists($this->path)) { 17 | $timestamp = (int)file_get_contents($this->path); 18 | return $timestamp + $seconds >= time(); 19 | } 20 | return false; 21 | } 22 | 23 | public function lock(): void { 24 | $dir = dirname($this->path); 25 | if (!file_exists($dir)) { 26 | mkdir($dir); 27 | } 28 | file_put_contents($this->path, time()); 29 | } 30 | 31 | public function unlock(): void { 32 | clearstatcache(); 33 | if (file_exists($this->path)) { 34 | unlink($this->path); 35 | } 36 | } 37 | 38 | public function tryLock(int $seconds): void { 39 | if ($this->isLocked($seconds)) { 40 | if (php_sapi_name() == "cli") { 41 | echo "Locked by " . basename($this->path); 42 | } 43 | die(); 44 | } 45 | $this->lock(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exceptions/ApiException.php: -------------------------------------------------------------------------------- 1 | headers['content-type'] = 'application/json'; 20 | 21 | foreach ($this->headers as $key => $value) { 22 | /** @var ResponseInterface $response */ 23 | $response = $response->withAddedHeader($key, $value); 24 | } 25 | 26 | if ($response->getBody()->isWritable()) { 27 | $response->getBody()->write(json_encode([ 28 | "error" => $this->errorCode, 29 | "error_description" => $this->errorMessage, 30 | "status_code" => $this->status, 31 | "reason_phrase" => $this->message 32 | ], flags: JSON_THROW_ON_ERROR)); 33 | } 34 | 35 | return $response->withStatus($this->status, $this->message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidValueException.php: -------------------------------------------------------------------------------- 1 | db = $db; 16 | $this->c = new TCache(); 17 | } 18 | 19 | #[\Override] 20 | public function has(ECacheKey $key, string $field): bool { 21 | $c = $this->c; 22 | 23 | return $this->db->select(<<key=:key 27 | AND $c->field=:field 28 | AND $c->expiry > UNIX_TIMESTAMP() 29 | SQL 30 | )->exists([ 31 | ":key" => $key, 32 | ":field" => $field 33 | ]); 34 | } 35 | 36 | #[\Override] 37 | public function get(ECacheKey $key, string $field): mixed { 38 | $c = $this->c; 39 | 40 | $data = $this->db->select(<<data 42 | FROM $c 43 | WHERE $c->key=:key 44 | AND $c->field=:field 45 | AND $c->expiry > UNIX_TIMESTAMP() 46 | SQL 47 | )->fetchValue([ 48 | ":key" => $key, 49 | ":field" => $field 50 | ]); 51 | 52 | if (!is_null($data)) { 53 | try { 54 | if (!is_string($data)) { 55 | throw new \Exception(); 56 | } 57 | return igbinary_unserialize($data); 58 | } catch(\Throwable) { 59 | $this->db->delete(<<key=:key 62 | AND $c->field=:field 63 | SQL 64 | )->delete([ 65 | ":key" => $key, 66 | ":field" => $field 67 | ]); 68 | } 69 | } 70 | 71 | return null; 72 | } 73 | 74 | #[\Override] 75 | public function set(ECacheKey $key, string $field, mixed $data, int $ttl): void { 76 | $c = $this->c; 77 | 78 | $this->db->insert($c) 79 | ->columns($c->key, $c->field, $c->data, $c->expiry) 80 | ->onDuplicateKeyUpdate($c->data, $c->expiry) 81 | ->persist((new DCache()) 82 | ->setKey($key) 83 | ->setField($field) 84 | ->setData(igbinary_serialize($data)) 85 | ->setExpiry(time()+$ttl) 86 | ) 87 | ; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Lib/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | key; 15 | } 16 | 17 | public function setKey(ECacheKey $key): self { 18 | $this->key = $key; 19 | return $this; 20 | } 21 | 22 | public function getField(): string { 23 | return $this->field; 24 | } 25 | 26 | public function setField(string $field): self { 27 | $this->field = $field; 28 | return $this; 29 | } 30 | 31 | public function getData(): ?string { 32 | return $this->data; 33 | } 34 | 35 | public function setData(?string $data): self { 36 | $this->data = $data; 37 | return $this; 38 | } 39 | 40 | public function getExpiry(): int { 41 | return $this->expiry; 42 | } 43 | 44 | public function setExpiry(int $expiry): self { 45 | $this->expiry = $expiry; 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lib/Cache/ECacheKey.php: -------------------------------------------------------------------------------- 1 | getQueryParams(); 19 | 20 | if (array_key_exists($name, $params)) { 21 | $this->value = match($params[$name]) { 22 | "1", "true" => true, 23 | "0", "false" => false, 24 | default => throw new InvalidValueException($name) 25 | }; 26 | } else { 27 | if (is_null($default) && !$nullable) { 28 | throw new MissingParameterException($name); 29 | } 30 | 31 | $this->value = $default; 32 | } 33 | } 34 | 35 | public function value(): ?bool { 36 | return $this->value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lib/Http/IntParam.php: -------------------------------------------------------------------------------- 1 | getQueryParams(); 19 | 20 | if (array_key_exists($name, $params)) { 21 | $value = $params[$name]; 22 | 23 | if (!preg_match("#^[0-9]+$#", $value)) { 24 | throw new InvalidValueException($name); 25 | } 26 | $this->value = intval($value); 27 | } else { 28 | if (is_null($default) && !$nullable) { 29 | throw new MissingParameterException($name); 30 | } 31 | 32 | $this->value = $default; 33 | } 34 | } 35 | 36 | public function value(): ?int { 37 | return $this->value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lib/Http/ListParam.php: -------------------------------------------------------------------------------- 1 | |null */ 10 | private ?array $value; 11 | 12 | /** 13 | * @param list|null $default 14 | * @param non-empty-string $separator 15 | */ 16 | public function __construct( 17 | ServerRequestInterface $request, 18 | string $name, 19 | string $separator = ",", 20 | ?array $default = null, 21 | bool $nullable = false 22 | ) { 23 | $params = $request->getQueryParams(); 24 | 25 | if (array_key_exists($name, $params) && is_string($params[$name])) { 26 | $this->value = explode($separator, $params[$name]); 27 | } else { 28 | if (is_null($default) && !$nullable) { 29 | throw new MissingParameterException($name); 30 | } 31 | 32 | $this->value = $default; 33 | } 34 | } 35 | 36 | /** 37 | * @return ?list 38 | */ 39 | public function value(): ?array { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Lib/Http/StringParam.php: -------------------------------------------------------------------------------- 1 | getQueryParams(); 19 | 20 | if (array_key_exists($name, $params)) { 21 | $value = trim($params[$name]); 22 | 23 | if (empty($value)) { 24 | throw new InvalidValueException($name); 25 | } 26 | $this->value = $value; 27 | } else { 28 | if (is_null($default) && !$nullable) { 29 | throw new MissingParameterException($name); 30 | } 31 | 32 | $this->value = $default; 33 | } 34 | } 35 | 36 | public function value(): ?string { 37 | return $this->value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lib/Loader/Crawler.php: -------------------------------------------------------------------------------- 1 | requestQueue = new SplQueue(); 20 | } 21 | 22 | protected function enqueueRequest(Item $item): void { 23 | $request = $this->loader->createRequest( 24 | $item, 25 | fn(Item $item, ResponseInterface $response, string $effectiveUri) => $this->successHandler($item, $response, $effectiveUri), 26 | fn(Item $item, Throwable $e) => $this->errorHandler($item, $e) 27 | ); 28 | $this->requestQueue->enqueue($request); 29 | } 30 | 31 | protected function mayProcess(Item $request, ResponseInterface $response, int $maxAttempts): bool { 32 | $status = $response->getStatusCode(); 33 | 34 | if ($status !== 200) { 35 | if ($status === 429) { 36 | $this->logger->info("Throttling"); 37 | sleep(60); 38 | } 39 | if ($request->getAttempt() <= $maxAttempts) { 40 | // replay request 41 | $request->incrementAttempt(); 42 | $this->enqueueRequest($request); 43 | $this->logger->info("Retrying", ["url" => $request->getUrl()]); 44 | } else { 45 | $this->logger->error($request->getUrl()); 46 | } 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | 53 | protected abstract function successHandler(Item $request, ResponseInterface $response, string $effectiveUri): void; 54 | 55 | protected function errorHandler(Item $item, Throwable $e): void { 56 | $this->logger->error($e->getMessage().": ".$item->getUrl(), $item->getData()); 57 | } 58 | 59 | protected function requestGenerator(): \Generator { 60 | while(true) { 61 | if ($this->requestQueue->isEmpty()) { break; } 62 | yield $this->requestQueue->dequeue(); 63 | } 64 | } 65 | 66 | protected function runLoader(): void { 67 | while (true) { 68 | if ($this->requestQueue->isEmpty()) { break; } 69 | $this->loader->run($this->requestGenerator()); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Lib/Loader/Item.php: -------------------------------------------------------------------------------- 1 | */ 10 | private array $curlOptions = []; 11 | private bool $allowRedirect = true; 12 | private ?string $body = null; 13 | /** @var array */ 14 | private array $headers = []; 15 | /** @var array */ 16 | private array $data = []; 17 | 18 | private int $attempt = 1; 19 | 20 | public function __construct(string $url) { 21 | $this->id = uniqid("", true); 22 | $this->url = $url; 23 | } 24 | 25 | public function getId(): string { 26 | return $this->id; 27 | } 28 | 29 | public function getMethod(): string { 30 | return $this->method; 31 | } 32 | 33 | public function setMethod(string $method): self { 34 | $this->method = $method; 35 | return $this; 36 | } 37 | 38 | public function getUrl(): string { 39 | return $this->url; 40 | } 41 | 42 | public function setUrl(string $url): self { 43 | $this->url = $url; 44 | return $this; 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function getCurlOptions(): array { 51 | return $this->curlOptions; 52 | } 53 | 54 | /** 55 | * @param array $options 56 | */ 57 | public function setCurlOptions(array $options): self { 58 | $this->curlOptions = $options; 59 | return $this; 60 | } 61 | 62 | public function isAllowRedirect(): bool { 63 | return $this->allowRedirect; 64 | } 65 | 66 | public function setAllowRedirect(bool $allowRedirect): self { 67 | $this->allowRedirect = $allowRedirect; 68 | return $this; 69 | } 70 | 71 | public function getBody(): ?string { 72 | return $this->body; 73 | } 74 | 75 | public function setBody(?string $body): self { 76 | $this->body = $body; 77 | return $this; 78 | } 79 | 80 | /** 81 | * @return array 82 | */ 83 | public function getHeaders(): array { 84 | return $this->headers; 85 | } 86 | 87 | /** 88 | * @param array $headers 89 | */ 90 | public function setHeaders(array $headers): self { 91 | $this->headers = $headers; 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getData(): array { 99 | return $this->data; 100 | } 101 | 102 | /** 103 | * @param array $data 104 | */ 105 | public function setData(array $data): self { 106 | $this->data = $data; 107 | return $this; 108 | } 109 | 110 | public function getAttempt(): int { 111 | return $this->attempt; 112 | } 113 | 114 | public function setAttempt(int $attempt): self { 115 | $this->attempt = $attempt; 116 | return $this; 117 | } 118 | 119 | public function incrementAttempt(): self { 120 | $this->attempt += 1; 121 | return $this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Lib/Loader/Loader.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 28 | $this->guzzle = $guzzle; 29 | } 30 | 31 | public function setConcurrency(int $concurrency): self { 32 | $this->concurrency = $concurrency; 33 | return $this; 34 | } 35 | 36 | public function setProxy(?ProxyInterface $proxy): self { 37 | $this->proxy = $proxy; 38 | return $this; 39 | } 40 | 41 | public function createRequest(Item $item, callable $responseHandler, callable $errorHandler): callable { 42 | 43 | return function() use ($item, $responseHandler, $errorHandler) { 44 | 45 | $curlOptions = array_replace( 46 | is_null($this->proxy) ? [] : $this->proxy->getCurlOptions(), 47 | $item->getCurlOptions() 48 | ); 49 | $curlOptions[CURLOPT_FOLLOWLOCATION] = false; // Force CURL follow location off, and let Guzzle handle it 50 | 51 | $settings = [ 52 | "http_errors" => false, // do not throw on 400 and 500 level errors 53 | "connect_timeout" => self::ConnectTimeout, 54 | "timeout" => self::Timeout, 55 | "allow_redirects" => [ 56 | "track_redirects" => true 57 | ], 58 | "headers" => [ 59 | "Accept-Encoding" => "gzip,deflate" 60 | ], 61 | "curl" => $curlOptions 62 | ]; 63 | 64 | foreach($item->getHeaders() as $header => $value) { 65 | $settings['headers'][$header] = $value; 66 | } 67 | 68 | if (!empty($item->getBody())) { 69 | $settings['body'] = $item->getBody(); 70 | } 71 | 72 | return $this->guzzle 73 | ->requestAsync($item->getMethod(), $item->getUrl(), $settings) 74 | ->then( 75 | function(ResponseInterface $response) use($item, $responseHandler) { 76 | try { 77 | $redirects = $response->getHeader("X-Guzzle-Redirect-History"); 78 | $uri = (empty($redirects) ? $item->getUrl() : end($redirects)); 79 | 80 | $responseHandler($item, $response, $uri); 81 | } catch (Throwable $e) { 82 | $this->logger->error($e->getMessage()); 83 | } 84 | }, 85 | function(GuzzleException $e) use($item, $errorHandler) { 86 | try { 87 | $errorHandler($item, $e); 88 | } catch (Throwable $e) { 89 | $this->logger->error($e->getMessage()); 90 | } 91 | } 92 | ); 93 | }; 94 | } 95 | 96 | 97 | /** 98 | * @param Iterator $requests 99 | */ 100 | public function run(Iterator $requests): void { 101 | $pool = new Pool($this->guzzle, $requests, [ 102 | "concurrency" => $this->concurrency, 103 | ]); 104 | $pool 105 | ->promise() 106 | ->wait(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Lib/Loader/Proxy/BrightDataProxy.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | 14 | /** 15 | * @return array 16 | */ 17 | public function getCurlOptions(): array { 18 | $rand = mt_rand(10000, 99999); 19 | 20 | $setup = "lum-customer-{$this->config->getUser()}-zone-{$this->config->getZone()}-session-rand{$rand}"; 21 | 22 | return [ 23 | CURLOPT_PROXY => $this->config->getUrl(), 24 | CURLOPT_PROXYPORT => $this->config->getPort(), 25 | CURLOPT_PROXYUSERPWD => "$setup:{$this->config->getPassword()}" 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Lib/Loader/Proxy/ProxyFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 14 | } 15 | 16 | public function createProxy(): ProxyInterface { 17 | return new BrightDataProxy($this->config); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Lib/Loader/Proxy/ProxyFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | public function getCurlOptions(): array; 10 | } 11 | -------------------------------------------------------------------------------- /src/Lib/Loader/SimpleLoader.php: -------------------------------------------------------------------------------- 1 | $curlOptions 18 | */ 19 | public function get(string $url, array $curlOptions = []): ?ResponseInterface { 20 | try { 21 | return $this->guzzle->get($url, [ 22 | "headers" => [ 23 | "User-Agent" => "AugmentedSteam/1.0 (+bots@isthereanydeal.com)", 24 | ], 25 | "curl" => $curlOptions 26 | ]); 27 | } catch (GuzzleException $e) { 28 | \Sentry\captureException($e); 29 | } 30 | return null; 31 | } 32 | 33 | /** 34 | * @param array $curlOptions 35 | */ 36 | public function post(string $url, mixed $body, array $curlOptions = []): ?ResponseInterface { 37 | try { 38 | return $this->guzzle->post($url, [ 39 | "headers" => [ 40 | "User-Agent" => "AugmentedSteam/1.0 (+bots@isthereanydeal.com)", 41 | ], 42 | "body" => $body, 43 | "curl" => $curlOptions 44 | ]); 45 | } catch (GuzzleException $e) { 46 | \Sentry\captureException($e); 47 | } 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Lib/Logging/LoggerFactory.php: -------------------------------------------------------------------------------- 1 | ignoreEmptyContextAndExtra(); 25 | } 26 | 27 | private function getFileHandler(string $channel): StreamHandler { 28 | $date = date("Y-m-d"); 29 | $logPath = $this->logsPath."/{$date}.{$channel}.log"; 30 | return (new StreamHandler($logPath, Level::Debug, true, 0666)); 31 | } 32 | 33 | public function getNullLogger(): LoggerInterface { 34 | return new Logger("null", [new NullHandler()]); 35 | } 36 | 37 | public function logger(string $channel): LoggerInterface { 38 | if (!$this->config->isEnabled()) { 39 | return $this->getNullLogger(); 40 | } 41 | 42 | $lineFormatter = $this->getLineFormatter(); 43 | 44 | $fileHandler = $this->getFileHandler($channel); 45 | $fileHandler->setFormatter($lineFormatter); 46 | 47 | return (new Logger($channel)) 48 | ->pushHandler($fileHandler) 49 | ->pushProcessor(new UidProcessor()); 50 | } 51 | 52 | public function access(): LoggerInterface { 53 | if (!$this->config->isEnabled()) { 54 | return $this->getNullLogger(); 55 | } 56 | 57 | $lineFormatter = $this->getLineFormatter(); 58 | 59 | $channel = "access"; 60 | $fileHandler = $this->getFileHandler($channel); 61 | $fileHandler->setFormatter($lineFormatter); 62 | 63 | return (new Logger($channel)) 64 | ->pushHandler($fileHandler) 65 | ->pushProcessor(new UidProcessor()) 66 | ->pushProcessor(new WebProcessor(extraFields: ["ip", "server", "referrer"])); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Lib/Logging/LoggerFactoryInterface.php: -------------------------------------------------------------------------------- 1 | $config 18 | */ 19 | public function __construct(array $config) { 20 | // @phpstan-ignore-next-line 21 | $this->config = (new Processor())->process(Expect::structure([ 22 | "enabled" => Expect::bool(true) 23 | ]), $config); 24 | } 25 | 26 | public function isEnabled(): bool { 27 | return $this->config->enabled; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Lib/Money/CurrencyConverter.php: -------------------------------------------------------------------------------- 1 | db->select(<<rate 27 | FROM $c 28 | WHERE $c->from=:from 29 | AND $c->to=:to 30 | SQL 31 | )->params([ 32 | ":from" => $from, 33 | ":to" => $to 34 | ])->fetch(DCurrency::class) 35 | ->getOne(); 36 | 37 | return $data?->getRate(); 38 | } 39 | 40 | /** 41 | * @param list $currencies 42 | * @return array> 43 | */ 44 | public function getAllConversionsTo(array $currencies): array { 45 | $c = new TCurrency(); 46 | 47 | $select = $this->db->select(<<from, $c->to, $c->rate 49 | FROM $c 50 | WHERE $c->to IN :to 51 | SQL 52 | )->params([ 53 | ":to" => $currencies 54 | ])->fetch(DCurrency::class); 55 | 56 | $result = []; 57 | foreach($select as $o) { 58 | $result[$o->getFrom()][$o->getTo()] = $o->getRate(); 59 | } 60 | return $result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Lib/OpenId/OpenId.php: -------------------------------------------------------------------------------- 1 | provider = new OpenIdProvider("https://".$host, $returnPath); 13 | } 14 | 15 | public function isAuthenticationStarted(): bool { 16 | return $this->provider->isAuthenticationInProgress(); 17 | } 18 | 19 | public function getAuthUrl(): Uri { 20 | return Uri::new($this->provider->getAuthUrl()); 21 | } 22 | 23 | public function getSteamId(): string { 24 | return $this->steamId; 25 | } 26 | 27 | public function authenticate(): bool { 28 | $result = $this->provider->validateLogin(); 29 | if (is_null($result)) { 30 | return false; 31 | } 32 | 33 | $this->steamId = $result; 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Lib/OpenId/OpenIdProvider.php: -------------------------------------------------------------------------------- 1 | host = $host; 19 | $this->returnUrl = rtrim($host, "/")."/".ltrim($returnPath, "/"); 20 | } 21 | 22 | public function isAuthenticationInProgress(): bool { 23 | return isset($_GET['openid_claimed_id']); 24 | } 25 | 26 | public function getAuthUrl(): string { 27 | return self::ProviderLoginUrl."?".http_build_query([ 28 | "openid.identity" => "http://specs.openid.net/auth/2.0/identifier_select", 29 | "openid.claimed_id" => "http://specs.openid.net/auth/2.0/identifier_select", 30 | "openid.ns" => "http://specs.openid.net/auth/2.0", 31 | "openid.mode" => "checkid_setup", 32 | "openid.realm" => $this->host, 33 | "openid.return_to" => $this->returnUrl 34 | ]); 35 | } 36 | 37 | /** 38 | * Validates OpenID data, and verifies with Steam 39 | * @return ?string Returns the 64-bit SteamID if successful or null on failure 40 | */ 41 | public function validateLogin(): ?string { 42 | // PHP automatically replaces dots with underscores in GET parameters 43 | // See https://www.php.net/variables.external#language.variables.external.dot-in-names 44 | if (filter_input(INPUT_GET, 'openid_mode') !== "id_res") { 45 | return null; 46 | } 47 | 48 | // See http://openid.net/specs/openid-authentication-2_0.html#positive_assertions 49 | $arguments = filter_input_array(INPUT_GET, [ 50 | "openid_ns" => FILTER_SANITIZE_URL, 51 | "openid_op_endpoint" => FILTER_SANITIZE_URL, 52 | "openid_claimed_id" => FILTER_SANITIZE_URL, 53 | "openid_identity" => FILTER_SANITIZE_URL, 54 | "openid_return_to" => FILTER_SANITIZE_URL, // Should equal to url we sent 55 | "openid_response_nonce" => FILTER_SANITIZE_SPECIAL_CHARS, 56 | "openid_assoc_handle" => FILTER_SANITIZE_SPECIAL_CHARS, // Steam just sends 1234567890 57 | "openid_signed" => FILTER_SANITIZE_SPECIAL_CHARS, 58 | "openid_sig" => FILTER_SANITIZE_SPECIAL_CHARS 59 | ], true); 60 | 61 | if (!is_array($arguments)) { 62 | return null; 63 | } 64 | 65 | foreach ($arguments as $value) { 66 | // An array value will be FALSE if the filter fails, or NULL if the variable is not set. 67 | // In our case we want everything to be a string. 68 | if (!is_string($value)) { 69 | return null; 70 | } 71 | } 72 | 73 | if ($arguments['openid_claimed_id'] !== $arguments['openid_identity'] 74 | || $arguments['openid_op_endpoint'] !== self::ProviderLoginUrl 75 | || $arguments['openid_ns'] !== "http://specs.openid.net/auth/2.0" 76 | || !is_string($arguments['openid_return_to']) 77 | || !is_string($arguments['openid_identity']) 78 | || strpos($arguments['openid_return_to'], $this->returnUrl) !== 0 79 | || preg_match("#^https?://steamcommunity.com/openid/id/(7656119\d{10})/?$#", $arguments['openid_identity'], $communityID) !== 1) { 80 | return null; 81 | } 82 | 83 | $arguments['openid_mode'] = "check_authentication"; 84 | 85 | $c = curl_init(); 86 | curl_setopt_array($c, [ 87 | CURLOPT_USERAGENT => "OpenID Verification (+https://github.com/xPaw/SteamOpenID.php)", 88 | CURLOPT_URL => self::ProviderLoginUrl, 89 | CURLOPT_RETURNTRANSFER => true, 90 | CURLOPT_CONNECTTIMEOUT => 6, 91 | CURLOPT_TIMEOUT => 6, 92 | CURLOPT_POST => true, 93 | CURLOPT_POSTFIELDS => $arguments, 94 | ]); 95 | 96 | $response = curl_exec($c); 97 | $code = curl_getinfo($c, CURLINFO_RESPONSE_CODE); 98 | 99 | curl_close($c); 100 | 101 | if ($code !== 200 || !is_string($response)) { 102 | return null; 103 | } 104 | 105 | $keyValues = $this->parseKeyValues($response); 106 | if (($keyValues['ns'] ?? null) !== "http://specs.openid.net/auth/2.0") { 107 | return null; 108 | } 109 | 110 | if (($keyValues['is_valid'] ?? null) !== "true") { 111 | return null; 112 | } 113 | 114 | return $communityID[1]; 115 | } 116 | 117 | /** 118 | * @return array 119 | */ 120 | private function parseKeyValues(string $response): array 121 | { 122 | // A message in Key-Value form is a sequence of lines. Each line begins with a key, 123 | // followed by a colon, and the value associated with the key. The line is terminated 124 | // by a single newline (UCS codepoint 10, "\n"). A key or value MUST NOT contain a 125 | // newline and a key also MUST NOT contain a colon. 126 | $responseLines = explode("\n", $response); 127 | $responseKeys = []; 128 | 129 | foreach($responseLines as $line) { 130 | $pair = explode(":", $line, 2); 131 | 132 | if (!isset($pair[1])) { 133 | continue; 134 | } 135 | 136 | list($key, $value) = $pair; 137 | $responseKeys[$key] = $value; 138 | } 139 | 140 | return $responseKeys; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Lib/OpenId/Session.php: -------------------------------------------------------------------------------- 1 | getCookieParams(); 22 | if (!isset($cookie[self::CookieName])) { 23 | return false; 24 | } 25 | 26 | $sessionCookie = $cookie[self::CookieName]; 27 | $parts = explode(":", $sessionCookie); 28 | if (count($parts) != 2) { 29 | return false; 30 | } 31 | 32 | $s = new TSessions(); 33 | /** @var ?DSession $session */ 34 | $session = $this->db->select(<<hash, $s->steam_id 36 | FROM $s 37 | WHERE $s->token = :token 38 | AND $s->expiry >= :timestamp 39 | SQL 40 | )->params([ 41 | ":token" => $parts[0], 42 | ":timestamp" => time() 43 | ])->fetch(DSession::class) 44 | ->getOne(); 45 | 46 | return !is_null($session) 47 | && $session->getSteamId() === $steamId 48 | && hash_equals($session->getHash(), hash(self::HashAlgorithm, $parts[1])); 49 | } 50 | 51 | private function saveSession(int $steamId): void { 52 | $token = bin2hex(openssl_random_pseudo_bytes(5)); 53 | $validator = bin2hex(openssl_random_pseudo_bytes(20)); 54 | $expiry = time() + self::CookieExpiry; 55 | 56 | setcookie(self::CookieName, "{$token}:{$validator}", $expiry, "/"); 57 | 58 | $s = new TSessions(); 59 | 60 | $this->db->delete(<<expiry < :timestamp 63 | SQL 64 | )->delete([ 65 | ":timestamp" => time() 66 | ]); 67 | 68 | $this->db->insert($s) 69 | ->columns($s->token, $s->hash, $s->steam_id, $s->expiry) 70 | ->persist((new DSession()) 71 | ->setToken($token) 72 | ->setHash(hash(self::HashAlgorithm, $validator)) 73 | ->setSteamId($steamId) 74 | ->setExpiry($expiry) 75 | ); 76 | } 77 | 78 | public function authorize(ServerRequestInterface $request, string $selfPath, string $errorUrl, ?int $steamId): int|RedirectResponse { 79 | if (is_null($steamId) || !$this->hasSession($request, $steamId)) { 80 | $openId = new OpenId($this->host, $selfPath); 81 | 82 | if (!$openId->isAuthenticationStarted()) { 83 | return new RedirectResponse($openId->getAuthUrl()->toString()); 84 | } 85 | 86 | if (!$openId->authenticate()) { 87 | return new RedirectResponse($errorUrl); 88 | } else { 89 | $steamId = (int)($openId->getSteamId()); 90 | $this->saveSession($steamId); 91 | } 92 | } 93 | 94 | return $steamId; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Lib/Redis/ERedisKey.php: -------------------------------------------------------------------------------- 1 | value 10 | : "{$this->value}:{$suffix}"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Lib/Redis/RedisClient.php: -------------------------------------------------------------------------------- 1 | $config->getScheme(), 9 | "host" => $config->getHost(), 10 | "port" => $config->getPort(), 11 | "database" => $config->getDatabase() 12 | ], [ 13 | "prefix" => $config->getPrefix(), 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Lib/Redis/RedisConfig.php: -------------------------------------------------------------------------------- 1 | $config */ 21 | public function __construct(array $config) { 22 | $this->data = (new Processor())->process( // @phpstan-ignore-line 23 | Expect::structure([ 24 | "scheme" => Expect::anyOf("tcp")->required(), 25 | "host" => Expect::string()->required(), 26 | "port" => Expect::int(6379), 27 | "prefix" => Expect::string()->required(), 28 | "database" => Expect::int()->required() 29 | ]), 30 | $config 31 | ); 32 | } 33 | 34 | public function getScheme(): string { 35 | return $this->data->scheme; 36 | } 37 | 38 | public function getHost(): string { 39 | return $this->data->host; 40 | } 41 | 42 | public function getPort(): int { 43 | return $this->data->port; 44 | } 45 | 46 | public function getPrefix(): string { 47 | return $this->data->prefix; 48 | } 49 | 50 | public function getDatabase(): int { 51 | return $this->data->database; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Routing/Middleware/AccessLogMiddleware.php: -------------------------------------------------------------------------------- 1 | getMethod(); 20 | $path = $request->getUri()->getPath(); 21 | $this->logger->info("{$method} {$path}", $request->getQueryParams()); 22 | return $handler->handle($request); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/Routing/Middleware/IpThrottleMiddleware.php: -------------------------------------------------------------------------------- 1 | getServerParams(); 28 | $ip = $server['REMOTE_ADDR']; 29 | 30 | $key = ERedisKey::ApiThrottleIp->getKey($ip); 31 | $count = $this->redis->get($key); 32 | 33 | if (!is_null($count) && $count >= self::Requests) { 34 | $expireTime = $this->redis->expiretime($key); 35 | if ($expireTime > 0) { 36 | $this->logger->info("{$ip} throttled", $request->getQueryParams()); 37 | return new EmptyResponse(429, [ 38 | "Retry-After" => $expireTime - time() 39 | ]); 40 | } 41 | } 42 | $this->redis->incr($key); 43 | $this->redis->expire($key, self::WindowLength, "NX"); 44 | 45 | return $handler->handle($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Routing/Router.php: -------------------------------------------------------------------------------- 1 | get("/rates/v1", [RatesController::class, "rates_v1"]); 33 | $router->get("/early-access/v1", [EarlyAccessController::class, "appids_v1"]); 34 | 35 | $router->post("/prices/v2", [PricesController::class, "prices_v2"]); 36 | $router->get("/app/{appid:\d+}/v2", [AppController::class, "appInfo_v2"]); 37 | $router->get("/dlc/{appid:\d+}/v2", [DLCController::class, "dlcInfo_v2"]); 38 | $router->get("/similar/{appid:\d+}/v2", [SimilarController::class, "similar_v2"]); 39 | 40 | $router->get("/twitch/{channel}/stream/v2", [TwitchController::class, "stream_v2"]); 41 | 42 | $router->group("/market", function(RouteGroup $g) { 43 | $g->get("/cards/v2", [MarketController::class, "cards_v2"]); 44 | $g->get("/cards/average-prices/v2", [MarketController::class, "averageCardPrices_v2"]); 45 | }); 46 | 47 | $router->group("/profile", function(RouteGroup $g) { 48 | $g->get("/{steamId:\d+}/v2", [ProfileController::class, "profile_v2"]); 49 | $g->get("/background/list/v2", [ProfileManagementController::class, "backgrounds_v2"]); 50 | $g->get("/background/games/v1", [ProfileManagementController::class, "games_v1"]); 51 | $g->get("/background/delete/v2", [ProfileManagementController::class, "deleteBackground_v2"]); 52 | $g->get("/background/save/v2", [ProfileManagementController::class, "saveBackground_v2"]); 53 | $g->get("/style/delete/v2", [ProfileManagementController::class, "deleteStyle_v2"]); 54 | $g->get("/style/save/v2", [ProfileManagementController::class, "saveStyle_v2"]); 55 | }); 56 | } 57 | 58 | public function route(Container $container): void { 59 | $redis = $container->get(RedisClient::class); 60 | 61 | $strategy = new ApiStrategy( 62 | $container->get(CoreConfig::class), 63 | $container->get(ResponseFactoryInterface::class) 64 | ); 65 | $strategy->setContainer($container); 66 | 67 | $router = new \League\Route\Router(); 68 | 69 | $logger = $container->get(LoggerFactoryInterface::class)->access(); 70 | $router->middleware(new AccessLogMiddleware($logger)); 71 | $router->middleware(new IpThrottleMiddleware($redis, $logger)); 72 | $router->setStrategy($strategy); 73 | 74 | $this->defineRoutes($router); 75 | 76 | $request = ServerRequestFactory::fromGlobals(); 77 | $response = $router->dispatch($request); 78 | 79 | (new SapiEmitter)->emit($response); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Routing/Strategy/ApiStrategy.php: -------------------------------------------------------------------------------- 1 | isDev = $config->isDev(); 30 | 31 | $this->addResponseDecorator(static function (ResponseInterface $response): ResponseInterface { 32 | if (false === $response->hasHeader("access-control-allow-origin")) { 33 | $response = $response->withHeader("access-control-allow-origin", "*"); 34 | } 35 | return $response; 36 | }); 37 | } 38 | 39 | public function getThrowableHandler(): MiddlewareInterface { 40 | return new class ($this->responseFactory->createResponse(), $this->isDev) implements MiddlewareInterface 41 | { 42 | public function __construct( 43 | private readonly ResponseInterface $response, 44 | private readonly bool $isDev 45 | ) {} 46 | 47 | public function process( 48 | ServerRequestInterface $request, 49 | RequestHandlerInterface $handler 50 | ): ResponseInterface { 51 | try { 52 | return $handler->handle($request); 53 | } catch (Throwable $e) { 54 | if ($this->isDev) { 55 | throw $e; 56 | } 57 | 58 | $response = $this->response; 59 | 60 | if ($e instanceof Http\Exception) { 61 | return $e->buildJsonResponse($response); 62 | } 63 | 64 | if ($e instanceof ClientException && $e->getCode() === 429) { 65 | return (new Http\Exception\TooManyRequestsException()) 66 | ->buildJsonResponse($response); 67 | } 68 | 69 | \Sentry\captureException($e); 70 | $response->getBody()->write(json_encode([ 71 | "status_code" => 500, 72 | "reason_phrase" => "Internal Server Error" 73 | ], flags: JSON_THROW_ON_ERROR)); 74 | 75 | return $response 76 | ->withAddedHeader("content-type", "application/json") 77 | ->withStatus(500, "Internal Server Error"); 78 | } 79 | } 80 | }; 81 | } 82 | 83 | public function invokeRouteCallable(Route $route, ServerRequestInterface $request): ResponseInterface { 84 | $controller = $route->getCallable($this->getContainer()); 85 | $response = $controller($request, $route->getVars()); 86 | 87 | if ($this->isJsonSerializable($response)) { 88 | $body = json_encode($response, $this->jsonFlags | JSON_THROW_ON_ERROR); 89 | $response = $this->responseFactory->createResponse(); 90 | $response->getBody()->write($body); 91 | } 92 | 93 | return $this->decorateResponse($response); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/bootstrap.php: -------------------------------------------------------------------------------- 1 | map([ 24 | CoreConfig::class => "core", 25 | DbConfig::class => "db", 26 | RedisConfig::class => "redis", 27 | LoggingConfig::class => "logging", 28 | KeysConfig::class => "keys", 29 | EndpointsConfig::class => "endpoints", 30 | BrightDataConfig::class => "brightdata" 31 | ]); 32 | $config->loadJsonFile(PROJECT_ROOT."/".getenv("AS_SERVER_CONFIG")); 33 | 34 | Container::init($config); 35 | 36 | /** @var AugmentedSteam\Server\Config\CoreConfig $coreConfig */ 37 | $coreConfig = $config->getConfig(CoreConfig::class); 38 | 39 | if ($coreConfig->usePrettyErrors()) { 40 | $whoops = (new \Whoops\Run); 41 | $whoops->pushHandler( 42 | \Whoops\Util\Misc::isCommandLine() 43 | ? new \Whoops\Handler\PlainTextHandler() 44 | : new \Whoops\Handler\JsonResponseHandler() 45 | ); 46 | $whoops->register(); 47 | } 48 | 49 | if ($coreConfig->isSentryEnabled()) { 50 | Sentry\init([ 51 | "dsn" => $coreConfig->getSentryDsn(), 52 | "environment" => $coreConfig->getSentryEnvironment(), 53 | "error_types" => E_ALL, 54 | ]); 55 | } 56 | -------------------------------------------------------------------------------- /src/job.php: -------------------------------------------------------------------------------- 1 | getJob(...array_slice($argv, 1)) 11 | ->execute(); 12 | --------------------------------------------------------------------------------