├── .github └── workflows │ ├── release.yml │ ├── sync.yml │ └── validation.yml ├── .gitignore ├── LICENSE ├── README.md ├── current_version.json ├── requirements.txt └── src ├── __init__.py ├── consts.py ├── default_config.cfg ├── game_cache.py ├── http_client.py ├── js ├── GenerateFingerprint.js ├── HashGen.js └── fingerprint2.js ├── local.py ├── manifest.json ├── plugin.py └── version.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | repository_dispatch: 5 | pull_request: 6 | types: [closed] 7 | 8 | jobs: 9 | build: 10 | if: (github.event_name == 'repository_dispatch' && github.event.action == 'release') || 11 | (github.head_ref == 'autoupdate' && github.base_ref == 'master' && github.event.pull_request.merged == true) 12 | strategy: 13 | matrix: 14 | os: [macOS-latest, windows-2019] 15 | include: # match Python bitness shipped with Galaxy 16 | - os: macOS-latest 17 | arch: x64 18 | - os: windows-2019 19 | arch: x86 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | 23 | - uses: actions/checkout@v1 24 | 25 | - name: Set up Python 3.7 26 | uses: actions/setup-python@v1 27 | with: 28 | python-version: 3.7 29 | architecture: ${{ matrix.arch }} 30 | 31 | - name: Setup scripts 32 | run: | 33 | curl https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integrations-updater/master/scripts.py --output ../scripts.py 34 | python -m pip install wheel 35 | python -m pip install -U pip 36 | python -m pip install PyGithub==1.54 37 | python -m pip install pip-tools 38 | python -m pip install pytest 39 | 40 | - name: Build 41 | env: 42 | MAILER_PASSWORD: ${{ secrets.MAILER_PASSWORD }} 43 | run: python ../scripts.py build --dir ~/build/${{ matrix.os }} 44 | 45 | - uses: actions/upload-artifact@v2 46 | with: 47 | name: build_${{ matrix.os }} 48 | path: ~/build/${{ matrix.os }} 49 | 50 | deploy: 51 | if: (github.event_name == 'repository_dispatch' && github.event.action == 'release') || 52 | (github.head_ref == 'autoupdate' && github.base_ref == 'master' && github.event.pull_request.merged == true) 53 | runs-on: ubuntu-18.04 54 | needs: build 55 | steps: 56 | 57 | - uses: actions/checkout@v1 58 | with: 59 | ref: master 60 | 61 | - name: Set up Python 3.7 62 | uses: actions/setup-python@v1 63 | with: 64 | python-version: 3.7 65 | 66 | - name: Setup scripts 67 | run: | 68 | curl https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integrations-updater/master/scripts.py --output ../scripts.py 69 | python -m pip install PyGithub==1.54 70 | 71 | - name: download Windows build 72 | uses: actions/download-artifact@v2 73 | with: 74 | name: build_windows-2019 75 | path: build/windows 76 | 77 | - name: download macOS build 78 | uses: actions/download-artifact@v2 79 | with: 80 | name: build_macOS-latest 81 | path: build/macos 82 | 83 | - name: Release 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | REPOSITORY: ${{ github.repository }} 87 | MAILER_PASSWORD: ${{ secrets.MAILER_PASSWORD }} 88 | run: python ../scripts.py release --dir build/ 89 | 90 | - name: Update Release File 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | REPOSITORY: ${{ github.repository }} 94 | MAILER_PASSWORD: ${{ secrets.MAILER_PASSWORD }} 95 | run: python ../scripts.py update_release_file 96 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Synchronization 2 | 3 | on: 4 | repository_dispatch: 5 | schedule: 6 | - cron: '0 */12 * * *' 7 | 8 | jobs: 9 | sync: 10 | if: (github.event_name == 'repository_dispatch' && github.event.action == 'sync') || 11 | (github.event_name == 'schedule') 12 | runs-on: ubuntu-18.04 13 | steps: 14 | 15 | - uses: actions/checkout@v1 16 | 17 | - name: Set up Python 3.7 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.7 21 | 22 | - name: Download scripts 23 | run: | 24 | curl https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integrations-updater/master/scripts.py --output ../scripts.py 25 | python -m pip install PyGithub==1.54 26 | 27 | - name: Check and sync to PR 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 30 | MAILER_PASSWORD: ${{ secrets.MAILER_PASSWORD }} 31 | run: python ../scripts.py sync 32 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Pre-review validation 2 | 3 | on: 4 | repository_dispatch: 5 | pull_request: 6 | types: [opened, reopened, synchronize] 7 | branches: 8 | - autoupdate 9 | 10 | jobs: 11 | build: 12 | if: (github.event_name == 'pull_request') || (github.event_name == 'repository_dispatch' && github.event.action == 'validation') 13 | strategy: 14 | matrix: 15 | os: [macOS-latest, windows-2019] 16 | include: # match Python bitness shipped with Galaxy 17 | - os: macOS-latest 18 | arch: x64 19 | - os: windows-2019 20 | arch: x86 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | 24 | - uses: actions/checkout@v1 25 | with: 26 | ref: autoupdate 27 | 28 | - name: Set up Python 3.7 29 | uses: actions/setup-python@v1 30 | with: 31 | python-version: 3.7 32 | architecture: ${{ matrix.arch }} 33 | 34 | - name: Setup scripts and tests 35 | run: | 36 | curl https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integrations-updater/master/scripts.py --output ../scripts.py 37 | curl https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integrations-updater/master/tests.py --output ../tests.py 38 | python -m pip install -U pip 39 | python -m pip install wheel 40 | python -m pip install PyGithub==1.54 41 | python -m pip install pip-tools 42 | python -m pip install pytest 43 | 44 | - name: Build 45 | env: 46 | MAILER_PASSWORD: ${{ secrets.MAILER_PASSWORD }} 47 | run: python ../scripts.py build --dir ~/build/${{ matrix.os }} 48 | 49 | - uses: actions/upload-artifact@v2 50 | with: 51 | name: build_${{ matrix.os }} 52 | path: ~/build/${{ matrix.os }} 53 | 54 | - name: Tests 55 | env: 56 | TARGET: ~/build/${{ matrix.os }} 57 | run: pytest ../tests.py 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | /src/config.cfg 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tyler Nichols 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 | # galaxy-integration-rockstar 2 | 3 | 4 | 5 | This is a fork of the original work: https://github.com/tylerbrawl/Galaxy-Plugin-Rockstar. The current build was reviewed by FriendsOfGalaxy community and scanned for any vulnerabilities. 6 | 7 | For any issues related to the functionality, please report for the original author. We are syncing with his repository periodically to ensure new features and bugfixes are shipped frequently. 8 | -------------------------------------------------------------------------------- /current_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "tag_name": "0.5.13", 3 | "assets": [ 4 | { 5 | "browser_download_url": "https://github.com/FriendsOfGalaxy/galaxy-integration-rockstar/releases/download/0.5.13/macos.zip", 6 | "name": "macos.zip" 7 | }, 8 | { 9 | "browser_download_url": "https://github.com/FriendsOfGalaxy/galaxy-integration-rockstar/releases/download/0.5.13/windows.zip", 10 | "name": "windows.zip" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | galaxy.plugin.api==0.65 2 | python-dateutil==2.8.0 3 | file_read_backwards==2.0.0 4 | yarl==1.3.0 5 | galaxyutils==0.1.5 6 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfGalaxy/galaxy-integration-rockstar/9abe4f1671035264085d3de301866323c80d3b4d/src/__init__.py -------------------------------------------------------------------------------- /src/consts.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | 4 | from time import time 5 | 6 | from galaxyutils.config_parser import Option, get_config_options 7 | 8 | 9 | class NoLogFoundException(Exception): 10 | pass 11 | 12 | 13 | class NoGamesInLogException(Exception): 14 | pass 15 | 16 | 17 | ARE_ACHIEVEMENTS_IMPLEMENTED = True 18 | 19 | CONFIG_OPTIONS = get_config_options([ 20 | Option(option_name='user_presence_mode', default_value=0, allowed_values=[i for i in range(0, 4)]), 21 | Option(option_name='log_sensitive_data'), 22 | Option(option_name='debug_always_refresh'), 23 | Option(option_name='rockstar_launcher_path_override', str_option=True, default_value=None) 24 | ]) 25 | 26 | LOG_SENSITIVE_DATA = CONFIG_OPTIONS['log_sensitive_data'] 27 | 28 | MANIFEST_URL = r"https://gamedownloads-rockstargames-com.akamaized.net/public/title_metadata.json" 29 | 30 | IS_WINDOWS = (sys.platform == 'win32') 31 | 32 | ROCKSTAR_LAUNCHERPATCHER_EXE = "LauncherPatcher.exe" 33 | ROCKSTAR_LAUNCHER_EXE = "Launcher.exe" # It's a terribly generic name for a launcher. 34 | 35 | USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 " 36 | "Safari/537.36") 37 | 38 | WINDOWS_UNINSTALL_KEY = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" 39 | 40 | AUTH_PARAMS = { 41 | "window_title": "Login to Rockstar Games Social Club", 42 | "window_width": 700, 43 | "window_height": 600, 44 | "start_uri": "https://signin.rockstargames.com/connect/authorize/rsg?lang=en-US", 45 | "end_uri_regex": r"https://scapi.rockstargames.com/profile/getbasicprofile" 46 | } 47 | 48 | 49 | async def get_unix_epoch_time_from_date(date): 50 | year = int(date[0:4]) 51 | month = int(date[5:7]) 52 | day = int(date[8:10]) 53 | hour = int(date[11:13]) 54 | minute = int(date[14:16]) 55 | second = int(date[17:19]) 56 | return int(datetime.datetime(year, month, day, hour, minute, second).timestamp()) 57 | 58 | 59 | async def get_time_passed(old_time: int) -> str: 60 | current_time = int(time()) 61 | difference = current_time - old_time 62 | days_passed = int(difference / (3600 * 24)) 63 | if days_passed == 0: 64 | return "Today" 65 | elif days_passed >= 365: 66 | years_passed = int(days_passed / 365) 67 | return f"{years_passed} Years Ago" if years_passed != 1 else "1 Year Ago" 68 | elif days_passed >= 30: 69 | months_passed = int(days_passed / 30) 70 | return f"{months_passed} Months Ago" if months_passed != 1 else "1 Month Ago" 71 | elif days_passed >= 7: 72 | weeks_passed = int(days_passed / 7) 73 | return f"{weeks_passed} Weeks Ago" if weeks_passed != 1 else "1 Week Ago" 74 | return f"{days_passed} Days Ago" if days_passed != 1 else "1 Day Ago" 75 | -------------------------------------------------------------------------------- /src/default_config.cfg: -------------------------------------------------------------------------------- 1 | ## DO NOT EDIT OR DELETE THIS FILE! If you need to make changes, then copy this file, save it as "config.cfg" in the 2 | ## plugin root directory, and edit that file instead! 3 | 4 | # This is the configuration file for the Rockstar plugin. It contains settings which advanced users may change to alter 5 | # how the plugin functions. An explanation describing what each setting does is provided beneath each setting. 6 | 7 | user_presence_mode=0 8 | # Default Value: 0 9 | # Allowed Values: 10 | # - 0 (Disable User Presence) 11 | # - 1 (Display Last Played Game) 12 | # - 2 (Display Grand Theft Auto Online Character Stats) 13 | # - 3 (Display Red Dead Online Character Stats) 14 | # 15 | # NOTE: If this setting is set to any value other than 0, then all friends who have played at least one game will 16 | # constantly appear online. This is because Galaxy 2.0 currently displays only online friends in the sidebar. However, 17 | # seeing that the friend is online in this sidebar does NOT mean that the friend is actually online on the Social Club! 18 | # 19 | # Since the online status of a Social Club user is found via an endpoint with its own API, bearer token, and more, it is 20 | # not feasible to get the presence of a user's friends. (For those wondering, the endpoint is 21 | # https://prod.ros.rockstargames.com/scui/v2/api/friend/getfriends.) 22 | # Due to this limitation, a more creative approach has been taken as a compromise for importing friend presence status. 23 | # The allowed values along with their respective meanings are displayed above. 24 | 25 | rockstar_launcher_path_override=None 26 | # Default Value: None 27 | # Allowed Values: 28 | # - None 29 | # - [A string containing the path of the Rockstar Games Launcher, i.e., 30 | # "C:\Program Files\Rockstar Games\Launcher\Launcher.exe."] 31 | # If the Rockstar Games Launcher is not installed via the traditional installer, then the install location registry key 32 | # will not be correctly set. If this is the case, then you can set a path to the Launcher.exe file with this option. 33 | # Make sure to use quotation marks to denote the beginning and end of the path string. 34 | 35 | log_sensitive_data=False 36 | # Default Value: False 37 | # Allowed Values: 38 | # - True 39 | # - False 40 | # If set to true, this setting will add extra information to the log file generated by the Rockstar plugin. This 41 | # information is highly sensitive in nature, and may compromise the security of your Rockstar account. As such, this 42 | # setting is best left to false, unless you are debugging the plugin. 43 | 44 | debug_always_refresh=False 45 | # Default Value: False 46 | # Allowed Values: 47 | # - True 48 | # - False 49 | # If set to true, the plugin will always attempt to refresh the user's credentials upon launching Galaxy 2.0. This is 50 | # useful for debugging credential refreshing, but serves no purpose for the average user. -------------------------------------------------------------------------------- /src/game_cache.py: -------------------------------------------------------------------------------- 1 | from galaxy.api.types import LicenseInfo 2 | from galaxy.api.consts import LicenseType 3 | 4 | # The onlineTitleId values are taken from https://www.rockstargames.com/games/get-games.json?sort=&direction=&family=& 5 | # platform=pc. 6 | # 7 | # All other data values can be found from https://gamedownloads-rockstargames-com.akamaized.net/public/ 8 | # title_metadata.json. 9 | games_cache = { 10 | "launcher": { 11 | "friendlyName": "Rockstar Games Launcher", 12 | "guid": "Rockstar Games Launcher", 13 | "rosTitleId": 21, 14 | "onlineTitleId": None, 15 | "googleTagId": "Launcher_PC", 16 | "launchEXE": "Launcher.exe", 17 | "achievementId": None, 18 | "licenseInfo": LicenseInfo(LicenseType.Unknown), 19 | "isPreOrder": False 20 | }, 21 | "gtasa": { 22 | "friendlyName": "Grand Theft Auto: San Andreas", 23 | "guid": "Grand Theft Auto: San Andreas", 24 | "rosTitleId": 18, 25 | "onlineTitleId": 31, 26 | "googleTagId": "GTASA_PC", 27 | "launchEXE": "gta_sa.exe", 28 | "achievementId": None, 29 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 30 | "isPreOrder": False 31 | }, 32 | "gta5": { 33 | "friendlyName": "Grand Theft Auto V", 34 | "guid": "{5EFC6C07-6B87-43FC-9524-F9E967241741}", 35 | "rosTitleId": 11, 36 | "onlineTitleId": 241, 37 | "googleTagId": "GTAV_PC", 38 | "launchEXE": "PlayGTAV.exe", 39 | "trackEXE": "GTA5.exe", # This value is for games that require the launch of multiple executables. So far, 40 | # Grand Theft Auto V seems to be the only game that requires this. 41 | "achievementId": "gtav", 42 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 43 | "isPreOrder": False 44 | }, 45 | "lanoire": { 46 | "friendlyName": "L.A. Noire: Complete Edition", 47 | "guid": "{915726DF-7891-444A-AA03-0DF1D64F561A}", 48 | "rosTitleId": 9, 49 | "onlineTitleId": 35, 50 | "googleTagId": "LAN_PC", 51 | "launchEXE": "LANoire.exe", 52 | "achievementId": "lan", 53 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 54 | "isPreOrder": False 55 | }, 56 | "mp3": { 57 | "friendlyName": "Max Payne 3", 58 | "guid": "{1AA94747-3BF6-4237-9E1A-7B3067738FE1}", 59 | "rosTitleId": 10, 60 | "onlineTitleId": 40, 61 | "googleTagId": "MP3_PC", 62 | "launchEXE": "MaxPayne3.exe", 63 | "achievementId": "mp3", 64 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 65 | "isPreOrder": False 66 | }, 67 | # "lanoirevr": { 68 | # "friendlyName": "L.A. Noire: The VR Case Files", 69 | # "guid": "L.A. Noire: The VR Case Files", 70 | # "rosTitleId": 24, 71 | # "onlineTitleId": 35, # For some reason, this is the same as L.A. Noire's ID. 72 | # "googleTagId": "LANVR_PC", 73 | # "launchEXE": "LANoireVR.exe", 74 | # "achievementId": "lanvr", 75 | # "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 76 | # "isPreOrder": False 77 | # }, 78 | "gta3": { 79 | "friendlyName": "Grand Theft Auto III", 80 | "guid": "Grand Theft Auto III", 81 | "rosTitleId": 26, 82 | "onlineTitleId": 24, 83 | "googleTagId": "GTAIII_PC", 84 | "launchEXE": "gta3.exe", 85 | "achievementId": None, 86 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 87 | "isPreOrder": False 88 | }, 89 | "gtavc": { 90 | "friendlyName": "Grand Theft Auto: Vice City", 91 | "guid": "Grand Theft Auto: Vice City", 92 | "rosTitleId": 27, 93 | "onlineTitleId": 33, 94 | "googleTagId": "GTAVC_PC", 95 | "launchEXE": "gta-vc.exe", 96 | "achievementId": None, 97 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 98 | "isPreOrder": False 99 | }, 100 | "bully": { 101 | "friendlyName": "Bully: Scholarship Edition", 102 | "guid": "Bully: Scholarship Edition", 103 | "rosTitleId": 23, 104 | "onlineTitleId": 19, 105 | "googleTagId": "Bully_PC", 106 | "launchEXE": "Bully.exe", 107 | "achievementId": None, # The Social Club website lists Bully as having achievements, but it is only for the 108 | # mobile version of the game. 109 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 110 | "isPreOrder": False 111 | }, 112 | "rdr2": { 113 | "friendlyName": "Red Dead Redemption 2", 114 | "guid": "Red Dead Redemption 2", 115 | "rosTitleId": 13, 116 | "onlineTitleId": 912, 117 | "googleTagId": "RDR2_PC", 118 | "launchEXE": "RDR2.exe", 119 | "achievementId": "rdr2", # The achievements link for Red Dead Redemption 2 is currently unavailable, as the 120 | # game has not been released yet. 121 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 122 | "isPreOrder": False 123 | }, 124 | "gta4": { 125 | "friendlyName": "Grand Theft Auto IV", 126 | "guid": "Grand Theft Auto IV", 127 | "rosTitleId": 1, 128 | "onlineTitleId": 25, 129 | "googleTagId": "GTAIV_PC", 130 | "launchEXE": "GTAIV.exe", 131 | "achievementId": "gtaiv", 132 | "licenseInfo": LicenseInfo(LicenseType.SinglePurchase), 133 | "isPreOrder": False 134 | } 135 | } 136 | 137 | ignore_game_title_ids_list = [ 138 | "rdr2_sp_steam", # Red Dead Redemption 2 Single Player - Steam 139 | "rdr2_sp_rgl", # Red Dead Redemption 2 Single Player - Rockstar Games Launcher 140 | "rdr2_sp", # Red Dead Redemption 2 Single Player - General 141 | "rdr2_rdo", # Red Dead Online Standalone 142 | "rdr2_sp_epic" # Red Dead Redemption 2 Single Player - Epic Games Store 143 | ] 144 | 145 | 146 | def get_game_title_id_from_ros_title_id(ros_title_id): 147 | # The rosTitleId value is used by the Rockstar Games Launcher to uniquely identify the games that it supports. 148 | # For some reason, Rockstar made these values different from the internal numerical IDs for the same games on their 149 | # website (which are listed here as the onlineTitleId value). 150 | for game, d in games_cache.items(): 151 | if d["rosTitleId"] == int(ros_title_id): 152 | return game 153 | return None 154 | 155 | 156 | def get_game_title_id_from_online_title_id(online_title_id): 157 | # The onlineTitleId value is used to uniquely identify each game across Rockstar's various websites, including 158 | # https://www.rockstargames.com/auth/get-user.json. These values seem to have no use within the Rockstar Games 159 | # Launcher. 160 | for game, d in games_cache.items(): 161 | if d["onlineTitleId"] == int(online_title_id): 162 | return game 163 | return None 164 | 165 | 166 | def get_game_title_id_from_google_tag_id(google_tag_id): 167 | # The Google Tag Manager setup data contains a list of the Social Club user's played games as a string. The values 168 | # present in the string differ from other forms of identifiers on Rockstar's websites in that it describes the 169 | # game's title, and is not just a numeric ID. 170 | for game, d in games_cache.items(): 171 | if 'googleTagId' in d and d['googleTagId'] == google_tag_id: 172 | return game 173 | return None 174 | 175 | 176 | def get_game_title_id_from_ugc_title_id(ugc_id): 177 | # The ugc ID for a game seems to be related to the Google Tag ID of the game, although this could be wrong. 178 | for game, d in games_cache.items(): 179 | if 'googleTagId' in d and d['googleTagId'].lower() == ugc_id.lower(): 180 | return game 181 | return None 182 | 183 | 184 | def get_achievement_id_from_ros_title_id(ros_title_id): 185 | # The achievementId value is used by the Social Club API to uniquely identify games. Here, it is used to get the 186 | # list of a game's achievements, as well as a user's unlocked achievements. 187 | for game, d in games_cache.items(): 188 | if d["rosTitleId"] == int(ros_title_id): 189 | return games_cache[game]["achievementId"] 190 | -------------------------------------------------------------------------------- /src/http_client.py: -------------------------------------------------------------------------------- 1 | from galaxy.http import create_client_session 2 | from galaxy.api.errors import AuthenticationRequired, BackendError, InvalidCredentials, NetworkError 3 | from galaxy.api.types import UserPresence 4 | from galaxy.api.consts import PresenceState 5 | from http.cookies import SimpleCookie 6 | 7 | from consts import USER_AGENT, LOG_SENSITIVE_DATA, CONFIG_OPTIONS, get_time_passed, get_unix_epoch_time_from_date 8 | from game_cache import get_game_title_id_from_google_tag_id, get_game_title_id_from_ugc_title_id, games_cache 9 | 10 | import aiohttp 11 | import asyncio 12 | import dataclasses 13 | import dateutil.tz 14 | import datetime 15 | import json 16 | import logging as log 17 | import pickle 18 | import re 19 | import urllib.parse 20 | 21 | from html.parser import HTMLParser 22 | from time import time 23 | 24 | from yarl import URL 25 | 26 | 27 | @dataclasses.dataclass 28 | class Token(object): 29 | _token = None 30 | _expires = None 31 | 32 | def set_token(self, token, expiration): 33 | self._token, self._expires = token, expiration 34 | 35 | def get_token(self): 36 | return self._token 37 | 38 | def get_expiration(self): 39 | return self._expires 40 | 41 | @property 42 | def expired(self): 43 | return self._expires <= time() 44 | 45 | 46 | class CookieJar(aiohttp.CookieJar): 47 | def __init__(self): 48 | super().__init__() 49 | self._cookies_updated_callback = None 50 | 51 | def set_cookies_updated_callback(self, callback): 52 | self._cookies_updated_callback = callback 53 | 54 | def update_cookies(self, cookies, url=URL()): 55 | super().update_cookies(cookies, url) 56 | if cookies and self._cookies_updated_callback: 57 | self._cookies_updated_callback(list(self)) 58 | 59 | # aiohttp.CookieJar provides no method for deleting a specific cookie, so we need to create our own methods for 60 | # this. We also need to create our own method for getting a specific cookie. 61 | 62 | def remove_cookie(self, remove_name, domain="signin.rockstargames.com"): 63 | for key, morsel in self._cookies[domain].items(): 64 | if remove_name == morsel.key: 65 | del self._cookies[domain][key] 66 | return 67 | log.debug("ROCKSTAR_REMOVE_COOKIE_ERROR: The cookie " + remove_name + " from domain " + domain + 68 | " does not exist!") 69 | 70 | def remove_cookie_regex(self, remove_regex, domain="signin.rockstargames.com"): 71 | for key, morsel in self._cookies[domain].items(): 72 | if re.search(remove_regex, morsel.key): 73 | del self._cookies[domain][key] 74 | return 75 | log.debug("ROCKSTAR_REMOVE_COOKIE_REGEX_ERROR: There is no cookie from domain " + domain + " that matches the " 76 | "regular expression " + remove_regex + "!") 77 | 78 | def get(self, cookie_name, domain="signin.rockstargames.com"): 79 | for key, morsel in self._cookies[domain].items(): 80 | if cookie_name == morsel.key: 81 | return self._cookies[domain][key].value 82 | log.debug("ROCKSTAR_GET_COOKIE_ERROR: The cookie " + cookie_name + " from domain " + domain + 83 | " does not exist!") 84 | return '' 85 | 86 | 87 | class BackendClient: 88 | def __init__(self, store_credentials): 89 | self._debug_always_refresh = CONFIG_OPTIONS['debug_always_refresh'] 90 | self._store_credentials = store_credentials 91 | self.bearer = None 92 | # The refresh token here is the RMT cookie. The other refresh token is the rsso cookie. The RMT cookie is blank 93 | # for users not using two-factor authentication. 94 | self.refresh_token = Token() 95 | self._fingerprint = None 96 | self.user = None 97 | local_time_zone = dateutil.tz.tzlocal() 98 | self._utc_offset = local_time_zone.utcoffset(datetime.datetime.now(local_time_zone)).total_seconds() / 60 99 | self._current_session = None 100 | self._auth_lost_callback = None 101 | self._current_auth_token = None 102 | self._current_sc_token = None 103 | self._first_auth = True 104 | self._refreshing = False 105 | # super().__init__(cookie_jar=self._cookie_jar) 106 | 107 | async def close(self): 108 | await self._current_session.close() 109 | 110 | def get_credentials(self): 111 | creds = self.user 112 | morsel_list = [] 113 | for morsel in self._current_session.cookie_jar.__iter__(): 114 | morsel_list.append(morsel) 115 | creds['cookie_jar'] = pickle.dumps(morsel_list).hex() 116 | creds['current_auth_token'] = self._current_auth_token 117 | creds['current_sc_token'] = self._current_sc_token 118 | creds['refresh_token'] = pickle.dumps(self.refresh_token).hex() 119 | creds['fingerprint'] = self._fingerprint 120 | return creds 121 | 122 | def set_cookies_updated_callback(self, callback): 123 | self._current_session.cookie_jar.set_cookies_updated_callback(callback) 124 | 125 | def update_cookie(self, cookie): 126 | # I believe that the cookie beginning with rsso gets a different name occasionally, so we need to delete the old 127 | # rsso cookie using regular expressions if we want to ensure that the refresh token can continue to be obtained. 128 | 129 | if re.search("^rsso", cookie['name']): 130 | self._current_session.cookie_jar.remove_cookie_regex("^rsso") 131 | 132 | if cookie['name'] != '': 133 | cookie_object = SimpleCookie() 134 | cookie_object[cookie['name']] = cookie['value'] 135 | cookie_object[cookie['name']]['domain'] = cookie['domain'] 136 | cookie_object[cookie['name']]['path'] = cookie['path'] 137 | self._current_session.cookie_jar.update_cookies(cookie_object) 138 | 139 | def set_auth_lost_callback(self, callback): 140 | self._auth_lost_callback = callback 141 | 142 | def is_authenticated(self): 143 | return self.user is not None and self._auth_lost_callback is None 144 | 145 | def set_current_auth_token(self, token): 146 | self._current_auth_token = token 147 | self._current_session.cookie_jar.update_cookies({'ScAuthTokenData2020': token}) 148 | 149 | def set_current_sc_token(self, token): 150 | self._current_sc_token = token 151 | self._current_session.cookie_jar.update_cookies({'BearerToken': token}) 152 | 153 | def get_current_auth_token(self): 154 | return self._current_auth_token 155 | 156 | def get_current_sc_token(self): 157 | return self._current_sc_token 158 | 159 | def get_named_cookie(self, cookie_name): 160 | return self._current_session.cookies[cookie_name] 161 | 162 | def get_rockstar_id(self): 163 | return self.user["rockstar_id"] 164 | 165 | def set_refresh_token(self, token): 166 | expiration_time = time() + (3600 * 24 * 365 * 20) 167 | self.refresh_token.set_token(token, expiration_time) 168 | self._current_session.cookie_jar.update_cookies({"RMT": token}) 169 | 170 | def set_refresh_token_absolute(self, token): 171 | self.refresh_token = token 172 | 173 | def get_refresh_token(self): 174 | if self.refresh_token.expired: 175 | log.debug("ROCKSTAR_REFRESH_EXPIRED: The refresh token has expired!") 176 | self.refresh_token.set_token(None, None) 177 | return self.refresh_token.get_token() 178 | 179 | def set_fingerprint(self, fingerprint): 180 | self._fingerprint = fingerprint 181 | 182 | def is_fingerprint_defined(self): 183 | return self._fingerprint is not None 184 | 185 | def create_session(self, stored_credentials): 186 | self._current_session = create_client_session(cookie_jar=CookieJar()) 187 | self._current_session.max_redirects = 300 188 | if stored_credentials is not None: 189 | morsel_list = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar'])) 190 | for morsel in morsel_list: 191 | cookie_object = SimpleCookie() 192 | cookie_object[morsel.key] = morsel.value 193 | cookie_object[morsel.key]['domain'] = morsel['domain'] 194 | cookie_object[morsel.key]['path'] = morsel['path'] 195 | self._current_session.cookie_jar.update_cookies(cookie_object) 196 | 197 | async def get_json_from_request_strict(self, url, include_default_headers=True, additional_headers=None): 198 | headers = additional_headers if additional_headers is not None else {} 199 | if include_default_headers: 200 | headers["Authorization"] = f"Bearer {self._current_sc_token}" 201 | headers["X-Requested-With"] = "XMLHttpRequest" 202 | headers["User-Agent"] = USER_AGENT 203 | try: 204 | resp = await self._current_session.get(url, headers=headers) 205 | await self._update_cookies_from_response(resp) 206 | return await resp.json() 207 | except Exception as e: 208 | log.exception(f"WARNING: The request failed with exception {repr(e)}. Attempting to refresh credentials...") 209 | await self._refresh_credentials_social_club_light() 210 | return await self.get_json_from_request_strict(url, include_default_headers, additional_headers) 211 | 212 | async def get_bearer_from_cookie_jar(self): 213 | morsel_list = self._current_session.cookie_jar.__iter__() 214 | cookies = {} 215 | for morsel in morsel_list: 216 | cookies[morsel.key] = morsel.value 217 | log.debug(cookies) 218 | return cookies['BearerToken'] 219 | 220 | async def get_cookies_for_headers(self): 221 | cookie_string = "" 222 | for morsel in self._current_session.cookie_jar.__iter__(): 223 | cookie_string += "" + str(morsel.key) + "=" + str(morsel.value) + ";" 224 | # log.debug("ROCKSTAR_CURR_COOKIE: " + cookie_string) 225 | return cookie_string[:len(cookie_string) - 1] 226 | 227 | async def _update_cookies_from_response(self, resp: aiohttp.ClientResponse, exclude=None): 228 | if exclude is None: 229 | exclude = [] 230 | filtered_cookies = resp.cookies 231 | for key, morsel in filtered_cookies.items(): 232 | if key not in exclude: 233 | if LOG_SENSITIVE_DATA: 234 | log.debug(f"ROCKSTAR_COOKIE_UPDATED: Found Cookie {key}: {str(morsel)}") 235 | self._current_session.cookie_jar.update_cookies({key: morsel}) 236 | 237 | async def _get_user_json(self, message=None): 238 | try: 239 | old_auth = self._current_auth_token 240 | if LOG_SENSITIVE_DATA: 241 | log.debug(f"ROCKSTAR_OLD_AUTH: {old_auth}") 242 | else: 243 | log.debug(f"ROCKSTAR_OLD_AUTH: ***") 244 | headers = { 245 | "accept": "*/*", 246 | "cookie": await self.get_cookies_for_headers(), 247 | "referer": "https://www.rockstargames.com", 248 | "user-agent": USER_AGENT 249 | } 250 | resp = await self._current_session.get("https://graph.rockstargames.com/?operationName=UserData&variables=" 251 | "%7B%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C" 252 | "%22sha256Hash%22%3A%224015efac722ba3668f30067cc729d9ecf9d7761f22ba5" 253 | "a58c8e1530a309ab029%22%7D%7D", headers=headers, 254 | allow_redirects=False) 255 | await self._update_cookies_from_response(resp) 256 | # aiohttp allows you to get a specified cookie from the previous response. 257 | filtered_cookies = resp.cookies 258 | if "TS019978c2" in filtered_cookies: 259 | ts_val = filtered_cookies['TS019978c2'].value 260 | if LOG_SENSITIVE_DATA: 261 | log.debug(f"ROCKSTAR_NEW_TS_COOKIE: {ts_val}") 262 | else: 263 | log.debug("ROCKSTAR_NEW_TS_COOKIE: ***") 264 | 265 | auth_cookie = None 266 | for cookie in filtered_cookies: 267 | if cookie.find("TSc") != -1: 268 | auth_cookie = cookie 269 | log.debug(f"ROCKSTAR_AUTH_FIND_NAME: {auth_cookie}") 270 | break 271 | else: 272 | log.debug(f"ROCKSTAR_AUTH_FIND_ERROR: The authentication cookie could not be found!") 273 | raise AuthenticationRequired 274 | 275 | new_auth = filtered_cookies[auth_cookie].value 276 | if LOG_SENSITIVE_DATA: 277 | log.debug(f"ROCKSTAR_NEW_AUTH: {new_auth}") 278 | else: 279 | log.debug(f"ROCKSTAR_NEW_AUTH: ***") 280 | self._current_auth_token = new_auth 281 | if LOG_SENSITIVE_DATA: 282 | log.warning("ROCKSTAR_AUTH_CHANGE: The authentication cookie's value has changed!") 283 | if self.user is not None: 284 | self._store_credentials(self.get_credentials()) 285 | else: 286 | # For security purposes, the authentication cookie value (whether hidden or not) is logged, regardless 287 | # of whether or not it has changed. If the logged outputs are similar between the two, it is harder to 288 | # tell if the value has really changed or not. 289 | if LOG_SENSITIVE_DATA: 290 | log.debug(f"ROCKSTAR_NEW_AUTH: {old_auth}") 291 | else: 292 | log.debug(f"ROCKSTAR_NEW_AUTH: ***") 293 | return await resp.json() 294 | except Exception as e: 295 | if message is not None: 296 | log.warning(message) 297 | else: 298 | log.warning("ROCKSTAR_USER_JSON_WARNING: The request to get the user from the graph resulted in this" 299 | " exception: " + repr(e) + ". Attempting to refresh credentials...") 300 | try: 301 | await self.refresh_credentials() 302 | return await self._get_user_json(message) 303 | except Exception: 304 | log.exception("ROCKSTAR_USER_JSON_ERROR: The request to get the user from the graph failed even after " 305 | "attempting to refresh credentials. Revoking user authentication...") 306 | raise AuthenticationRequired 307 | 308 | async def _get_bearer(self): 309 | try: 310 | resp_json = await self._get_user_json() 311 | if LOG_SENSITIVE_DATA: 312 | log.debug("ROCKSTAR_USER_GRAPH_JSON: " + str(resp_json)) 313 | 314 | cookie_json = json.loads(urllib.parse.unquote(self._current_auth_token)) 315 | if LOG_SENSITIVE_DATA: 316 | log.debug("ROCKSTAR_AUTH_COOKIE: " + str(cookie_json)) 317 | new_bearer = cookie_json["access_token"] 318 | self.bearer = new_bearer 319 | self.refresh = cookie_json["refresh_token"] 320 | return new_bearer 321 | except Exception as e: 322 | log.error("ERROR: The request to refresh credentials resulted in this exception: " + repr(e)) 323 | raise 324 | 325 | async def _get_request_verification_token(self, url, referer): 326 | class RockstarHTMLParser(HTMLParser): 327 | rv_token = None 328 | 329 | def handle_starttag(self, tag, attrs): 330 | if tag == "input" and ('name', "__RequestVerificationToken") in attrs: 331 | for attr, value in attrs: 332 | if attr == "value": 333 | self.rv_token = value 334 | break 335 | 336 | def get_token(self): 337 | return self.rv_token 338 | 339 | while self._refreshing: 340 | await asyncio.sleep(1) 341 | headers = { 342 | "Accept": ("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8," 343 | "application/signed-exchange;v=b3"), 344 | "Cookie": await self.get_cookies_for_headers(), 345 | "Referer": referer, 346 | "User-Agent": USER_AGENT 347 | } 348 | resp = await self._current_session.get(url, headers=headers) 349 | await self._update_cookies_from_response(resp) 350 | resp_text = await resp.text() 351 | parser = RockstarHTMLParser() 352 | parser.feed(resp_text) 353 | rv_token = parser.get_token() 354 | parser.close() 355 | return rv_token 356 | 357 | async def _get_google_tag_data(self): 358 | # To gain access to this information, we need to scrape a hidden input value called __RequestVerificationToken 359 | # located on the html file at https://socialclub.rockstargames.com/. 360 | rv_token = await self._get_request_verification_token("https://socialclub.rockstargames.com/", 361 | "https://socialclub.rockstargames.com/") 362 | 363 | if LOG_SENSITIVE_DATA: 364 | log.debug(f"ROCKSTAR_SC_REQUEST_VERIFICATION_TOKEN: {rv_token}") 365 | 366 | headers = { 367 | "Cookie": await self.get_cookies_for_headers(), 368 | "__RequestVerificationToken": rv_token, 369 | "User-Agent": USER_AGENT, 370 | "X-Requested-With": "XMLHttpRequest" 371 | } 372 | url = f"https://socialclub.rockstargames.com/ajax/getGoogleTagManagerSetupData?_={int(time() * 1000)}" 373 | resp = await self._current_session.get(url, headers=headers) 374 | await self._update_cookies_from_response(resp) 375 | return await resp.json() 376 | 377 | async def get_played_games(self, callback=False): 378 | try: 379 | resp_json = await self._get_google_tag_data() 380 | if LOG_SENSITIVE_DATA: 381 | log.debug(f"ROCKSTAR_SC_TAG_DATA: {resp_json}") 382 | else: 383 | log.debug(f"ROCKSTAR_SC_TAG_DATA: ***") 384 | if resp_json['loginState'] == "false": 385 | raise AuthenticationRequired 386 | games_owned_string = resp_json['gamesOwned'] 387 | owned_games = [] 388 | for game in games_owned_string.split('|'): 389 | if game != "Launcher_PC": 390 | title_id = get_game_title_id_from_google_tag_id(game) 391 | if title_id: 392 | owned_games.append(title_id) 393 | return owned_games 394 | except Exception: 395 | if not callback: 396 | try: 397 | await self._refresh_credentials_social_club_light() 398 | return await self.get_played_games(callback=True) 399 | except Exception as e: 400 | log.exception("ROCKSTAR_PLAYED_GAMES_ERROR: The request to scrape the user's played games resulted " 401 | "in this exception: " + repr(e)) 402 | raise 403 | raise 404 | 405 | async def get_last_played_game(self, friend_name): 406 | headers = { 407 | "Authorization": f"Bearer {self._current_sc_token}", 408 | "User-Agent": USER_AGENT, 409 | "X-Requested-With": "XMLHttpRequest" 410 | } 411 | try: 412 | resp = await self._current_session.get("https://scapi.rockstargames.com/profile/getprofile?nickname=" 413 | f"{friend_name}&maxFriends=3", headers=headers) 414 | await self._update_cookies_from_response(resp) 415 | resp_json = await resp.json() 416 | except AssertionError: 417 | await self._refresh_credentials_social_club_light() 418 | return await self.get_last_played_game(friend_name) 419 | try: 420 | # The last played game is always listed first in the ownedGames list. 421 | last_played_ugc = resp_json['accounts'][0]['rockstarAccount']['gamesOwned'][0]['name'] 422 | title_id = get_game_title_id_from_ugc_title_id(last_played_ugc + "_PC") 423 | last_played_time = await get_unix_epoch_time_from_date(resp_json['accounts'][0] 424 | ['rockstarAccount']['gamesOwned'][0] 425 | ['lastSeen']) 426 | if LOG_SENSITIVE_DATA: 427 | log.debug(f"{friend_name}'s Last Played Game: " 428 | f"{games_cache[title_id]['friendlyName'] if title_id else last_played_ugc}") 429 | return UserPresence(PresenceState.Online, 430 | game_id=str(games_cache[title_id]['rosTitleId']) if title_id else last_played_ugc, 431 | in_game_status=f"Last Played {await get_time_passed(last_played_time)}") 432 | except IndexError: 433 | # If a game is not found in the gamesOwned list, then the user has not played any games. In this case, we 434 | # cannot be certain of their presence status. 435 | if LOG_SENSITIVE_DATA: 436 | log.warning(f"ROCKSTAR_LAST_PLAYED_WARNING: The user {friend_name} has not played any games!") 437 | return UserPresence(PresenceState.Unknown) 438 | 439 | async def get_gta_online_stats(self, user_id, friend_name): 440 | class GTAOnlineStatParser(HTMLParser): 441 | char_rank = None 442 | char_title = None 443 | rank_internal_pos = None 444 | 445 | def handle_starttag(self, tag, attrs): 446 | if not self.rank_internal_pos and tag == "div" and len(attrs) > 0: 447 | class_, name = attrs[0] 448 | if not re.search(r"^rankHex right-grad .*", name): 449 | return 450 | self.rank_internal_pos = self.getpos()[0] 451 | 452 | def handle_data(self, data): 453 | if not self.rank_internal_pos: 454 | return 455 | if not self.char_rank and self.getpos()[0] == (self.rank_internal_pos + 1): 456 | self.char_rank = data 457 | elif not self.char_title and self.getpos()[0] == (self.rank_internal_pos + 2): 458 | # There is a bug in the Social Club API where a user who is past rank 105 no longer has their title 459 | # shown. However, they are still a "Kingpin." 460 | if int(self.char_rank) >= 105: 461 | self.char_title = "Kingpin" 462 | else: 463 | self.char_title = data 464 | 465 | def get_stats(self): 466 | return self.char_rank, self.char_title 467 | 468 | url = ("https://socialclub.rockstargames.com/games/gtav/career/overviewAjax?character=Freemode&" 469 | f"rockstarIds={user_id}&slot=Freemode&nickname={friend_name}&gamerHandle=&gamerTag=&category=Overview" 470 | f"&_={int(time() * 1000)}") 471 | headers = { 472 | 'Accept': 'text/html, */*', 473 | 'Cookie': await self.get_cookies_for_headers(), 474 | "RequestVerificationToken": await self._get_request_verification_token( 475 | "https://socialclub.rockstargames.com/games/gtav/pc/career/overview/gtaonline", 476 | "https://socialclub.rockstargames.com/games"), 477 | 'User-Agent': USER_AGENT 478 | } 479 | while True: 480 | try: 481 | resp = await self._current_session.get(url, headers=headers) 482 | await self._update_cookies_from_response(resp) 483 | break 484 | except aiohttp.ClientResponseError as e: 485 | if e.status == 429: 486 | await asyncio.sleep(5) 487 | else: 488 | raise e 489 | except Exception: 490 | raise 491 | resp_text = await resp.text() 492 | parser = GTAOnlineStatParser() 493 | parser.feed(resp_text) 494 | rank, title = parser.get_stats() 495 | parser.close() 496 | if rank and title: 497 | log.debug(f"ROCKSTAR_GTA_ONLINE_STATS: [{friend_name}] Grand Theft Auto Online: Rank {rank} {title}") 498 | return UserPresence(PresenceState.Online, 499 | game_id="11", 500 | in_game_status=f"Grand Theft Auto Online: Rank {rank} {title}") 501 | else: 502 | if LOG_SENSITIVE_DATA: 503 | log.debug(f"ROCKSTAR_GTA_ONLINE_STATS_MISSING: {friend_name} (Rockstar ID: {user_id}) does not have " 504 | f"any character stats for Grand Theft Auto Online. Returning default user presence...") 505 | return await self.get_last_played_game(friend_name) 506 | 507 | async def get_rdo_stats(self, user_id, friend_name): 508 | headers = { 509 | 'Authorization': f'Bearer {self._current_sc_token}', 510 | 'User-Agent': USER_AGENT, 511 | 'X-Requested-With': 'XMLHttpRequest' 512 | } 513 | try: 514 | resp = await self._current_session.get("https://scapi.rockstargames.com/games/rdo/navigationData?platform=pc&" 515 | f"rockstarId={user_id}", headers=headers) 516 | await self._update_cookies_from_response(resp) 517 | resp_json = await resp.json() 518 | except AssertionError: 519 | await self._refresh_credentials_social_club_light() 520 | return await self.get_rdo_stats(user_id, friend_name) 521 | try: 522 | char_name = resp_json['result']['onlineCharacterName'] 523 | char_rank = resp_json['result']['onlineCharacterRank'] 524 | except KeyError: 525 | if LOG_SENSITIVE_DATA: 526 | log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS_MISSING: {friend_name} (Rockstar ID: {user_id}) does not " 527 | f"have any character stats for Red Dead Online. Returning default user presence...") 528 | return await self.get_last_played_game(friend_name) 529 | if LOG_SENSITIVE_DATA: 530 | log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS_PARTIAL: {friend_name} (Rockstar ID: {user_id}) has a character " 531 | f"named {char_name}, who is at rank {str(char_rank)}.") 532 | 533 | # As an added bonus, we will find the user's preferred role (bounty hunter, collector, or trader). This is 534 | # determined by the acquired rank in each role. 535 | resp = await self._current_session.get("https://scapi.rockstargames.com/games/rdo/awards/progress?platform=pc&" 536 | f"rockstarId={user_id}", headers=headers) 537 | await self._update_cookies_from_response(resp) 538 | resp_json = await resp.json() 539 | ranks = { 540 | "Bounty Hunter": None, 541 | "Collector": None, 542 | "Trader": None 543 | } 544 | for goal in resp_json['challengeGoals']: 545 | if goal['id'] == "MPAC_Role_BountyHunter_001": 546 | ranks['Bounty Hunter'] = goal['goalValue'] 547 | elif goal['id'] == "MPAC_Role_Collector_001": 548 | ranks['Collector'] = goal['goalValue'] 549 | elif goal['id'] == "MPAC_Role_Trader_001": 550 | ranks['Trader'] = goal['goalValue'] 551 | for rank, val in ranks.items(): 552 | if not val: 553 | break 554 | else: 555 | break 556 | max_rank = 0 557 | highest_rank = "" 558 | for rank, val in ranks.items(): 559 | if val > max_rank: 560 | max_rank = val 561 | highest_rank = rank 562 | # If two roles have the same rank, then the character is considered to have a Hybrid role. 563 | elif val == max_rank and max_rank != 0: 564 | highest_rank = "Hybrid" 565 | break 566 | if LOG_SENSITIVE_DATA: 567 | log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS: [{friend_name}] Red Dead Online: {char_name} - Rank {char_rank}" 568 | f" {highest_rank}") 569 | return UserPresence(PresenceState.Online, 570 | game_id="13", 571 | in_game_status=f"Red Dead Online: {char_name} - Rank {char_rank} {highest_rank}") 572 | 573 | def _get_rsso_cookie(self) -> (str, str): 574 | for morsel in self._current_session.cookie_jar.__iter__(): 575 | if re.search("^rsso", morsel.key): 576 | rsso_name = morsel.key 577 | if LOG_SENSITIVE_DATA: 578 | log.debug(f"ROCKSTAR_RSSO_NAME: {rsso_name}") 579 | rsso_value = morsel.value 580 | if LOG_SENSITIVE_DATA: 581 | log.debug(f"ROCKSTAR_RSSO_VALUE: {rsso_value}") 582 | return rsso_name, rsso_value 583 | 584 | async def refresh_credentials(self): 585 | while self._refreshing: 586 | # If we are already refreshing the credentials, then no other refresh requests should be accepted. 587 | await asyncio.sleep(3) 588 | self._refreshing = True 589 | await self._refresh_credentials_base() 590 | await self._refresh_credentials_social_club() 591 | self._refreshing = False 592 | 593 | async def _refresh_credentials_base(self): 594 | # This request returns a new cookie beginning with "TSc", which is used as authentication for the base website 595 | # of https://www.rockstargames.com/. The cookie itself is updated in a GET request to Rockstar's newly 596 | # implemented graph. 597 | 598 | # It seems like the Rockstar website connects to https://signin.rockstargames.com/connect/cors/check/rsg via a 599 | # POST request in order to re-authenticate the user. This request uses a fingerprint as form data. 600 | 601 | # This POST request then returns a message with a code, which is then sent in an encoded URL to 602 | # https://www.rockstargames.com/graph.json (see below for the full URL). This is an endpoint to Rockstar's 603 | # GraphQL server, and the SHA-256 hash is derived from the request made to the server as a query. As of now, 604 | # this is the string that generates the correct hash: 605 | 606 | # query User($code: String) { 607 | # user(code: $code) { 608 | # ...userFields 609 | # __typename 610 | # } 611 | # } 612 | # 613 | # fragment userFields on RockstarGames_Users_Graph_Type_User { 614 | # id 615 | # nickname 616 | # avatar 617 | # profile_link 618 | # dob 619 | # __typename 620 | # } 621 | # 622 | 623 | # (NOTE: The intentional new-line character at the end must be included to generate the correct hash.) 624 | 625 | # Finally, this last request updates the cookies that are used for further authentication. 626 | try: 627 | url = "https://signin.rockstargames.com/connect/cors/check/rsg" 628 | headers = { 629 | "Accept": "*/*", 630 | "Cookie": await self.get_cookies_for_headers(), 631 | "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", 632 | "Host": "signin.rockstargames.com", 633 | "Origin": "https://www.rockstargames.com", 634 | "Referer": "https://www.rockstargames.com/", 635 | "User-Agent": USER_AGENT, 636 | "X-Requested-With": "XMLHttpRequest" 637 | } 638 | data = {"fingerprint": self._fingerprint} 639 | refresh_resp = await self._current_session.post(url, data=data, headers=headers) 640 | await self._update_cookies_from_response(refresh_resp) 641 | refresh_code = await refresh_resp.text() 642 | if LOG_SENSITIVE_DATA: 643 | log.debug("ROCKSTAR_REFRESH_CODE: Got code " + refresh_code + "!") 644 | # We need to set the new refresh token here, if it is updated. 645 | try: 646 | self.set_refresh_token(refresh_resp.cookies['RMT'].value) 647 | except KeyError: 648 | if LOG_SENSITIVE_DATA: 649 | log.debug("ROCKSTAR_RMT_MISSING: The RMT cookie is missing, presumably because the user has not " 650 | "enabled two-factor authentication. Proceeding anyways...") 651 | self.set_refresh_token('') 652 | old_auth = self._current_auth_token 653 | self._current_auth_token = None 654 | if LOG_SENSITIVE_DATA and old_auth: 655 | log.debug("ROCKSTAR_OLD_AUTH_REFRESH: " + old_auth) 656 | else: 657 | log.debug(f"ROCKSTAR_OLD_AUTH_REFRESH: ***") 658 | url = f"https://www.rockstargames.com/auth/gateway.json?code={refresh_code[1:-1]}" 659 | headers = { 660 | "Accept": "*/*", 661 | "Cookie": await self.get_cookies_for_headers(), 662 | "Content-type": "application/json", 663 | "Referer": "https://www.rockstargames.com/", 664 | "User-Agent": USER_AGENT 665 | } 666 | final_request = await self._current_session.get(url, headers=headers) 667 | await self._update_cookies_from_response(final_request) 668 | final_json = await final_request.json() 669 | if LOG_SENSITIVE_DATA: 670 | log.debug("ROCKSTAR_REFRESH_JSON: " + str(final_json)) 671 | 672 | new_auth = final_json["bearerToken"] 673 | self._current_auth_token = new_auth 674 | if LOG_SENSITIVE_DATA: 675 | log.debug("ROCKSTAR_NEW_AUTH_REFRESH: " + new_auth) 676 | else: 677 | log.debug(f"ROCKSTAR_NEW_AUTH_REFRESH: ***") 678 | if old_auth != new_auth: 679 | log.debug("ROCKSTAR_REFRESH_SUCCESS: The user has been successfully re-authenticated!") 680 | except Exception as e: 681 | log.exception("ROCKSTAR_REFRESH_FAILURE: The attempt to re-authenticate the user has failed with the " 682 | "exception " + repr(e) + ". Logging the user out...") 683 | self._refreshing = False 684 | raise InvalidCredentials 685 | 686 | async def _refresh_credentials_social_club_light(self): 687 | # If the user attempts to use the Social Club bearer token within ten hours of having received its latest 688 | # version, then they may simply make a POST request to 689 | # https://socialclub.rockstargames.com/connect/refreshaccess in order to get a new bearer token. 690 | self._refreshing = True 691 | old_auth = self._current_sc_token 692 | headers = { 693 | "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", 694 | "User-Agent": USER_AGENT, 695 | "X-Requested-With": "XMLHttpRequest" 696 | } 697 | data = f"accessToken={old_auth}" 698 | try: 699 | resp = await self._current_session.post("https://socialclub.rockstargames.com/connect/refreshaccess", 700 | data=data, headers=headers, allow_redirects=True) 701 | await self._update_cookies_from_response(resp) 702 | filtered_cookies = resp.cookies 703 | if "BearerToken" in filtered_cookies: 704 | self._current_sc_token = filtered_cookies["BearerToken"].value 705 | if LOG_SENSITIVE_DATA: 706 | log.debug(f"ROCKSTAR_SC_BEARER_NEW: {self._current_sc_token}") 707 | else: 708 | log.debug(f"ROCKSTAR_SC_BEARER_NEW: {self._current_sc_token[:5]}***{self._current_sc_token[-3:]}") 709 | if old_auth != self._current_sc_token: 710 | log.debug("ROCKSTAR_SC_LIGHT_REFRESH_SUCCESS: The Social Club user was successfully " 711 | "re-authenticated!") 712 | self._refreshing = False 713 | else: 714 | # If a request was made to get a new bearer token but a new token was not granted, then it is assumed 715 | # that the alternate longer method for refreshing the user's credentials is required. 716 | log.warning("ROCKSTAR_SC_LIGHT_REFRESH_FAILED: The light method for refreshing the Social Club " 717 | "user's authentication has failed. Falling back to the strict refresh method...") 718 | self._refreshing = False 719 | await self.refresh_credentials() 720 | except aiohttp.ClientResponseError as e: 721 | if e.status == 401: 722 | log.warning("ROCKSTAR_SC_LIGHT_REFRESH_FAILED: The light method for refreshing the Social Club user's " 723 | "authentication has failed. Falling back to the strict refresh method...") 724 | self._refreshing = False 725 | await self.refresh_credentials() 726 | 727 | async def _refresh_credentials_social_club(self): 728 | # There are instances where the bearer token provided by the get-user.json endpoint is insufficient (i.e., 729 | # sending a message to a user or getting the tags from the Google Tag Manager). This requires a separate access 730 | # (bearer) token from the https://socialclub.rockstargames.com/ website. 731 | 732 | # To refresh the Social Club bearer token (hereafter referred to as the BearerToken), first make a GET request 733 | # to https://signin.rockstargames.com/connect/check/socialclub?returnUrl=%2FBlocker%2FAuthCheck&lang=en-US. Make 734 | # sure to supply the current cookies as a header. Also, this request sets a new TS01a305c4 cookie, so its value 735 | # should be updated. 736 | 737 | # Next, make a POST request to https://signin.rockstargames.com/api/connect/check/socialclub. For request 738 | # headers, Content-Type must be application/json, Cookie must be the current cookies, and X-Requested-With must 739 | # be XMLHttpRequest. This response returns a JSON containing a single key: redirectUrl, which corresponds to the 740 | # unique URL for the user to refresh their bearer token. 741 | 742 | # Lastly, make a GET request to the specified redirectUrl and set the request header X-Requested-With to 743 | # XMLHttpRequest. This request sets the updated value for the BearerToken cookie, allowing further requests to 744 | # the Social Club API to be made. 745 | try: 746 | old_auth = self._current_sc_token 747 | if LOG_SENSITIVE_DATA: 748 | log.debug(f"ROCKSTAR_SC_BEARER_OLD: {old_auth}") 749 | else: 750 | log.debug(f"ROCKSTAR_SC_BEARER_OLD: {old_auth[:5]}***{old_auth[-3:]}") 751 | url = ("https://signin.rockstargames.com/connect/check/socialclub?returnUrl=%2FBlocker%2FAuthCheck&lang=en-" 752 | "US") 753 | headers = { 754 | "Cookie": await self.get_cookies_for_headers(), 755 | "User-Agent": USER_AGENT 756 | } 757 | resp = await self._current_session.get(url, headers=headers) 758 | await self._update_cookies_from_response(resp) 759 | 760 | url = "https://signin.rockstargames.com/api/connect/check/socialclub" 761 | rsso_name, rsso_value = self._get_rsso_cookie() 762 | headers = { 763 | "Content-Type": "application/json", 764 | # A 400 error is returned by lazily submitting all cookies, so we need to send only the cookies that 765 | # matter. 766 | "Cookie": f"RMT={self.get_refresh_token()};{rsso_name}={rsso_value}", 767 | "Referer": ("https://signin.rockstargames.com/connect/check/socialclub?returnUrl=%2FBlocker%2FAuthCheck" 768 | "&lang=en-US"), 769 | "User-Agent": USER_AGENT, 770 | "X-Requested-With": "XMLHttpRequest" 771 | } 772 | data = { 773 | "fingerprint": self._fingerprint, 774 | "returnUrl": "/Blocker/AuthCheck" 775 | } 776 | # Using a context manager here will prevent the extra cookies from being sent. 777 | async with create_client_session() as s: 778 | resp = await s.post(url, json=data, headers=headers) 779 | await self._update_cookies_from_response(resp) 780 | filtered_cookies = resp.cookies 781 | if "TS01a305c4" in filtered_cookies: 782 | if LOG_SENSITIVE_DATA: 783 | log.debug(f"ROCKSTAR_SC_TS01a305c4: {str(filtered_cookies['TS01a305c4'].value)}") 784 | else: 785 | log.debug("ROCKSTAR_SC_TS01a305c4: ***") 786 | else: 787 | raise BackendError 788 | # We need to set the new refresh token here, if it is updated. 789 | try: 790 | self.set_refresh_token(resp.cookies['RMT'].value) 791 | except KeyError: 792 | if LOG_SENSITIVE_DATA: 793 | log.debug("ROCKSTAR_RMT_MISSING: The RMT cookie is missing, presumably because the user has not " 794 | "enabled two-factor authentication. Proceeding anyways...") 795 | self.set_refresh_token('') 796 | resp_json = await resp.json() 797 | url = resp_json["redirectUrl"] 798 | if LOG_SENSITIVE_DATA: 799 | log.debug(f"ROCKSTAR_SC_REDIRECT_URL: {url}") 800 | headers = { 801 | "Content-Type": "application/json", 802 | "Cookie": await self.get_cookies_for_headers(), 803 | "User-Agent": USER_AGENT, 804 | "X-Requested-With": "XMLHttpRequest" 805 | } 806 | resp = await self._current_session.get(url, headers=headers, allow_redirects=False) 807 | await self._update_cookies_from_response(resp) 808 | filtered_cookies = resp.cookies 809 | for key, morsel in filtered_cookies.items(): 810 | if key == "BearerToken": 811 | if LOG_SENSITIVE_DATA: 812 | log.debug(f"ROCKSTAR_SC_BEARER_NEW: {morsel.value}") 813 | else: 814 | log.debug(f"ROCKSTAR_SC_BEARER_NEW: {morsel.value[:5]}***{morsel.value[-3:]}") 815 | self._current_sc_token = morsel.value 816 | if old_auth != self._current_sc_token: 817 | log.debug("ROCKSTAR_SC_REFRESH_SUCCESS: The Social Club user has been successfully " 818 | "re-authenticated!") 819 | break 820 | except aiohttp.ClientConnectorError: 821 | log.error(f"ROCKSTAR_PLUGIN_OFFLINE: The user is not online.") 822 | self._refreshing = False 823 | raise NetworkError 824 | except Exception as e: 825 | log.exception(f"ROCKSTAR_SC_REFRESH_FAILURE: The attempt to re-authenticate the user on the Social Club has" 826 | f" failed with the exception {repr(e)}. Logging the user out...") 827 | self._refreshing = False 828 | raise InvalidCredentials 829 | 830 | async def authenticate(self): 831 | await self._refresh_credentials_social_club() 832 | if self._auth_lost_callback or self._debug_always_refresh: 833 | # We need to refresh the credentials. 834 | await self.refresh_credentials() 835 | self._auth_lost_callback = None 836 | 837 | self.bearer = self._current_sc_token 838 | if LOG_SENSITIVE_DATA: 839 | log.debug("ROCKSTAR_HTTP_CHECK: Got bearer token: " + self.bearer) 840 | else: 841 | log.debug(f"ROCKSTAR_HTTP_CHECK: Got bearer token: {self.bearer[:5]}***{self.bearer[-3:]}") 842 | 843 | # With the bearer token, we can now access the profile information. 844 | 845 | url = "https://scapi.rockstargames.com/profile/getbasicprofile" 846 | headers = { 847 | "Accept": ("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8," 848 | "application/signed-exchange;application/json;v=b3"), 849 | "Accept-Encoding": "gzip, deflate, br", 850 | "Accept-Language": "en-US, en;q=0.9", 851 | "Authorization": f"Bearer {self.bearer}", 852 | "Cache-Control": "max-age=0", 853 | "Connection": "keep-alive", 854 | "dnt": "1", 855 | "Host": "scapi.rockstargames.com", 856 | "Sec-Fetch-Mode": "navigate", 857 | "Sec-Fetch-Site": "none", 858 | "Sec-Fetch-User": "?1", 859 | "Upgrade-Insecure-Requests": "1", 860 | "X-Requested-With": "XMLHttpRequest", 861 | "User-Agent": USER_AGENT 862 | } 863 | try: 864 | resp_user = await self._current_session.get(url, headers=headers) 865 | await self._update_cookies_from_response(resp_user) 866 | resp_user_text = await resp_user.json() 867 | except Exception as e: 868 | log.exception("ERROR: There was a problem with getting the user information with the token. " 869 | "Exception: " + repr(e)) 870 | raise InvalidCredentials 871 | if LOG_SENSITIVE_DATA: 872 | log.debug(resp_user_text) 873 | working_dict = resp_user_text['accounts'][0]['rockstarAccount'] # The returned json is a nightmare. 874 | display_name = working_dict['displayName'] 875 | rockstar_id = working_dict['rockstarId'] 876 | if LOG_SENSITIVE_DATA: 877 | log.debug("ROCKSTAR_HTTP_CHECK: Got display name: " + display_name + " / Got Rockstar ID: " + 878 | str(rockstar_id)) 879 | else: 880 | log.debug(f"ROCKSTAR_HTTP_CHECK: Got display name: {display_name[:1]}*** / Got Rockstar ID: ***") 881 | self.user = {"display_name": display_name, "rockstar_id": str(rockstar_id)} 882 | log.debug("ROCKSTAR_STORE_CREDENTIALS: Preparing to store credentials...") 883 | # log.debug(self.get_credentials()) - Reduce Console Spam (Enable this if you need to.) 884 | self._store_credentials(self.get_credentials()) 885 | return self.user 886 | -------------------------------------------------------------------------------- /src/js/GenerateFingerprint.js: -------------------------------------------------------------------------------- 1 | window.onerror = function (msg, url, lineNum) { 2 | alert('Error: ' + msg + ' / Line Number: ' + lineNum); 3 | return false; 4 | } 5 | 6 | var options = { 7 | swfContainerId: 'fingerprintjs2', 8 | swfPath: 'flash/compiled/FontList.swf', 9 | detectScreenOrientation: false, 10 | sortPluginsFor: [/palemoon/i], 11 | excludeColorDepth: true, 12 | excludeScreenResolution: true, 13 | excludeAddBehavior: true, 14 | excludeHasLiedLanguages: true, 15 | excludeUserTamperedWithScreenRes: true, 16 | excludeHasLiedResolution: true, 17 | excludeHasLiedBrowser: true, 18 | excludeFlashFonts: true, 19 | excludeAvailableScreenResolution: true, 20 | excludeIEPlugins: true 21 | }; 22 | 23 | setTimeout(function() { 24 | var fp = new Fingerprint2(options); 25 | fp.get(function(result, components) { 26 | var fpString = '{"fp":{'; 27 | for(var i = 0; i < components.length; i++) { 28 | var object = components[i]; 29 | var key = object.key; 30 | if(typeof object.value == "string" || typeof object.value == "object") { 31 | var workingValue; 32 | if(typeof object.value.join !== "undefined") 33 | workingValue = object.value.join(";"); 34 | else 35 | workingValue = object.value; 36 | var value = (workingValue.length > 32 && key !== "device_name" ? 37 | x64hash128Gen(workingValue, 31) : 38 | workingValue); 39 | fpString += '"' + key + '":"' + value + '"'; 40 | } 41 | else { 42 | var value = object.value; 43 | fpString += '"' + key + '":' + value; 44 | } 45 | if(i != (components.length - 1)) 46 | fpString += ','; 47 | } 48 | fpString += '}}'; 49 | 50 | //Galaxy 2.0's cookie extraction cuts off the name of the cookie after the first semicolon (;), so all occurrences of semicolons 51 | //will be temporarily replaced with dollar signs ($), as I have yet to see this character used in a fingerprint. 52 | document.cookie = ('fingerprint=' + fpString.replace(/;/g, "$")); //+ '; expires=' + expiry + '; path=/'; 53 | }); 54 | }, 500); 55 | 56 | setTimeout(function () { 57 | window.location.href = "https://socialclub.rockstargames.com/"; 58 | }, 1000); 59 | -------------------------------------------------------------------------------- /src/js/HashGen.js: -------------------------------------------------------------------------------- 1 | // MurmurHash3 related functions 2 | 3 | // 4 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 5 | // added together as a 64bit int (as an array of two 32bit ints). 6 | // 7 | x64AddGen = function (m, n) { 8 | m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff] 9 | n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff] 10 | var o = [0, 0, 0, 0] 11 | o[3] += m[3] + n[3] 12 | o[2] += o[3] >>> 16 13 | o[3] &= 0xffff 14 | o[2] += m[2] + n[2] 15 | o[1] += o[2] >>> 16 16 | o[2] &= 0xffff 17 | o[1] += m[1] + n[1] 18 | o[0] += o[1] >>> 16 19 | o[1] &= 0xffff 20 | o[0] += m[0] + n[0] 21 | o[0] &= 0xffff 22 | return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]] 23 | }, 24 | 25 | // 26 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 27 | // multiplied together as a 64bit int (as an array of two 32bit ints). 28 | // 29 | x64MultiplyGen = function (m, n) { 30 | m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff] 31 | n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff] 32 | var o = [0, 0, 0, 0] 33 | o[3] += m[3] * n[3] 34 | o[2] += o[3] >>> 16 35 | o[3] &= 0xffff 36 | o[2] += m[2] * n[3] 37 | o[1] += o[2] >>> 16 38 | o[2] &= 0xffff 39 | o[2] += m[3] * n[2] 40 | o[1] += o[2] >>> 16 41 | o[2] &= 0xffff 42 | o[1] += m[1] * n[3] 43 | o[0] += o[1] >>> 16 44 | o[1] &= 0xffff 45 | o[1] += m[2] * n[2] 46 | o[0] += o[1] >>> 16 47 | o[1] &= 0xffff 48 | o[1] += m[3] * n[1] 49 | o[0] += o[1] >>> 16 50 | o[1] &= 0xffff 51 | o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0]) 52 | o[0] &= 0xffff 53 | return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]] 54 | }, 55 | // 56 | // Given a 64bit int (as an array of two 32bit ints) and an int 57 | // representing a number of bit positions, returns the 64bit int (as an 58 | // array of two 32bit ints) rotated left by that number of positions. 59 | // 60 | x64RotlGen = function (m, n) { 61 | n %= 64 62 | if (n === 32) { 63 | return [m[1], m[0]] 64 | } else if (n < 32) { 65 | return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))] 66 | } else { 67 | n -= 32 68 | return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))] 69 | } 70 | }, 71 | // 72 | // Given a 64bit int (as an array of two 32bit ints) and an int 73 | // representing a number of bit positions, returns the 64bit int (as an 74 | // array of two 32bit ints) shifted left by that number of positions. 75 | // 76 | x64LeftShiftGen = function (m, n) { 77 | n %= 64 78 | if (n === 0) { 79 | return m 80 | } else if (n < 32) { 81 | return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n] 82 | } else { 83 | return [m[1] << (n - 32), 0] 84 | } 85 | }, 86 | // 87 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 88 | // xored together as a 64bit int (as an array of two 32bit ints). 89 | // 90 | x64XorGen = function (m, n) { 91 | return [m[0] ^ n[0], m[1] ^ n[1]] 92 | }, 93 | // 94 | // Given a block, returns murmurHash3's final x64 mix of that block. 95 | // (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the 96 | // only place where we need to right shift 64bit ints.) 97 | // 98 | x64FmixGen = function (h) { 99 | h = this.x64XorGen(h, [0, h[0] >>> 1]) 100 | h = this.x64MultiplyGen(h, [0xff51afd7, 0xed558ccd]) 101 | h = this.x64XorGen(h, [0, h[0] >>> 1]) 102 | h = this.x64MultiplyGen(h, [0xc4ceb9fe, 0x1a85ec53]) 103 | h = this.x64XorGen(h, [0, h[0] >>> 1]) 104 | return h 105 | }, 106 | 107 | // 108 | // Given a string and an optional seed as an int, returns a 128 bit 109 | // hash using the x64 flavor of MurmurHash3, as an unsigned hex. 110 | // 111 | x64hash128Gen = function (key, seed) { 112 | key = key || '' 113 | seed = seed || 0 114 | var remainder = key.length % 16 115 | var bytes = key.length - remainder 116 | var h1 = [0, seed] 117 | var h2 = [0, seed] 118 | var k1 = [0, 0] 119 | var k2 = [0, 0] 120 | var c1 = [0x87c37b91, 0x114253d5] 121 | var c2 = [0x4cf5ad43, 0x2745937f] 122 | for (var i = 0; i < bytes; i = i + 16) { 123 | k1 = [((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24), ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24)] 124 | k2 = [((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24), ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24)] 125 | k1 = this.x64MultiplyGen(k1, c1) 126 | k1 = this.x64RotlGen(k1, 31) 127 | k1 = this.x64MultiplyGen(k1, c2) 128 | h1 = this.x64XorGen(h1, k1) 129 | h1 = this.x64RotlGen(h1, 27) 130 | h1 = this.x64AddGen(h1, h2) 131 | h1 = this.x64AddGen(this.x64MultiplyGen(h1, [0, 5]), [0, 0x52dce729]) 132 | k2 = this.x64MultiplyGen(k2, c2) 133 | k2 = this.x64RotlGen(k2, 33) 134 | k2 = this.x64MultiplyGen(k2, c1) 135 | h2 = this.x64XorGen(h2, k2) 136 | h2 = this.x64RotlGen(h2, 31) 137 | h2 = this.x64AddGen(h2, h1) 138 | h2 = this.x64AddGen(this.x64MultiplyGen(h2, [0, 5]), [0, 0x38495ab5]) 139 | } 140 | k1 = [0, 0] 141 | k2 = [0, 0] 142 | switch (remainder) { 143 | case 15: 144 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 14)], 48)) 145 | // fallthrough 146 | case 14: 147 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 13)], 40)) 148 | // fallthrough 149 | case 13: 150 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 12)], 32)) 151 | // fallthrough 152 | case 12: 153 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 11)], 24)) 154 | // fallthrough 155 | case 11: 156 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 10)], 16)) 157 | // fallthrough 158 | case 10: 159 | k2 = this.x64XorGen(k2, this.x64LeftShiftGen([0, key.charCodeAt(i + 9)], 8)) 160 | // fallthrough 161 | case 9: 162 | k2 = this.x64XorGen(k2, [0, key.charCodeAt(i + 8)]) 163 | k2 = this.x64MultiplyGen(k2, c2) 164 | k2 = this.x64RotlGen(k2, 33) 165 | k2 = this.x64MultiplyGen(k2, c1) 166 | h2 = this.x64XorGen(h2, k2) 167 | // fallthrough 168 | case 8: 169 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 7)], 56)) 170 | // fallthrough 171 | case 7: 172 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 6)], 48)) 173 | // fallthrough 174 | case 6: 175 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 5)], 40)) 176 | // fallthrough 177 | case 5: 178 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 4)], 32)) 179 | // fallthrough 180 | case 4: 181 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 3)], 24)) 182 | // fallthrough 183 | case 3: 184 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 2)], 16)) 185 | // fallthrough 186 | case 2: 187 | k1 = this.x64XorGen(k1, this.x64LeftShiftGen([0, key.charCodeAt(i + 1)], 8)) 188 | // fallthrough 189 | case 1: 190 | k1 = this.x64XorGen(k1, [0, key.charCodeAt(i)]) 191 | k1 = this.x64MultiplyGen(k1, c1) 192 | k1 = this.x64RotlGen(k1, 31) 193 | k1 = this.x64MultiplyGen(k1, c2) 194 | h1 = this.x64XorGen(h1, k1) 195 | // fallthrough 196 | } 197 | h1 = this.x64XorGen(h1, [0, key.length]) 198 | h2 = this.x64XorGen(h2, [0, key.length]) 199 | h1 = this.x64AddGen(h1, h2) 200 | h2 = this.x64AddGen(h2, h1) 201 | h1 = this.x64FmixGen(h1) 202 | h2 = this.x64FmixGen(h2) 203 | h1 = this.x64AddGen(h1, h2) 204 | h2 = this.x64AddGen(h2, h1) 205 | return ('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[1] >>> 0).toString(16)).slice(-8) 206 | } -------------------------------------------------------------------------------- /src/js/fingerprint2.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Fingerprintjs2 1.4.1 - Modern & flexible browser fingerprint library v2 3 | * https://github.com/Valve/fingerprintjs2 4 | * Copyright (c) 2015 Valentin Vasilyev (valentin.vasilyev@outlook.com) 5 | * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. 6 | * 7 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 8 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 9 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 10 | * ARE DISCLAIMED. IN NO EVENT SHALL VALENTIN VASILYEV BE LIABLE FOR ANY 11 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 12 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 13 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 14 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 15 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | */ 18 | 19 | (function (name, context, definition) { 20 | "use strict"; 21 | if (typeof module !== "undefined" && module.exports) { module.exports = definition(); } 22 | else if (typeof define === "function" && define.amd) { define(definition); } 23 | else { context[name] = definition(); } 24 | })("Fingerprint2", this, function() { 25 | "use strict"; 26 | // This will only be polyfilled for IE8 and older 27 | // Taken from Mozilla MDC 28 | if (!Array.prototype.indexOf) { 29 | Array.prototype.indexOf = function(searchElement, fromIndex) { 30 | var k; 31 | if (this == null) { 32 | throw new TypeError("'this' is null or undefined"); 33 | } 34 | var O = Object(this); 35 | var len = O.length >>> 0; 36 | if (len === 0) { 37 | return -1; 38 | } 39 | var n = +fromIndex || 0; 40 | if (Math.abs(n) === Infinity) { 41 | n = 0; 42 | } 43 | if (n >= len) { 44 | return -1; 45 | } 46 | k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); 47 | while (k < len) { 48 | if (k in O && O[k] === searchElement) { 49 | return k; 50 | } 51 | k++; 52 | } 53 | return -1; 54 | }; 55 | } 56 | var Fingerprint2 = function(options) { 57 | var defaultOptions = { 58 | swfContainerId: "fingerprintjs2", 59 | swfPath: "flash/compiled/FontList.swf", 60 | detectScreenOrientation: true, 61 | sortPluginsFor: [/palemoon/i], 62 | userDefinedFonts: [] 63 | }; 64 | this.options = this.extend(options, defaultOptions); 65 | this.nativeForEach = Array.prototype.forEach; 66 | this.nativeMap = Array.prototype.map; 67 | }; 68 | Fingerprint2.prototype = { 69 | extend: function(source, target) { 70 | if (source == null) { return target; } 71 | for (var k in source) { 72 | if(source[k] != null && target[k] !== source[k]) { 73 | target[k] = source[k]; 74 | } 75 | } 76 | return target; 77 | }, 78 | log: function(msg){ 79 | if(window.console){ 80 | console.log(msg); 81 | } 82 | }, 83 | get: function(done){ 84 | var keys = []; 85 | keys = this.userAgentKey(keys); 86 | keys = this.languageKey(keys); 87 | keys = this.colorDepthKey(keys); 88 | keys = this.pixelRatioKey(keys); 89 | keys = this.screenResolutionKey(keys); 90 | keys = this.availableScreenResolutionKey(keys); 91 | keys = this.timezoneOffsetKey(keys); 92 | keys = this.sessionStorageKey(keys); 93 | keys = this.localStorageKey(keys); 94 | keys = this.indexedDbKey(keys); 95 | keys = this.addBehaviorKey(keys); 96 | keys = this.openDatabaseKey(keys); 97 | keys = this.cpuClassKey(keys); 98 | keys = this.platformKey(keys); 99 | keys = this.doNotTrackKey(keys); 100 | keys = this.pluginsKey(keys); 101 | keys = this.canvasKey(keys); 102 | keys = this.webglKey(keys); 103 | keys = this.adBlockKey(keys); 104 | keys = this.hasLiedLanguagesKey(keys); 105 | keys = this.hasLiedResolutionKey(keys); 106 | keys = this.hasLiedOsKey(keys); 107 | keys = this.hasLiedBrowserKey(keys); 108 | keys = this.touchSupportKey(keys); 109 | keys = this.deviceNameKey(keys); 110 | var that = this; 111 | this.fontsKey(keys, function(newKeys){ 112 | var values = []; 113 | that.each(newKeys, function(pair) { 114 | var value = pair.value; 115 | if (typeof pair.value.join !== "undefined") { 116 | value = pair.value.join(";"); 117 | } 118 | values.push(value); 119 | }); 120 | var murmur = that.x64hash128(values.join("~~~"), 31); 121 | return done(murmur, newKeys); 122 | }); 123 | }, 124 | userAgentKey: function(keys) { 125 | if(!this.options.excludeUserAgent) { 126 | keys.push({key: "user_agent", value: this.getUserAgent()}); 127 | } 128 | return keys; 129 | }, 130 | // for tests 131 | getUserAgent: function(){ 132 | return navigator.userAgent; 133 | }, 134 | languageKey: function(keys) { 135 | if(!this.options.excludeLanguage) { 136 | // IE 9,10 on Windows 10 does not have the `navigator.language` property any longer 137 | keys.push({ key: "language", value: navigator.language || navigator.userLanguage || navigator.browserLanguage || navigator.systemLanguage || "" }); 138 | } 139 | return keys; 140 | }, 141 | colorDepthKey: function(keys) { 142 | if(!this.options.excludeColorDepth) { 143 | keys.push({key: "color_depth", value: screen.colorDepth}); 144 | } 145 | return keys; 146 | }, 147 | pixelRatioKey: function(keys) { 148 | if(!this.options.excludePixelRatio) { 149 | keys.push({key: "pixel_ratio", value: this.getPixelRatio()}); 150 | } 151 | return keys; 152 | }, 153 | getPixelRatio: function() { 154 | return window.devicePixelRatio || ""; 155 | }, 156 | screenResolutionKey: function(keys) { 157 | if(!this.options.excludeScreenResolution) { 158 | return this.getScreenResolution(keys); 159 | } 160 | return keys; 161 | }, 162 | getScreenResolution: function(keys) { 163 | var resolution; 164 | if(this.options.detectScreenOrientation) { 165 | resolution = (screen.height > screen.width) ? [screen.height, screen.width] : [screen.width, screen.height]; 166 | } else { 167 | resolution = [screen.width, screen.height]; 168 | } 169 | if(typeof resolution !== "undefined") { // headless browsers 170 | keys.push({key: "resolution", value: resolution}); 171 | } 172 | return keys; 173 | }, 174 | availableScreenResolutionKey: function(keys) { 175 | if (!this.options.excludeAvailableScreenResolution) { 176 | return this.getAvailableScreenResolution(keys); 177 | } 178 | return keys; 179 | }, 180 | getAvailableScreenResolution: function(keys) { 181 | var available; 182 | if(screen.availWidth && screen.availHeight) { 183 | if(this.options.detectScreenOrientation) { 184 | available = (screen.availHeight > screen.availWidth) ? [screen.availHeight, screen.availWidth] : [screen.availWidth, screen.availHeight]; 185 | } else { 186 | available = [screen.availHeight, screen.availWidth]; 187 | } 188 | } 189 | if(typeof available !== "undefined") { // headless browsers 190 | keys.push({key: "available_resolution", value: available}); 191 | } 192 | return keys; 193 | }, 194 | timezoneOffsetKey: function(keys) { 195 | if(!this.options.excludeTimezoneOffset) { 196 | keys.push({key: "timezone_offset", value: new Date().getTimezoneOffset()}); 197 | } 198 | return keys; 199 | }, 200 | sessionStorageKey: function(keys) { 201 | if(!this.options.excludeSessionStorage && this.hasSessionStorage()) { 202 | keys.push({key: "session_storage", value: 1}); 203 | } 204 | return keys; 205 | }, 206 | localStorageKey: function(keys) { 207 | if(!this.options.excludeSessionStorage && this.hasLocalStorage()) { 208 | keys.push({key: "local_storage", value: 1}); 209 | } 210 | return keys; 211 | }, 212 | indexedDbKey: function(keys) { 213 | if(!this.options.excludeIndexedDB && this.hasIndexedDB()) { 214 | keys.push({key: "indexed_db", value: 1}); 215 | } 216 | return keys; 217 | }, 218 | addBehaviorKey: function(keys) { 219 | //body might not be defined at this point or removed programmatically 220 | if(document.body && !this.options.excludeAddBehavior && document.body.addBehavior) { 221 | keys.push({key: "add_behavior", value: 1}); 222 | } 223 | return keys; 224 | }, 225 | openDatabaseKey: function(keys) { 226 | if(!this.options.excludeOpenDatabase && window.openDatabase) { 227 | keys.push({key: "open_database", value: 1}); 228 | } 229 | return keys; 230 | }, 231 | cpuClassKey: function(keys) { 232 | if(!this.options.excludeCpuClass) { 233 | keys.push({key: "cpu_class", value: this.getNavigatorCpuClass()}); 234 | } 235 | return keys; 236 | }, 237 | platformKey: function(keys) { 238 | if(!this.options.excludePlatform) { 239 | keys.push({key: "navigator_platform", value: this.getNavigatorPlatform()}); 240 | } 241 | return keys; 242 | }, 243 | doNotTrackKey: function(keys) { 244 | if(!this.options.excludeDoNotTrack) { 245 | keys.push({key: "do_not_track", value: this.getDoNotTrack()}); 246 | } 247 | return keys; 248 | }, 249 | canvasKey: function(keys) { 250 | if(!this.options.excludeCanvas && this.isCanvasSupported()) { 251 | keys.push({key: "canvas", value: this.getCanvasFp()}); 252 | } 253 | return keys; 254 | }, 255 | webglKey: function(keys) { 256 | if(this.options.excludeWebGL) { 257 | if(typeof NODEBUG === "undefined"){ 258 | this.log("Skipping WebGL fingerprinting per excludeWebGL configuration option"); 259 | } 260 | return keys; 261 | } 262 | if(!this.isWebGlSupported()) { 263 | if(typeof NODEBUG === "undefined"){ 264 | this.log("Skipping WebGL fingerprinting because it is not supported in this browser"); 265 | } 266 | return keys; 267 | } 268 | keys.push({key: "webgl", value: this.getWebglFp()}); 269 | return keys; 270 | }, 271 | adBlockKey: function(keys){ 272 | if(!this.options.excludeAdBlock) { 273 | keys.push({key: "adblock", value: this.getAdBlock()}); 274 | } 275 | return keys; 276 | }, 277 | hasLiedLanguagesKey: function(keys){ 278 | if(!this.options.excludeHasLiedLanguages){ 279 | keys.push({key: "has_lied_languages", value: this.getHasLiedLanguages()}); 280 | } 281 | return keys; 282 | }, 283 | hasLiedResolutionKey: function(keys){ 284 | if(!this.options.excludeHasLiedResolution){ 285 | keys.push({key: "has_lied_resolution", value: this.getHasLiedResolution()}); 286 | } 287 | return keys; 288 | }, 289 | hasLiedOsKey: function(keys){ 290 | if(!this.options.excludeHasLiedOs){ 291 | keys.push({key: "has_lied_os", value: this.getHasLiedOs()}); 292 | } 293 | return keys; 294 | }, 295 | hasLiedBrowserKey: function(keys){ 296 | if(!this.options.excludeHasLiedBrowser){ 297 | keys.push({key: "has_lied_browser", value: this.getHasLiedBrowser()}); 298 | } 299 | return keys; 300 | }, 301 | fontsKey: function(keys, done) { 302 | if (this.options.excludeJsFonts) { 303 | return this.flashFontsKey(keys, done); 304 | } 305 | return this.jsFontsKey(keys, done); 306 | }, 307 | // flash fonts (will increase fingerprinting time 20X to ~ 130-150ms) 308 | flashFontsKey: function(keys, done) { 309 | if(this.options.excludeFlashFonts) { 310 | if(typeof NODEBUG === "undefined"){ 311 | this.log("Skipping flash fonts detection per excludeFlashFonts configuration option"); 312 | } 313 | return done(keys); 314 | } 315 | // we do flash if swfobject is loaded 316 | if(!this.hasSwfObjectLoaded()){ 317 | if(typeof NODEBUG === "undefined"){ 318 | this.log("Swfobject is not detected, Flash fonts enumeration is skipped"); 319 | } 320 | return done(keys); 321 | } 322 | if(!this.hasMinFlashInstalled()){ 323 | if(typeof NODEBUG === "undefined"){ 324 | this.log("Flash is not installed, skipping Flash fonts enumeration"); 325 | } 326 | return done(keys); 327 | } 328 | if(typeof this.options.swfPath === "undefined"){ 329 | if(typeof NODEBUG === "undefined"){ 330 | this.log("To use Flash fonts detection, you must pass a valid swfPath option, skipping Flash fonts enumeration"); 331 | } 332 | return done(keys); 333 | } 334 | this.loadSwfAndDetectFonts(function(fonts){ 335 | keys.push({key: "swf_fonts", value: fonts.join(";")}); 336 | done(keys); 337 | }); 338 | }, 339 | // kudos to http://www.lalit.org/lab/javascript-css-font-detect/ 340 | jsFontsKey: function(keys, done) { 341 | var that = this; 342 | // doing js fonts detection in a pseudo-async fashion 343 | return setTimeout(function(){ 344 | 345 | // a font will be compared against all the three default fonts. 346 | // and if it doesn't match all 3 then that font is not available. 347 | var baseFonts = ["monospace", "sans-serif", "serif"]; 348 | 349 | var fontList = [ 350 | "Andale Mono", "Arial", "Arial Black", "Arial Hebrew", "Arial MT", "Arial Narrow", "Arial Rounded MT Bold", "Arial Unicode MS", 351 | "Bitstream Vera Sans Mono", "Book Antiqua", "Bookman Old Style", 352 | "Calibri", "Cambria", "Cambria Math", "Century", "Century Gothic", "Century Schoolbook", "Comic Sans", "Comic Sans MS", "Consolas", "Courier", "Courier New", 353 | "Garamond", "Geneva", "Georgia", 354 | "Helvetica", "Helvetica Neue", 355 | "Impact", 356 | "Lucida Bright", "Lucida Calligraphy", "Lucida Console", "Lucida Fax", "LUCIDA GRANDE", "Lucida Handwriting", "Lucida Sans", "Lucida Sans Typewriter", "Lucida Sans Unicode", 357 | "Microsoft Sans Serif", "Monaco", "Monotype Corsiva", "MS Gothic", "MS Outlook", "MS PGothic", "MS Reference Sans Serif", "MS Sans Serif", "MS Serif", "MYRIAD", "MYRIAD PRO", 358 | "Palatino", "Palatino Linotype", 359 | "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Light", "Segoe UI Semibold", "Segoe UI Symbol", 360 | "Tahoma", "Times", "Times New Roman", "Times New Roman PS", "Trebuchet MS", 361 | "Verdana", "Wingdings", "Wingdings 2", "Wingdings 3" 362 | ]; 363 | var extendedFontList = [ 364 | "Abadi MT Condensed Light", "Academy Engraved LET", "ADOBE CASLON PRO", "Adobe Garamond", "ADOBE GARAMOND PRO", "Agency FB", "Aharoni", "Albertus Extra Bold", "Albertus Medium", "Algerian", "Amazone BT", "American Typewriter", 365 | "American Typewriter Condensed", "AmerType Md BT", "Andalus", "Angsana New", "AngsanaUPC", "Antique Olive", "Aparajita", "Apple Chancery", "Apple Color Emoji", "Apple SD Gothic Neo", "Arabic Typesetting", "ARCHER", 366 | "ARNO PRO", "Arrus BT", "Aurora Cn BT", "AvantGarde Bk BT", "AvantGarde Md BT", "AVENIR", "Ayuthaya", "Bandy", "Bangla Sangam MN", "Bank Gothic", "BankGothic Md BT", "Baskerville", 367 | "Baskerville Old Face", "Batang", "BatangChe", "Bauer Bodoni", "Bauhaus 93", "Bazooka", "Bell MT", "Bembo", "Benguiat Bk BT", "Berlin Sans FB", "Berlin Sans FB Demi", "Bernard MT Condensed", "BernhardFashion BT", "BernhardMod BT", "Big Caslon", "BinnerD", 368 | "Blackadder ITC", "BlairMdITC TT", "Bodoni 72", "Bodoni 72 Oldstyle", "Bodoni 72 Smallcaps", "Bodoni MT", "Bodoni MT Black", "Bodoni MT Condensed", "Bodoni MT Poster Compressed", 369 | "Bookshelf Symbol 7", "Boulder", "Bradley Hand", "Bradley Hand ITC", "Bremen Bd BT", "Britannic Bold", "Broadway", "Browallia New", "BrowalliaUPC", "Brush Script MT", "Californian FB", "Calisto MT", "Calligrapher", "Candara", 370 | "CaslonOpnface BT", "Castellar", "Centaur", "Cezanne", "CG Omega", "CG Times", "Chalkboard", "Chalkboard SE", "Chalkduster", "Charlesworth", "Charter Bd BT", "Charter BT", "Chaucer", 371 | "ChelthmITC Bk BT", "Chiller", "Clarendon", "Clarendon Condensed", "CloisterBlack BT", "Cochin", "Colonna MT", "Constantia", "Cooper Black", "Copperplate", "Copperplate Gothic", "Copperplate Gothic Bold", 372 | "Copperplate Gothic Light", "CopperplGoth Bd BT", "Corbel", "Cordia New", "CordiaUPC", "Cornerstone", "Coronet", "Cuckoo", "Curlz MT", "DaunPenh", "Dauphin", "David", "DB LCD Temp", "DELICIOUS", "Denmark", 373 | "DFKai-SB", "Didot", "DilleniaUPC", "DIN", "DokChampa", "Dotum", "DotumChe", "Ebrima", "Edwardian Script ITC", "Elephant", "English 111 Vivace BT", "Engravers MT", "EngraversGothic BT", "Eras Bold ITC", "Eras Demi ITC", "Eras Light ITC", "Eras Medium ITC", 374 | "EucrosiaUPC", "Euphemia", "Euphemia UCAS", "EUROSTILE", "Exotc350 Bd BT", "FangSong", "Felix Titling", "Fixedsys", "FONTIN", "Footlight MT Light", "Forte", 375 | "FrankRuehl", "Fransiscan", "Freefrm721 Blk BT", "FreesiaUPC", "Freestyle Script", "French Script MT", "FrnkGothITC Bk BT", "Fruitger", "FRUTIGER", 376 | "Futura", "Futura Bk BT", "Futura Lt BT", "Futura Md BT", "Futura ZBlk BT", "FuturaBlack BT", "Gabriola", "Galliard BT", "Gautami", "Geeza Pro", "Geometr231 BT", "Geometr231 Hv BT", "Geometr231 Lt BT", "GeoSlab 703 Lt BT", 377 | "GeoSlab 703 XBd BT", "Gigi", "Gill Sans", "Gill Sans MT", "Gill Sans MT Condensed", "Gill Sans MT Ext Condensed Bold", "Gill Sans Ultra Bold", "Gill Sans Ultra Bold Condensed", "Gisha", "Gloucester MT Extra Condensed", "GOTHAM", "GOTHAM BOLD", 378 | "Goudy Old Style", "Goudy Stout", "GoudyHandtooled BT", "GoudyOLSt BT", "Gujarati Sangam MN", "Gulim", "GulimChe", "Gungsuh", "GungsuhChe", "Gurmukhi MN", "Haettenschweiler", "Harlow Solid Italic", "Harrington", "Heather", "Heiti SC", "Heiti TC", "HELV", 379 | "Herald", "High Tower Text", "Hiragino Kaku Gothic ProN", "Hiragino Mincho ProN", "Hoefler Text", "Humanst 521 Cn BT", "Humanst521 BT", "Humanst521 Lt BT", "Imprint MT Shadow", "Incised901 Bd BT", "Incised901 BT", 380 | "Incised901 Lt BT", "INCONSOLATA", "Informal Roman", "Informal011 BT", "INTERSTATE", "IrisUPC", "Iskoola Pota", "JasmineUPC", "Jazz LET", "Jenson", "Jester", "Jokerman", "Juice ITC", "Kabel Bk BT", "Kabel Ult BT", "Kailasa", "KaiTi", "Kalinga", "Kannada Sangam MN", 381 | "Kartika", "Kaufmann Bd BT", "Kaufmann BT", "Khmer UI", "KodchiangUPC", "Kokila", "Korinna BT", "Kristen ITC", "Krungthep", "Kunstler Script", "Lao UI", "Latha", "Leelawadee", "Letter Gothic", "Levenim MT", "LilyUPC", "Lithograph", "Lithograph Light", "Long Island", 382 | "Lydian BT", "Magneto", "Maiandra GD", "Malayalam Sangam MN", "Malgun Gothic", 383 | "Mangal", "Marigold", "Marion", "Marker Felt", "Market", "Marlett", "Matisse ITC", "Matura MT Script Capitals", "Meiryo", "Meiryo UI", "Microsoft Himalaya", "Microsoft JhengHei", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Tai Le", 384 | "Microsoft Uighur", "Microsoft YaHei", "Microsoft Yi Baiti", "MingLiU", "MingLiU_HKSCS", "MingLiU_HKSCS-ExtB", "MingLiU-ExtB", "Minion", "Minion Pro", "Miriam", "Miriam Fixed", "Mistral", "Modern", "Modern No. 20", "Mona Lisa Solid ITC TT", "Mongolian Baiti", 385 | "MONO", "MoolBoran", "Mrs Eaves", "MS LineDraw", "MS Mincho", "MS PMincho", "MS Reference Specialty", "MS UI Gothic", "MT Extra", "MUSEO", "MV Boli", 386 | "Nadeem", "Narkisim", "NEVIS", "News Gothic", "News GothicMT", "NewsGoth BT", "Niagara Engraved", "Niagara Solid", "Noteworthy", "NSimSun", "Nyala", "OCR A Extended", "Old Century", "Old English Text MT", "Onyx", "Onyx BT", "OPTIMA", "Oriya Sangam MN", 387 | "OSAKA", "OzHandicraft BT", "Palace Script MT", "Papyrus", "Parchment", "Party LET", "Pegasus", "Perpetua", "Perpetua Titling MT", "PetitaBold", "Pickwick", "Plantagenet Cherokee", "Playbill", "PMingLiU", "PMingLiU-ExtB", 388 | "Poor Richard", "Poster", "PosterBodoni BT", "PRINCETOWN LET", "Pristina", "PTBarnum BT", "Pythagoras", "Raavi", "Rage Italic", "Ravie", "Ribbon131 Bd BT", "Rockwell", "Rockwell Condensed", "Rockwell Extra Bold", "Rod", "Roman", "Sakkal Majalla", 389 | "Santa Fe LET", "Savoye LET", "Sceptre", "Script", "Script MT Bold", "SCRIPTINA", "Serifa", "Serifa BT", "Serifa Th BT", "ShelleyVolante BT", "Sherwood", 390 | "Shonar Bangla", "Showcard Gothic", "Shruti", "Signboard", "SILKSCREEN", "SimHei", "Simplified Arabic", "Simplified Arabic Fixed", "SimSun", "SimSun-ExtB", "Sinhala Sangam MN", "Sketch Rockwell", "Skia", "Small Fonts", "Snap ITC", "Snell Roundhand", "Socket", 391 | "Souvenir Lt BT", "Staccato222 BT", "Steamer", "Stencil", "Storybook", "Styllo", "Subway", "Swis721 BlkEx BT", "Swiss911 XCm BT", "Sylfaen", "Synchro LET", "System", "Tamil Sangam MN", "Technical", "Teletype", "Telugu Sangam MN", "Tempus Sans ITC", 392 | "Terminal", "Thonburi", "Traditional Arabic", "Trajan", "TRAJAN PRO", "Tristan", "Tubular", "Tunga", "Tw Cen MT", "Tw Cen MT Condensed", "Tw Cen MT Condensed Extra Bold", 393 | "TypoUpright BT", "Unicorn", "Univers", "Univers CE 55 Medium", "Univers Condensed", "Utsaah", "Vagabond", "Vani", "Vijaya", "Viner Hand ITC", "VisualUI", "Vivaldi", "Vladimir Script", "Vrinda", "Westminster", "WHITNEY", "Wide Latin", 394 | "ZapfEllipt BT", "ZapfHumnst BT", "ZapfHumnst Dm BT", "Zapfino", "Zurich BlkEx BT", "Zurich Ex BT", "ZWAdobeF"]; 395 | 396 | if(that.options.extendedJsFonts) { 397 | fontList = fontList.concat(extendedFontList); 398 | } 399 | 400 | fontList = fontList.concat(that.options.userDefinedFonts); 401 | 402 | //we use m or w because these two characters take up the maximum width. 403 | // And we use a LLi so that the same matching fonts can get separated 404 | var testString = "mmmmmmmmmmlli"; 405 | 406 | //we test using 72px font size, we may use any size. I guess larger the better. 407 | var testSize = "72px"; 408 | 409 | var h = document.getElementsByTagName("body")[0]; 410 | 411 | // div to load spans for the base fonts 412 | var baseFontsDiv = document.createElement("div"); 413 | 414 | // div to load spans for the fonts to detect 415 | var fontsDiv = document.createElement("div"); 416 | 417 | var defaultWidth = {}; 418 | var defaultHeight = {}; 419 | 420 | // creates a span where the fonts will be loaded 421 | var createSpan = function() { 422 | var s = document.createElement("span"); 423 | /* 424 | * We need this css as in some weird browser this 425 | * span elements shows up for a microSec which creates a 426 | * bad user experience 427 | */ 428 | s.style.position = "absolute"; 429 | s.style.left = "-9999px"; 430 | s.style.fontSize = testSize; 431 | s.innerHTML = testString; 432 | return s; 433 | }; 434 | 435 | // creates a span and load the font to detect and a base font for fallback 436 | var createSpanWithFonts = function(fontToDetect, baseFont) { 437 | var s = createSpan(); 438 | s.style.fontFamily = "'" + fontToDetect + "'," + baseFont; 439 | return s; 440 | }; 441 | 442 | // creates spans for the base fonts and adds them to baseFontsDiv 443 | var initializeBaseFontsSpans = function() { 444 | var spans = []; 445 | for (var index = 0, length = baseFonts.length; index < length; index++) { 446 | var s = createSpan(); 447 | s.style.fontFamily = baseFonts[index]; 448 | baseFontsDiv.appendChild(s); 449 | spans.push(s); 450 | } 451 | return spans; 452 | }; 453 | 454 | // creates spans for the fonts to detect and adds them to fontsDiv 455 | var initializeFontsSpans = function() { 456 | var spans = {}; 457 | for(var i = 0, l = fontList.length; i < l; i++) { 458 | var fontSpans = []; 459 | for(var j = 0, numDefaultFonts = baseFonts.length; j < numDefaultFonts; j++) { 460 | var s = createSpanWithFonts(fontList[i], baseFonts[j]); 461 | fontsDiv.appendChild(s); 462 | fontSpans.push(s); 463 | } 464 | spans[fontList[i]] = fontSpans; // Stores {fontName : [spans for that font]} 465 | } 466 | return spans; 467 | }; 468 | 469 | // checks if a font is available 470 | var isFontAvailable = function(fontSpans) { 471 | var detected = false; 472 | for(var i = 0; i < baseFonts.length; i++) { 473 | detected = (fontSpans[i].offsetWidth !== defaultWidth[baseFonts[i]] || fontSpans[i].offsetHeight !== defaultHeight[baseFonts[i]]); 474 | if(detected) { 475 | return detected; 476 | } 477 | } 478 | return detected; 479 | }; 480 | 481 | // create spans for base fonts 482 | var baseFontsSpans = initializeBaseFontsSpans(); 483 | 484 | // add the spans to the DOM 485 | h.appendChild(baseFontsDiv); 486 | 487 | // get the default width for the three base fonts 488 | for (var index = 0, length = baseFonts.length; index < length; index++) { 489 | defaultWidth[baseFonts[index]] = baseFontsSpans[index].offsetWidth; // width for the default font 490 | defaultHeight[baseFonts[index]] = baseFontsSpans[index].offsetHeight; // height for the default font 491 | } 492 | 493 | // create spans for fonts to detect 494 | var fontsSpans = initializeFontsSpans(); 495 | 496 | // add all the spans to the DOM 497 | h.appendChild(fontsDiv); 498 | 499 | // check available fonts 500 | var available = []; 501 | for(var i = 0, l = fontList.length; i < l; i++) { 502 | if(isFontAvailable(fontsSpans[fontList[i]])) { 503 | available.push(fontList[i]); 504 | } 505 | } 506 | 507 | // remove spans from DOM 508 | h.removeChild(fontsDiv); 509 | h.removeChild(baseFontsDiv); 510 | 511 | keys.push({key: "js_fonts", value: available}); 512 | done(keys); 513 | }, 1); 514 | }, 515 | deviceNameKey: function (keys) { 516 | if(navigator.platform == "Win32") 517 | keys.push({ key: "device_name", value: "Chrome on Windows" }); 518 | else 519 | keys.push({ key: "device_name", value: "Chrome on Mac" }); 520 | return keys; 521 | }, 522 | pluginsKey: function(keys) { 523 | if(!this.options.excludePlugins){ 524 | if(this.isIE()){ 525 | if(!this.options.excludeIEPlugins) { 526 | keys.push({key: "ie_plugins", value: this.getIEPlugins()}); 527 | } 528 | } else { 529 | keys.push({key: "regular_plugins", value: this.getRegularPlugins()}); 530 | } 531 | } 532 | return keys; 533 | }, 534 | getRegularPlugins: function () { 535 | var plugins = []; 536 | for(var i = 0, l = navigator.plugins.length; i < l; i++) { 537 | plugins.push(navigator.plugins[i]); 538 | } 539 | // sorting plugins only for those user agents, that we know randomize the plugins 540 | // every time we try to enumerate them 541 | if(this.pluginsShouldBeSorted()) { 542 | plugins = plugins.sort(function(a, b) { 543 | if(a.name > b.name){ return 1; } 544 | if(a.name < b.name){ return -1; } 545 | return 0; 546 | }); 547 | } 548 | return this.map(plugins, function (p) { 549 | var mimeTypes = this.map(p, function(mt){ 550 | return [mt.type, mt.suffixes].join("~"); 551 | }).join(","); 552 | return [p.name, p.description, mimeTypes].join("::"); 553 | }, this); 554 | }, 555 | getIEPlugins: function () { 556 | var result = []; 557 | if((Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(window, "ActiveXObject")) || ("ActiveXObject" in window)) { 558 | var names = [ 559 | "AcroPDF.PDF", // Adobe PDF reader 7+ 560 | "Adodb.Stream", 561 | "AgControl.AgControl", // Silverlight 562 | "DevalVRXCtrl.DevalVRXCtrl.1", 563 | "MacromediaFlashPaper.MacromediaFlashPaper", 564 | "Msxml2.DOMDocument", 565 | "Msxml2.XMLHTTP", 566 | "PDF.PdfCtrl", // Adobe PDF reader 6 and earlier, brrr 567 | "QuickTime.QuickTime", // QuickTime 568 | "QuickTimeCheckObject.QuickTimeCheck.1", 569 | "RealPlayer", 570 | "RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)", 571 | "RealVideo.RealVideo(tm) ActiveX Control (32-bit)", 572 | "Scripting.Dictionary", 573 | "SWCtl.SWCtl", // ShockWave player 574 | "Shell.UIHelper", 575 | "ShockwaveFlash.ShockwaveFlash", //flash plugin 576 | "Skype.Detection", 577 | "TDCCtl.TDCCtl", 578 | "WMPlayer.OCX", // Windows media player 579 | "rmocx.RealPlayer G2 Control", 580 | "rmocx.RealPlayer G2 Control.1" 581 | ]; 582 | // starting to detect plugins in IE 583 | result = this.map(names, function(name) { 584 | try { 585 | new ActiveXObject(name); // eslint-disable-no-new 586 | return name; 587 | } catch(e) { 588 | return null; 589 | } 590 | }); 591 | } 592 | if(navigator.plugins) { 593 | result = result.concat(this.getRegularPlugins()); 594 | } 595 | return result; 596 | }, 597 | pluginsShouldBeSorted: function () { 598 | var should = false; 599 | for(var i = 0, l = this.options.sortPluginsFor.length; i < l; i++) { 600 | var re = this.options.sortPluginsFor[i]; 601 | if(navigator.userAgent.match(re)) { 602 | should = true; 603 | break; 604 | } 605 | } 606 | return should; 607 | }, 608 | touchSupportKey: function (keys) { 609 | if(!this.options.excludeTouchSupport){ 610 | keys.push({key: "touch_support", value: this.getTouchSupport()}); 611 | } 612 | return keys; 613 | }, 614 | hasSessionStorage: function () { 615 | try { 616 | return !!window.sessionStorage; 617 | } catch(e) { 618 | return true; // SecurityError when referencing it means it exists 619 | } 620 | }, 621 | // https://bugzilla.mozilla.org/show_bug.cgi?id=781447 622 | hasLocalStorage: function () { 623 | try { 624 | return !!window.localStorage; 625 | } catch(e) { 626 | return true; // SecurityError when referencing it means it exists 627 | } 628 | }, 629 | hasIndexedDB: function (){ 630 | return !!window.indexedDB; 631 | }, 632 | getNavigatorCpuClass: function () { 633 | if(navigator.cpuClass){ 634 | return navigator.cpuClass; 635 | } else { 636 | return "unknown"; 637 | } 638 | }, 639 | getNavigatorPlatform: function () { 640 | if(navigator.platform) { 641 | return navigator.platform; 642 | } else { 643 | return "unknown"; 644 | } 645 | }, 646 | getDoNotTrack: function () { 647 | if(navigator.doNotTrack) { 648 | return navigator.doNotTrack; 649 | } else { 650 | return "unknown"; 651 | } 652 | }, 653 | // This is a crude and primitive touch screen detection. 654 | // It's not possible to currently reliably detect the availability of a touch screen 655 | // with a JS, without actually subscribing to a touch event. 656 | // http://www.stucox.com/blog/you-cant-detect-a-touchscreen/ 657 | // https://github.com/Modernizr/Modernizr/issues/548 658 | // method returns an array of 3 values: 659 | // maxTouchPoints, the success or failure of creating a TouchEvent, 660 | // and the availability of the 'ontouchstart' property 661 | getTouchSupport: function () { 662 | var maxTouchPoints = 0; 663 | var touchEvent = false; 664 | if(typeof navigator.maxTouchPoints !== "undefined") { 665 | maxTouchPoints = navigator.maxTouchPoints; 666 | } else if (typeof navigator.msMaxTouchPoints !== "undefined") { 667 | maxTouchPoints = navigator.msMaxTouchPoints; 668 | } 669 | try { 670 | document.createEvent("TouchEvent"); 671 | touchEvent = true; 672 | } catch(_) { /* squelch */ } 673 | var touchStart = "ontouchstart" in window; 674 | return [maxTouchPoints, touchEvent, touchStart]; 675 | }, 676 | // https://www.browserleaks.com/canvas#how-does-it-work 677 | getCanvasFp: function() { 678 | var result = []; 679 | // Very simple now, need to make it more complex (geo shapes etc) 680 | var canvas = document.createElement("canvas"); 681 | canvas.width = 2000; 682 | canvas.height = 200; 683 | canvas.style.display = "inline"; 684 | var ctx = canvas.getContext("2d"); 685 | // detect browser support of canvas winding 686 | // http://blogs.adobe.com/webplatform/2013/01/30/winding-rules-in-canvas/ 687 | // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/canvas/winding.js 688 | ctx.rect(0, 0, 10, 10); 689 | ctx.rect(2, 2, 6, 6); 690 | result.push("canvas winding:" + ((ctx.isPointInPath(5, 5, "evenodd") === false) ? "yes" : "no")); 691 | 692 | ctx.textBaseline = "alphabetic"; 693 | ctx.fillStyle = "#f60"; 694 | ctx.fillRect(125, 1, 62, 20); 695 | ctx.fillStyle = "#069"; 696 | // https://github.com/Valve/fingerprintjs2/issues/66 697 | if(this.options.dontUseFakeFontInCanvas) { 698 | ctx.font = "11pt Arial"; 699 | } else { 700 | ctx.font = "11pt no-real-font-123"; 701 | } 702 | ctx.fillText("Cwm fjordbank glyphs vext quiz, \ud83d\ude03", 2, 15); 703 | ctx.fillStyle = "rgba(102, 204, 0, 0.2)"; 704 | ctx.font = "18pt Arial"; 705 | ctx.fillText("Cwm fjordbank glyphs vext quiz, \ud83d\ude03", 4, 45); 706 | 707 | // canvas blending 708 | // http://blogs.adobe.com/webplatform/2013/01/28/blending-features-in-canvas/ 709 | // http://jsfiddle.net/NDYV8/16/ 710 | ctx.globalCompositeOperation = "multiply"; 711 | ctx.fillStyle = "rgb(255,0,255)"; 712 | ctx.beginPath(); 713 | ctx.arc(50, 50, 50, 0, Math.PI * 2, true); 714 | ctx.closePath(); 715 | ctx.fill(); 716 | ctx.fillStyle = "rgb(0,255,255)"; 717 | ctx.beginPath(); 718 | ctx.arc(100, 50, 50, 0, Math.PI * 2, true); 719 | ctx.closePath(); 720 | ctx.fill(); 721 | ctx.fillStyle = "rgb(255,255,0)"; 722 | ctx.beginPath(); 723 | ctx.arc(75, 100, 50, 0, Math.PI * 2, true); 724 | ctx.closePath(); 725 | ctx.fill(); 726 | ctx.fillStyle = "rgb(255,0,255)"; 727 | // canvas winding 728 | // http://blogs.adobe.com/webplatform/2013/01/30/winding-rules-in-canvas/ 729 | // http://jsfiddle.net/NDYV8/19/ 730 | ctx.arc(75, 75, 75, 0, Math.PI * 2, true); 731 | ctx.arc(75, 75, 25, 0, Math.PI * 2, true); 732 | ctx.fill("evenodd"); 733 | 734 | result.push("canvas fp:" + canvas.toDataURL()); 735 | return result.join("~"); 736 | }, 737 | 738 | getWebglFp: function() { 739 | var gl; 740 | var fa2s = function(fa) { 741 | gl.clearColor(0.0, 0.0, 0.0, 1.0); 742 | gl.enable(gl.DEPTH_TEST); 743 | gl.depthFunc(gl.LEQUAL); 744 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 745 | return "[" + fa[0] + ", " + fa[1] + "]"; 746 | }; 747 | var maxAnisotropy = function(gl) { 748 | var anisotropy, ext = gl.getExtension("EXT_texture_filter_anisotropic") || gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic") || gl.getExtension("MOZ_EXT_texture_filter_anisotropic"); 749 | return ext ? (anisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT), 0 === anisotropy && (anisotropy = 2), anisotropy) : null; 750 | }; 751 | gl = this.getWebglCanvas(); 752 | if(!gl) { return null; } 753 | // WebGL fingerprinting is a combination of techniques, found in MaxMind antifraud script & Augur fingerprinting. 754 | // First it draws a gradient object with shaders and convers the image to the Base64 string. 755 | // Then it enumerates all WebGL extensions & capabilities and appends them to the Base64 string, resulting in a huge WebGL string, potentially very unique on each device 756 | // Since iOS supports webgl starting from version 8.1 and 8.1 runs on several graphics chips, the results may be different across ios devices, but we need to verify it. 757 | var result = []; 758 | var vShaderTemplate = "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}"; 759 | var fShaderTemplate = "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}"; 760 | var vertexPosBuffer = gl.createBuffer(); 761 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); 762 | var vertices = new Float32Array([-.2, -.9, 0, .4, -.26, 0, 0, .732134444, 0]); 763 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 764 | vertexPosBuffer.itemSize = 3; 765 | vertexPosBuffer.numItems = 3; 766 | var program = gl.createProgram(), vshader = gl.createShader(gl.VERTEX_SHADER); 767 | gl.shaderSource(vshader, vShaderTemplate); 768 | gl.compileShader(vshader); 769 | var fshader = gl.createShader(gl.FRAGMENT_SHADER); 770 | gl.shaderSource(fshader, fShaderTemplate); 771 | gl.compileShader(fshader); 772 | gl.attachShader(program, vshader); 773 | gl.attachShader(program, fshader); 774 | gl.linkProgram(program); 775 | gl.useProgram(program); 776 | program.vertexPosAttrib = gl.getAttribLocation(program, "attrVertex"); 777 | program.offsetUniform = gl.getUniformLocation(program, "uniformOffset"); 778 | gl.enableVertexAttribArray(program.vertexPosArray); 779 | gl.vertexAttribPointer(program.vertexPosAttrib, vertexPosBuffer.itemSize, gl.FLOAT, !1, 0, 0); 780 | gl.uniform2f(program.offsetUniform, 1, 1); 781 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexPosBuffer.numItems); 782 | if (gl.canvas != null) { result.push(gl.canvas.toDataURL()); } 783 | result.push("extensions:" + gl.getSupportedExtensions().join(";")); 784 | result.push("webgl aliased line width range:" + fa2s(gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE))); 785 | result.push("webgl aliased point size range:" + fa2s(gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE))); 786 | result.push("webgl alpha bits:" + gl.getParameter(gl.ALPHA_BITS)); 787 | result.push("webgl antialiasing:" + (gl.getContextAttributes().antialias ? "yes" : "no")); 788 | result.push("webgl blue bits:" + gl.getParameter(gl.BLUE_BITS)); 789 | result.push("webgl depth bits:" + gl.getParameter(gl.DEPTH_BITS)); 790 | result.push("webgl green bits:" + gl.getParameter(gl.GREEN_BITS)); 791 | result.push("webgl max anisotropy:" + maxAnisotropy(gl)); 792 | result.push("webgl max combined texture image units:" + gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)); 793 | result.push("webgl max cube map texture size:" + gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)); 794 | result.push("webgl max fragment uniform vectors:" + gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)); 795 | result.push("webgl max render buffer size:" + gl.getParameter(gl.MAX_RENDERBUFFER_SIZE)); 796 | result.push("webgl max texture image units:" + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)); 797 | result.push("webgl max texture size:" + gl.getParameter(gl.MAX_TEXTURE_SIZE)); 798 | result.push("webgl max varying vectors:" + gl.getParameter(gl.MAX_VARYING_VECTORS)); 799 | result.push("webgl max vertex attribs:" + gl.getParameter(gl.MAX_VERTEX_ATTRIBS)); 800 | result.push("webgl max vertex texture image units:" + gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS)); 801 | result.push("webgl max vertex uniform vectors:" + gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS)); 802 | result.push("webgl max viewport dims:" + fa2s(gl.getParameter(gl.MAX_VIEWPORT_DIMS))); 803 | result.push("webgl red bits:" + gl.getParameter(gl.RED_BITS)); 804 | result.push("webgl renderer:" + gl.getParameter(gl.RENDERER)); 805 | result.push("webgl shading language version:" + gl.getParameter(gl.SHADING_LANGUAGE_VERSION)); 806 | result.push("webgl stencil bits:" + gl.getParameter(gl.STENCIL_BITS)); 807 | result.push("webgl vendor:" + gl.getParameter(gl.VENDOR)); 808 | result.push("webgl version:" + gl.getParameter(gl.VERSION)); 809 | 810 | if (!gl.getShaderPrecisionFormat) { 811 | if (typeof NODEBUG === "undefined") { 812 | this.log("WebGL fingerprinting is incomplete, because your browser does not support getShaderPrecisionFormat"); 813 | } 814 | return result.join("~"); 815 | } 816 | 817 | result.push("webgl vertex shader high float precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT ).precision); 818 | result.push("webgl vertex shader high float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT ).rangeMin); 819 | result.push("webgl vertex shader high float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT ).rangeMax); 820 | result.push("webgl vertex shader medium float precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).precision); 821 | result.push("webgl vertex shader medium float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).rangeMin); 822 | result.push("webgl vertex shader medium float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).rangeMax); 823 | result.push("webgl vertex shader low float precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT ).precision); 824 | result.push("webgl vertex shader low float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT ).rangeMin); 825 | result.push("webgl vertex shader low float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT ).rangeMax); 826 | result.push("webgl fragment shader high float precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).precision); 827 | result.push("webgl fragment shader high float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).rangeMin); 828 | result.push("webgl fragment shader high float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).rangeMax); 829 | result.push("webgl fragment shader medium float precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).precision); 830 | result.push("webgl fragment shader medium float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).rangeMin); 831 | result.push("webgl fragment shader medium float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).rangeMax); 832 | result.push("webgl fragment shader low float precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT ).precision); 833 | result.push("webgl fragment shader low float precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT ).rangeMin); 834 | result.push("webgl fragment shader low float precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT ).rangeMax); 835 | result.push("webgl vertex shader high int precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT ).precision); 836 | result.push("webgl vertex shader high int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT ).rangeMin); 837 | result.push("webgl vertex shader high int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT ).rangeMax); 838 | result.push("webgl vertex shader medium int precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT ).precision); 839 | result.push("webgl vertex shader medium int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT ).rangeMin); 840 | result.push("webgl vertex shader medium int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT ).rangeMax); 841 | result.push("webgl vertex shader low int precision:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT ).precision); 842 | result.push("webgl vertex shader low int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT ).rangeMin); 843 | result.push("webgl vertex shader low int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT ).rangeMax); 844 | result.push("webgl fragment shader high int precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT ).precision); 845 | result.push("webgl fragment shader high int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT ).rangeMin); 846 | result.push("webgl fragment shader high int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT ).rangeMax); 847 | result.push("webgl fragment shader medium int precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT ).precision); 848 | result.push("webgl fragment shader medium int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT ).rangeMin); 849 | result.push("webgl fragment shader medium int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT ).rangeMax); 850 | result.push("webgl fragment shader low int precision:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT ).precision); 851 | result.push("webgl fragment shader low int precision rangeMin:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT ).rangeMin); 852 | result.push("webgl fragment shader low int precision rangeMax:" + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT ).rangeMax); 853 | return result.join("~"); 854 | }, 855 | getAdBlock: function(){ 856 | var ads = document.createElement("div"); 857 | ads.innerHTML = " "; 858 | ads.className = "adsbox"; 859 | var result = false; 860 | try { 861 | // body may not exist, that's why we need try/catch 862 | document.body.appendChild(ads); 863 | result = document.getElementsByClassName("adsbox")[0].offsetHeight === 0; 864 | document.body.removeChild(ads); 865 | } catch (e) { 866 | result = false; 867 | } 868 | return result; 869 | }, 870 | getHasLiedLanguages: function(){ 871 | //We check if navigator.language is equal to the first language of navigator.languages 872 | if(typeof navigator.languages !== "undefined"){ 873 | try { 874 | var firstLanguages = navigator.languages[0].substr(0, 2); 875 | if(firstLanguages !== navigator.language.substr(0, 2)){ 876 | return true; 877 | } 878 | } catch(err) { 879 | return true; 880 | } 881 | } 882 | return false; 883 | }, 884 | getHasLiedResolution: function(){ 885 | if(screen.width < screen.availWidth){ 886 | return true; 887 | } 888 | if(screen.height < screen.availHeight){ 889 | return true; 890 | } 891 | return false; 892 | }, 893 | getHasLiedOs: function(){ 894 | var userAgent = navigator.userAgent.toLowerCase(); 895 | var oscpu = navigator.oscpu; 896 | var platform = navigator.platform.toLowerCase(); 897 | var os; 898 | //We extract the OS from the user agent (respect the order of the if else if statement) 899 | if(userAgent.indexOf("windows phone") >= 0){ 900 | os = "Windows Phone"; 901 | } else if(userAgent.indexOf("win") >= 0){ 902 | os = "Windows"; 903 | } else if(userAgent.indexOf("android") >= 0){ 904 | os = "Android"; 905 | } else if(userAgent.indexOf("linux") >= 0){ 906 | os = "Linux"; 907 | } else if(userAgent.indexOf("iphone") >= 0 || userAgent.indexOf("ipad") >= 0 ){ 908 | os = "iOS"; 909 | } else if(userAgent.indexOf("mac") >= 0){ 910 | os = "Mac"; 911 | } else{ 912 | os = "Other"; 913 | } 914 | // We detect if the person uses a mobile device 915 | var mobileDevice; 916 | if (("ontouchstart" in window) || 917 | (navigator.maxTouchPoints > 0) || 918 | (navigator.msMaxTouchPoints > 0)) { 919 | mobileDevice = true; 920 | } else{ 921 | mobileDevice = false; 922 | } 923 | 924 | if(mobileDevice && os !== "Windows Phone" && os !== "Android" && os !== "iOS" && os !== "Other"){ 925 | return true; 926 | } 927 | 928 | // We compare oscpu with the OS extracted from the UA 929 | if(typeof oscpu !== "undefined"){ 930 | oscpu = oscpu.toLowerCase(); 931 | if(oscpu.indexOf("win") >= 0 && os !== "Windows" && os !== "Windows Phone"){ 932 | return true; 933 | } else if(oscpu.indexOf("linux") >= 0 && os !== "Linux" && os !== "Android"){ 934 | return true; 935 | } else if(oscpu.indexOf("mac") >= 0 && os !== "Mac" && os !== "iOS"){ 936 | return true; 937 | } else if(oscpu.indexOf("win") === 0 && oscpu.indexOf("linux") === 0 && oscpu.indexOf("mac") >= 0 && os !== "other"){ 938 | return true; 939 | } 940 | } 941 | 942 | //We compare platform with the OS extracted from the UA 943 | if(platform.indexOf("win") >= 0 && os !== "Windows" && os !== "Windows Phone"){ 944 | return true; 945 | } else if((platform.indexOf("linux") >= 0 || platform.indexOf("android") >= 0 || platform.indexOf("pike") >= 0) && os !== "Linux" && os !== "Android"){ 946 | return true; 947 | } else if((platform.indexOf("mac") >= 0 || platform.indexOf("ipad") >= 0 || platform.indexOf("ipod") >= 0 || platform.indexOf("iphone") >= 0) && os !== "Mac" && os !== "iOS"){ 948 | return true; 949 | } else if(platform.indexOf("win") === 0 && platform.indexOf("linux") === 0 && platform.indexOf("mac") >= 0 && os !== "other"){ 950 | return true; 951 | } 952 | 953 | if(typeof navigator.plugins === "undefined" && os !== "Windows" && os !== "Windows Phone"){ 954 | //We are are in the case where the person uses ie, therefore we can infer that it's windows 955 | return true; 956 | } 957 | 958 | return false; 959 | }, 960 | getHasLiedBrowser: function () { 961 | var userAgent = navigator.userAgent.toLowerCase(); 962 | var productSub = navigator.productSub; 963 | 964 | //we extract the browser from the user agent (respect the order of the tests) 965 | var browser; 966 | if(userAgent.indexOf("firefox") >= 0){ 967 | browser = "Firefox"; 968 | } else if(userAgent.indexOf("opera") >= 0 || userAgent.indexOf("opr") >= 0){ 969 | browser = "Opera"; 970 | } else if(userAgent.indexOf("chrome") >= 0){ 971 | browser = "Chrome"; 972 | } else if(userAgent.indexOf("safari") >= 0){ 973 | browser = "Safari"; 974 | } else if(userAgent.indexOf("trident") >= 0){ 975 | browser = "Internet Explorer"; 976 | } else{ 977 | browser = "Other"; 978 | } 979 | 980 | if((browser === "Chrome" || browser === "Safari" || browser === "Opera") && productSub !== "20030107"){ 981 | return true; 982 | } 983 | 984 | var tempRes = eval.toString().length; 985 | if(tempRes === 37 && browser !== "Safari" && browser !== "Firefox" && browser !== "Other"){ 986 | return true; 987 | } else if(tempRes === 39 && browser !== "Internet Explorer" && browser !== "Other"){ 988 | return true; 989 | } else if(tempRes === 33 && browser !== "Chrome" && browser !== "Opera" && browser !== "Other"){ 990 | return true; 991 | } 992 | 993 | //We create an error to see how it is handled 994 | var errFirefox; 995 | try { 996 | throw "a"; 997 | } catch(err){ 998 | try{ 999 | err.toSource(); 1000 | errFirefox = true; 1001 | } catch(errOfErr){ 1002 | errFirefox = false; 1003 | } 1004 | } 1005 | if(errFirefox && browser !== "Firefox" && browser !== "Other"){ 1006 | return true; 1007 | } 1008 | return false; 1009 | }, 1010 | isCanvasSupported: function () { 1011 | var elem = document.createElement("canvas"); 1012 | return !!(elem.getContext && elem.getContext("2d")); 1013 | }, 1014 | isWebGlSupported: function() { 1015 | // code taken from Modernizr 1016 | if (!this.isCanvasSupported()) { 1017 | return false; 1018 | } 1019 | 1020 | var canvas = document.createElement("canvas"), 1021 | glContext; 1022 | 1023 | try { 1024 | glContext = canvas.getContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")); 1025 | } catch(e) { 1026 | glContext = false; 1027 | } 1028 | 1029 | return !!window.WebGLRenderingContext && !!glContext; 1030 | }, 1031 | isIE: function () { 1032 | if(navigator.appName === "Microsoft Internet Explorer") { 1033 | return true; 1034 | } else if(navigator.appName === "Netscape" && /Trident/.test(navigator.userAgent)) { // IE 11 1035 | return true; 1036 | } 1037 | return false; 1038 | }, 1039 | hasSwfObjectLoaded: function(){ 1040 | return typeof window.swfobject !== "undefined"; 1041 | }, 1042 | hasMinFlashInstalled: function () { 1043 | return swfobject.hasFlashPlayerVersion("9.0.0"); 1044 | }, 1045 | addFlashDivNode: function() { 1046 | var node = document.createElement("div"); 1047 | node.setAttribute("id", this.options.swfContainerId); 1048 | document.body.appendChild(node); 1049 | }, 1050 | loadSwfAndDetectFonts: function(done) { 1051 | var hiddenCallback = "___fp_swf_loaded"; 1052 | window[hiddenCallback] = function(fonts) { 1053 | done(fonts); 1054 | }; 1055 | var id = this.options.swfContainerId; 1056 | this.addFlashDivNode(); 1057 | var flashvars = { onReady: hiddenCallback}; 1058 | var flashparams = { allowScriptAccess: "always", menu: "false" }; 1059 | swfobject.embedSWF(this.options.swfPath, id, "1", "1", "9.0.0", false, flashvars, flashparams, {}); 1060 | }, 1061 | getWebglCanvas: function() { 1062 | var canvas = document.createElement("canvas"); 1063 | var gl = null; 1064 | try { 1065 | gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); 1066 | } catch(e) { /* squelch */ } 1067 | if (!gl) { gl = null; } 1068 | return gl; 1069 | }, 1070 | each: function (obj, iterator, context) { 1071 | if (obj === null) { 1072 | return; 1073 | } 1074 | if (this.nativeForEach && obj.forEach === this.nativeForEach) { 1075 | obj.forEach(iterator, context); 1076 | } else if (obj.length === +obj.length) { 1077 | for (var i = 0, l = obj.length; i < l; i++) { 1078 | if (iterator.call(context, obj[i], i, obj) === {}) { return; } 1079 | } 1080 | } else { 1081 | for (var key in obj) { 1082 | if (obj.hasOwnProperty(key)) { 1083 | if (iterator.call(context, obj[key], key, obj) === {}) { return; } 1084 | } 1085 | } 1086 | } 1087 | }, 1088 | 1089 | map: function(obj, iterator, context) { 1090 | var results = []; 1091 | // Not using strict equality so that this acts as a 1092 | // shortcut to checking for `null` and `undefined`. 1093 | if (obj == null) { return results; } 1094 | if (this.nativeMap && obj.map === this.nativeMap) { return obj.map(iterator, context); } 1095 | this.each(obj, function(value, index, list) { 1096 | results[results.length] = iterator.call(context, value, index, list); 1097 | }); 1098 | return results; 1099 | }, 1100 | 1101 | /// MurmurHash3 related functions 1102 | 1103 | // 1104 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 1105 | // added together as a 64bit int (as an array of two 32bit ints). 1106 | // 1107 | x64Add: function(m, n) { 1108 | m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]; 1109 | n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]; 1110 | var o = [0, 0, 0, 0]; 1111 | o[3] += m[3] + n[3]; 1112 | o[2] += o[3] >>> 16; 1113 | o[3] &= 0xffff; 1114 | o[2] += m[2] + n[2]; 1115 | o[1] += o[2] >>> 16; 1116 | o[2] &= 0xffff; 1117 | o[1] += m[1] + n[1]; 1118 | o[0] += o[1] >>> 16; 1119 | o[1] &= 0xffff; 1120 | o[0] += m[0] + n[0]; 1121 | o[0] &= 0xffff; 1122 | return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]; 1123 | }, 1124 | 1125 | // 1126 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 1127 | // multiplied together as a 64bit int (as an array of two 32bit ints). 1128 | // 1129 | x64Multiply: function(m, n) { 1130 | m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]; 1131 | n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]; 1132 | var o = [0, 0, 0, 0]; 1133 | o[3] += m[3] * n[3]; 1134 | o[2] += o[3] >>> 16; 1135 | o[3] &= 0xffff; 1136 | o[2] += m[2] * n[3]; 1137 | o[1] += o[2] >>> 16; 1138 | o[2] &= 0xffff; 1139 | o[2] += m[3] * n[2]; 1140 | o[1] += o[2] >>> 16; 1141 | o[2] &= 0xffff; 1142 | o[1] += m[1] * n[3]; 1143 | o[0] += o[1] >>> 16; 1144 | o[1] &= 0xffff; 1145 | o[1] += m[2] * n[2]; 1146 | o[0] += o[1] >>> 16; 1147 | o[1] &= 0xffff; 1148 | o[1] += m[3] * n[1]; 1149 | o[0] += o[1] >>> 16; 1150 | o[1] &= 0xffff; 1151 | o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0]); 1152 | o[0] &= 0xffff; 1153 | return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]; 1154 | }, 1155 | // 1156 | // Given a 64bit int (as an array of two 32bit ints) and an int 1157 | // representing a number of bit positions, returns the 64bit int (as an 1158 | // array of two 32bit ints) rotated left by that number of positions. 1159 | // 1160 | x64Rotl: function(m, n) { 1161 | n %= 64; 1162 | if (n === 32) { 1163 | return [m[1], m[0]]; 1164 | } 1165 | else if (n < 32) { 1166 | return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))]; 1167 | } 1168 | else { 1169 | n -= 32; 1170 | return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))]; 1171 | } 1172 | }, 1173 | // 1174 | // Given a 64bit int (as an array of two 32bit ints) and an int 1175 | // representing a number of bit positions, returns the 64bit int (as an 1176 | // array of two 32bit ints) shifted left by that number of positions. 1177 | // 1178 | x64LeftShift: function(m, n) { 1179 | n %= 64; 1180 | if (n === 0) { 1181 | return m; 1182 | } 1183 | else if (n < 32) { 1184 | return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n]; 1185 | } 1186 | else { 1187 | return [m[1] << (n - 32), 0]; 1188 | } 1189 | }, 1190 | // 1191 | // Given two 64bit ints (as an array of two 32bit ints) returns the two 1192 | // xored together as a 64bit int (as an array of two 32bit ints). 1193 | // 1194 | x64Xor: function(m, n) { 1195 | return [m[0] ^ n[0], m[1] ^ n[1]]; 1196 | }, 1197 | // 1198 | // Given a block, returns murmurHash3's final x64 mix of that block. 1199 | // (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the 1200 | // only place where we need to right shift 64bit ints.) 1201 | // 1202 | x64Fmix: function(h) { 1203 | h = this.x64Xor(h, [0, h[0] >>> 1]); 1204 | h = this.x64Multiply(h, [0xff51afd7, 0xed558ccd]); 1205 | h = this.x64Xor(h, [0, h[0] >>> 1]); 1206 | h = this.x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]); 1207 | h = this.x64Xor(h, [0, h[0] >>> 1]); 1208 | return h; 1209 | }, 1210 | 1211 | // 1212 | // Given a string and an optional seed as an int, returns a 128 bit 1213 | // hash using the x64 flavor of MurmurHash3, as an unsigned hex. 1214 | // 1215 | x64hash128: function (key, seed) { 1216 | key = key || ""; 1217 | seed = seed || 0; 1218 | var remainder = key.length % 16; 1219 | var bytes = key.length - remainder; 1220 | var h1 = [0, seed]; 1221 | var h2 = [0, seed]; 1222 | var k1 = [0, 0]; 1223 | var k2 = [0, 0]; 1224 | var c1 = [0x87c37b91, 0x114253d5]; 1225 | var c2 = [0x4cf5ad43, 0x2745937f]; 1226 | for (var i = 0; i < bytes; i = i + 16) { 1227 | k1 = [((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24), ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24)]; 1228 | k2 = [((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24), ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24)]; 1229 | k1 = this.x64Multiply(k1, c1); 1230 | k1 = this.x64Rotl(k1, 31); 1231 | k1 = this.x64Multiply(k1, c2); 1232 | h1 = this.x64Xor(h1, k1); 1233 | h1 = this.x64Rotl(h1, 27); 1234 | h1 = this.x64Add(h1, h2); 1235 | h1 = this.x64Add(this.x64Multiply(h1, [0, 5]), [0, 0x52dce729]); 1236 | k2 = this.x64Multiply(k2, c2); 1237 | k2 = this.x64Rotl(k2, 33); 1238 | k2 = this.x64Multiply(k2, c1); 1239 | h2 = this.x64Xor(h2, k2); 1240 | h2 = this.x64Rotl(h2, 31); 1241 | h2 = this.x64Add(h2, h1); 1242 | h2 = this.x64Add(this.x64Multiply(h2, [0, 5]), [0, 0x38495ab5]); 1243 | } 1244 | k1 = [0, 0]; 1245 | k2 = [0, 0]; 1246 | switch(remainder) { 1247 | case 15: 1248 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 14)], 48)); 1249 | case 14: 1250 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 13)], 40)); 1251 | case 13: 1252 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 12)], 32)); 1253 | case 12: 1254 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 11)], 24)); 1255 | case 11: 1256 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 10)], 16)); 1257 | case 10: 1258 | k2 = this.x64Xor(k2, this.x64LeftShift([0, key.charCodeAt(i + 9)], 8)); 1259 | case 9: 1260 | k2 = this.x64Xor(k2, [0, key.charCodeAt(i + 8)]); 1261 | k2 = this.x64Multiply(k2, c2); 1262 | k2 = this.x64Rotl(k2, 33); 1263 | k2 = this.x64Multiply(k2, c1); 1264 | h2 = this.x64Xor(h2, k2); 1265 | case 8: 1266 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 7)], 56)); 1267 | case 7: 1268 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 6)], 48)); 1269 | case 6: 1270 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 5)], 40)); 1271 | case 5: 1272 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 4)], 32)); 1273 | case 4: 1274 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 3)], 24)); 1275 | case 3: 1276 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 2)], 16)); 1277 | case 2: 1278 | k1 = this.x64Xor(k1, this.x64LeftShift([0, key.charCodeAt(i + 1)], 8)); 1279 | case 1: 1280 | k1 = this.x64Xor(k1, [0, key.charCodeAt(i)]); 1281 | k1 = this.x64Multiply(k1, c1); 1282 | k1 = this.x64Rotl(k1, 31); 1283 | k1 = this.x64Multiply(k1, c2); 1284 | h1 = this.x64Xor(h1, k1); 1285 | } 1286 | h1 = this.x64Xor(h1, [0, key.length]); 1287 | h2 = this.x64Xor(h2, [0, key.length]); 1288 | h1 = this.x64Add(h1, h2); 1289 | h2 = this.x64Add(h2, h1); 1290 | h1 = this.x64Fmix(h1); 1291 | h2 = this.x64Fmix(h2); 1292 | h1 = this.x64Add(h1, h2); 1293 | h2 = this.x64Add(h2, h1); 1294 | return ("00000000" + (h1[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (h1[1] >>> 0).toString(16)).slice(-8) + ("00000000" + (h2[0] >>> 0).toString(16)).slice(-8) + ("00000000" + (h2[1] >>> 0).toString(16)).slice(-8); 1295 | } 1296 | }; 1297 | Fingerprint2.VERSION = "1.4.1"; 1298 | return Fingerprint2; 1299 | }); 1300 | -------------------------------------------------------------------------------- /src/local.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from winreg import * 3 | import logging as log 4 | import subprocess 5 | import asyncio 6 | 7 | from galaxy.proc_tools import pids 8 | 9 | from consts import WINDOWS_UNINSTALL_KEY, LOG_SENSITIVE_DATA, CONFIG_OPTIONS 10 | from game_cache import games_cache 11 | 12 | 13 | def check_if_process_exists(pid): 14 | if not pid: 15 | return False 16 | if int(pid) in pids(): 17 | return True 18 | return False 19 | 20 | 21 | class LocalClient: 22 | def __init__(self): 23 | self.root_reg = ConnectRegistry(None, HKEY_LOCAL_MACHINE) 24 | self.installer_location = None 25 | self.get_local_launcher_path() 26 | 27 | def get_local_launcher_path(self): 28 | try: 29 | if CONFIG_OPTIONS['rockstar_launcher_path_override']: 30 | self.installer_location = CONFIG_OPTIONS['rockstar_launcher_path_override'] 31 | if LOG_SENSITIVE_DATA: 32 | log.debug("ROCKSTAR_INSTALLER_PATH: " + self.installer_location) 33 | else: 34 | log.debug("ROCKSTAR_INSTALLER_PATH: ***") 35 | else: 36 | # The uninstall key for the launcher is called Rockstar Games Launcher. 37 | key = OpenKey(self.root_reg, WINDOWS_UNINSTALL_KEY + "Rockstar Games Launcher") 38 | dir, type = QueryValueEx(key, "InstallLocation") 39 | self.installer_location = dir[:-1] + "\\Launcher.exe\"" 40 | if LOG_SENSITIVE_DATA: 41 | log.debug("ROCKSTAR_INSTALLER_PATH: " + self.installer_location) 42 | else: 43 | log.debug("ROCKSTAR_INSTALLER_PATH: ***") 44 | except WindowsError: 45 | self.installer_location = None 46 | return self.installer_location 47 | 48 | async def kill_launcher(self): 49 | # The Launcher exits without displaying an error message if LauncherPatcher.exe is killed before Launcher.exe. 50 | subprocess.Popen("taskkill /im SocialClubHelper.exe") 51 | 52 | def get_path_to_game(self, title_id): 53 | try: 54 | key = OpenKey(self.root_reg, WINDOWS_UNINSTALL_KEY + games_cache[title_id]['guid']) 55 | dir, type = QueryValueEx(key, "InstallLocation") 56 | return dir 57 | except WindowsError: 58 | # log.debug("ROCKSTAR_GAME_NOT_INSTALLED: The game with ID " + title_id + " is not installed.") - Reduce 59 | # Console Spam (Enable this if you need to.) 60 | return None 61 | 62 | async def get_game_size_in_bytes(self, title_id) -> Optional[int]: 63 | path = self.get_path_to_game(title_id) 64 | # We will add quotes if they are not present already. 65 | if path[:1] != '"': 66 | path = f'"{path}"' 67 | find_game_size = subprocess.Popen( 68 | f'chcp 65001 & dir {path} /a /s /-c', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 69 | output, err = find_game_size.communicate() 70 | 71 | # The file size will be listed in the second-to-last line of the output. 72 | line_list = output.decode().splitlines() 73 | game_size_line = line_list[len(line_list) - 2] 74 | size = None 75 | if "bytes" in game_size_line: 76 | size = int([str(s) for s in game_size_line.split() if s.isdigit()][1]) 77 | if size: 78 | log.debug(f"ROCKSTAR_GAME_SIZE: The size of {title_id} is {size} bytes.") 79 | else: 80 | log.warning(f"ROCKSTAR_GAME_SIZE_FAILURE: The size of {title_id} could not be determined!") 81 | return size 82 | 83 | async def game_pid_from_tasklist(self, title_id) -> str: 84 | pid = None 85 | tracked_key = "trackEXE" if "trackEXE" in games_cache[title_id] else "launchEXE" 86 | # When reading output from the Windows Command Prompt, it is a good idea to first set the code page to one that 87 | # is used by the application. In this case, "chcp 65001" is sent to change the code page to Unicode, which is 88 | # what Python uses. Changes to the code page in this manner are temporary, so it should be sent along with the 89 | # desired command in one call to subprocess.Popen() (such as by using "&"). The "shell" parameter must also be 90 | # set to "True." 91 | find_actual_pid = subprocess.Popen( 92 | f'chcp 65001 & tasklist /FI "IMAGENAME eq {games_cache[title_id][tracked_key]} " /FI "STATUS eq running" ' 93 | f'/FO LIST', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 94 | output, err = find_actual_pid.communicate() 95 | 96 | for line in output.decode().splitlines(): 97 | if "PID" in line: 98 | pid = [str(s) for s in line.split() if s.isdigit()][0] 99 | break 100 | return pid 101 | 102 | async def launch_game_from_title_id(self, title_id): 103 | path = self.get_path_to_game(title_id) 104 | path = path.replace('"', '').replace(',0', '') 105 | if not path: 106 | log.error(f"ROCKSTAR_LAUNCH_FAILURE: The game {title_id} could not be launched.") 107 | return 108 | game_path = f"{path}\\{games_cache[title_id]['launchEXE']}" 109 | log.debug(f"ROCKSTAR_LAUNCH_REQUEST: Requesting to launch {game_path}...") 110 | subprocess.Popen([game_path, "-launchTitleInFolder", path, "@commandline.txt"], stdout=subprocess.DEVNULL, 111 | stderr=subprocess.DEVNULL, shell=False) 112 | launcher_pid = None 113 | retries = 120 114 | while not launcher_pid: 115 | await asyncio.sleep(1) 116 | launcher_pid = await self.game_pid_from_tasklist("launcher") 117 | retries -= 1 118 | if retries == 0: 119 | log.debug("ROCKSTAR_LAUNCHER_PID_FAILURE: The Rockstar Games Launcher took too long to launch!") 120 | return None 121 | log.debug(f"ROCKSTAR_LAUNCHER_PID: {launcher_pid}") 122 | 123 | # The Rockstar Games Launcher can be painfully slow to boot up games, loop will be just fine 124 | retries = 30 125 | while True: 126 | await asyncio.sleep(1) 127 | pid = await self.game_pid_from_tasklist(title_id) 128 | if pid: 129 | return pid 130 | retries -= 1 131 | if retries == 0: 132 | # If it has been this long and the game still has not launched, then it might be downloading an update. 133 | # We should refresh the retries counter if the Rockstar Games Launcher is still running; otherwise, we 134 | # return None. 135 | if await self.game_pid_from_tasklist("launcher"): 136 | log.debug(f"ROCKSTAR_LAUNCH_WAITING: The game {title_id} has not launched yet, but the Rockstar " 137 | f"Games Launcher is still running. Restarting the loop...") 138 | retries += 30 139 | else: 140 | return None 141 | 142 | def install_game_from_title_id(self, title_id): 143 | if not self.installer_location: 144 | return 145 | subprocess.call(self.installer_location + " -enableFullMode -install=" + title_id, stdout=subprocess.DEVNULL, 146 | stderr=subprocess.DEVNULL, shell=False) 147 | 148 | def uninstall_game_from_title_id(self, title_id): 149 | if not self.installer_location: 150 | return 151 | subprocess.call(self.installer_location + " -enableFullMode -uninstall=" + title_id, stdout=subprocess.DEVNULL, 152 | stderr=subprocess.DEVNULL, shell=False) 153 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rockstar Integration Plugin for Galaxy 2.0", 3 | "platform": "rockstar", 4 | "guid": "774732b5-69c4-405c-b6c9-92cd55740cfe", 5 | "version": "0.5.13", 6 | "description": "This plugin will allow for (basic) integration with the Rockstar Games Launcher.", 7 | "author": "Tylerbrawl", 8 | "email": "tylerbrawl@gmail.com", 9 | "url": "https://github.com/tylerbrawl/Galaxy-Plugin-Rockstar", 10 | "update_url": "https://raw.githubusercontent.com/tylerbrawl/Galaxy-Plugin-Rockstar/master/current_version.json", 11 | "script": "plugin.py" 12 | } 13 | -------------------------------------------------------------------------------- /src/plugin.py: -------------------------------------------------------------------------------- 1 | from galaxy.api.plugin import Plugin, create_and_run_plugin 2 | from galaxy.api.consts import Platform, PresenceState 3 | from galaxy.api.types import NextStep, Authentication, Game, LocalGame, LocalGameState, UserInfo, Achievement, \ 4 | GameTime, UserPresence 5 | from galaxy.api.errors import InvalidCredentials, AuthenticationRequired, NetworkError, UnknownError 6 | 7 | from file_read_backwards import FileReadBackwards 8 | from time import time 9 | from typing import List, Any, Optional 10 | import asyncio 11 | import dataclasses 12 | import datetime 13 | import logging as log 14 | import os 15 | import pickle 16 | import re 17 | import sys 18 | import webbrowser 19 | 20 | from consts import AUTH_PARAMS, NoGamesInLogException, NoLogFoundException, IS_WINDOWS, LOG_SENSITIVE_DATA, \ 21 | ARE_ACHIEVEMENTS_IMPLEMENTED, CONFIG_OPTIONS, get_unix_epoch_time_from_date 22 | from game_cache import games_cache, get_game_title_id_from_ros_title_id, get_achievement_id_from_ros_title_id, \ 23 | ignore_game_title_ids_list 24 | from http_client import BackendClient 25 | from version import __version__ 26 | 27 | if IS_WINDOWS: 28 | import ctypes.wintypes 29 | from local import LocalClient, check_if_process_exists 30 | 31 | 32 | @dataclasses.dataclass 33 | class RunningGameInfo(object): 34 | _pid = None 35 | _start_time = None 36 | 37 | def set_info(self, pid): 38 | self._pid = pid 39 | self._start_time = datetime.datetime.now().timestamp() 40 | 41 | def get_pid(self): 42 | return self._pid 43 | 44 | def clear_pid(self): 45 | self._pid = None 46 | 47 | def get_start_time(self): 48 | return self._start_time 49 | 50 | def update_start_time(self): 51 | self._start_time = datetime.datetime.now().timestamp() 52 | 53 | 54 | class RockstarPlugin(Plugin): 55 | def __init__(self, reader, writer, token): 56 | super().__init__(Platform.Rockstar, __version__, reader, writer, token) 57 | self.games_cache = games_cache 58 | self._http_client = BackendClient(self.store_credentials) 59 | self._local_client = None 60 | self.total_games_cache = self.create_total_games_cache() 61 | self.friends_cache = [] 62 | self.presence_cache = {} 63 | self.owned_games_cache = [] 64 | self.last_online_game_check = time() - 300 65 | self.local_games_cache = {} 66 | self.game_time_cache = {} 67 | self.running_games_info_list = {} 68 | self.game_is_loading = True 69 | self.checking_for_new_games = False 70 | self.updating_game_statuses = False 71 | self.buffer = None 72 | if IS_WINDOWS: 73 | self._local_client = LocalClient() 74 | self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) 75 | ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer) 76 | self.documents_location = self.buffer.value 77 | 78 | def is_authenticated(self): 79 | return self._http_client.is_authenticated() 80 | 81 | @staticmethod 82 | def loads_js(file): 83 | with open(os.path.abspath(os.path.join(__file__, '..', 'js', file)), 'r') as f: 84 | return f.read() 85 | 86 | def handshake_complete(self): 87 | game_time_cache_in_persistent_cache = False 88 | for key, value in self.persistent_cache.items(): 89 | # if "achievements_" in key: 90 | # log.debug("ROCKSTAR_CACHE_IMPORT: Importing " + key + " from persistent cache...") 91 | # self._all_achievements_cache[key] = pickle.loads(bytes.fromhex(value)) 92 | if key == "game_time_cache": 93 | self.game_time_cache = pickle.loads(bytes.fromhex(value)) 94 | game_time_cache_in_persistent_cache = True 95 | if IS_WINDOWS and not game_time_cache_in_persistent_cache: 96 | # The game time cache was not found in the persistent cache, so the plugin will instead attempt to get the 97 | # cache from the user's file stored on their disk. 98 | file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt") 99 | try: 100 | file = open(file_location, "r") 101 | for line in file.readlines(): 102 | if line[:1] != "#": 103 | log.debug("ROCKSTAR_LOCAL_GAME_TIME_FROM_FILE: " + str(pickle.loads(bytes.fromhex(line)))) 104 | self.game_time_cache = pickle.loads(bytes.fromhex(line)) 105 | break 106 | if not self.game_time_cache: 107 | log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the " 108 | "persistent cache nor the designated local file. Let's hope that the user is new...") 109 | except FileNotFoundError: 110 | log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the persistent" 111 | " cache nor the designated local file. Let's hope that the user is new...") 112 | 113 | async def authenticate(self, stored_credentials=None): 114 | try: 115 | self._http_client.create_session(stored_credentials) 116 | except KeyError: 117 | log.error("ROCKSTAR_OLD_LOG_IN: The user has likely previously logged into the plugin with a version less " 118 | "than v0.3, and their credentials might be corrupted. Forcing a log-out...") 119 | raise InvalidCredentials() 120 | if not stored_credentials: 121 | # We will create the fingerprint JavaScript dictionary here. 122 | fingerprint_js = { 123 | r'https://www.rockstargames.com/': [ 124 | self.loads_js("fingerprint2.js"), 125 | self.loads_js("HashGen.js"), 126 | self.loads_js("GenerateFingerprint.js") 127 | ] 128 | } 129 | return NextStep("web_session", AUTH_PARAMS, js=fingerprint_js) 130 | try: 131 | log.info("INFO: The credentials were successfully obtained.") 132 | if LOG_SENSITIVE_DATA: 133 | cookies = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar'])) 134 | log.debug("ROCKSTAR_COOKIES_FROM_HEX: " + str(cookies)) # sensitive data hidden by default 135 | # for cookie in cookies: 136 | # self._http_client.update_cookies({cookie.name: cookie.value}) 137 | self._http_client.set_current_auth_token(stored_credentials['current_auth_token']) 138 | self._http_client.set_current_sc_token(stored_credentials['current_sc_token']) 139 | self._http_client.set_refresh_token_absolute( 140 | pickle.loads(bytes.fromhex(stored_credentials['refresh_token']))) 141 | self._http_client.set_fingerprint(stored_credentials['fingerprint']) 142 | log.info("INFO: The stored credentials were successfully parsed. Beginning authentication...") 143 | user = await self._http_client.authenticate() 144 | return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) 145 | except (NetworkError, UnknownError): 146 | raise 147 | except Exception as e: 148 | log.warning("ROCKSTAR_AUTH_WARNING: The exception " + repr(e) + " was thrown, presumably because of " 149 | "outdated credentials. Attempting to get new credentials...") 150 | self._http_client.set_auth_lost_callback(True) 151 | try: 152 | user = await self._http_client.authenticate() 153 | return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) 154 | except Exception as e: 155 | log.error("ROCKSTAR_AUTH_FAILURE: Something went terribly wrong with the re-authentication. " + repr(e)) 156 | log.exception("ROCKSTAR_STACK_TRACE") 157 | raise InvalidCredentials 158 | 159 | async def pass_login_credentials(self, step, credentials, cookies): 160 | if LOG_SENSITIVE_DATA: 161 | log.debug("ROCKSTAR_COOKIE_LIST: " + str(cookies)) 162 | for cookie in cookies: 163 | if cookie['name'].find("TSc") != -1: 164 | self._http_client.set_current_auth_token(cookie['value']) 165 | if cookie['name'] == "BearerToken": 166 | self._http_client.set_current_sc_token(cookie['value']) 167 | if cookie['name'] == "RMT": 168 | if cookie['value'] != "": 169 | if LOG_SENSITIVE_DATA: 170 | log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: " + cookie['value']) 171 | else: 172 | log.debug("ROCKSRAR_REMEMBER_ME: Got RMT: ***") # Only asterisks are shown here for consistency 173 | # with the output when the user has a blank RMT from multi-factor authentication. 174 | self._http_client.set_refresh_token(cookie['value']) 175 | else: 176 | if LOG_SENSITIVE_DATA: 177 | log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: [Blank!]") 178 | else: 179 | log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: ***") 180 | self._http_client.set_refresh_token('') 181 | if cookie['name'] == "fingerprint": 182 | if LOG_SENSITIVE_DATA: 183 | log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: " + cookie['value'].replace("$", ";")) 184 | else: 185 | log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: ***") 186 | self._http_client.set_fingerprint(cookie['value'].replace("$", ";")) 187 | # We will not add the fingerprint as a cookie to the session; it will instead be stored with the user's 188 | # credentials. 189 | continue 190 | if re.search("^rsso", cookie['name']): 191 | if LOG_SENSITIVE_DATA: 192 | log.debug("ROCKSTAR_RSSO: Got " + cookie['name'] + ": " + cookie['value']) 193 | else: 194 | log.debug(f"ROCKSTAR_RSSO: Got rsso-***: {cookie['value'][:5]}***{cookie['value'][-3:]}") 195 | cookie_object = { 196 | "name": cookie['name'], 197 | "value": cookie['value'], 198 | "domain": cookie['domain'], 199 | "path": cookie['path'] 200 | } 201 | self._http_client.update_cookie(cookie_object) 202 | try: 203 | user = await self._http_client.authenticate() 204 | except Exception as e: 205 | log.error(repr(e)) 206 | raise InvalidCredentials 207 | return Authentication(user_id=user["rockstar_id"], user_name=user["display_name"]) 208 | 209 | async def shutdown(self): 210 | # At this point, we can write to a file to keep a cached copy of the user's played time. 211 | # This will prevent the play time from being erased if the user loses authentication. 212 | if IS_WINDOWS and self.game_time_cache: 213 | # For the sake of convenience, we will store this file in the user's Documents folder. 214 | # Obviously, this feature is only compatible with (and relevant for) Windows machines. 215 | file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt") 216 | file = open(file_location, "w+") 217 | file.write("# This file contains a cached copy of the user's play time for the Rockstar plugin for GOG " 218 | "Galaxy 2.0.\n") 219 | file.write("# DO NOT EDIT THIS FILE IN ANY WAY, LEST THE CACHE GETS CORRUPTED AND YOUR PLAY TIME IS LOST!\n" 220 | ) 221 | file.write(pickle.dumps(self.game_time_cache).hex()) 222 | file.close() 223 | await self._http_client.close() 224 | await super().shutdown() 225 | 226 | def create_total_games_cache(self): 227 | cache = [] 228 | for title_id in list(games_cache): 229 | cache.append(self.create_game_from_title_id(title_id)) 230 | return cache 231 | 232 | if ARE_ACHIEVEMENTS_IMPLEMENTED: 233 | async def get_unlocked_achievements(self, game_id, context): 234 | # The Social Club API has an authentication endpoint located at https://scapi.rockstargames.com/ 235 | # achievements/awardedAchievements?title=[game-id]&platform=pc&rockstarId=[rockstar-ID], which returns a 236 | # list of the user's unlocked achievements for the specified game. It uses the Social Club standard for 237 | # authentication (a request header named Authorization containing "Bearer [Bearer-Token]"). 238 | 239 | title_id = get_game_title_id_from_ros_title_id(game_id) 240 | if games_cache[title_id]["achievementId"] is None or \ 241 | (games_cache[title_id]["isPreOrder"]): 242 | return [] 243 | log.debug("ROCKSTAR_ACHIEVEMENT_CHECK: Beginning achievements check for " + 244 | title_id + " (Achievement ID: " + get_achievement_id_from_ros_title_id(game_id) + ")...") 245 | # Now, we can begin getting the user's achievements for the specified game. 246 | achievement_id = get_achievement_id_from_ros_title_id(game_id) 247 | url = (f"https://scapi.rockstargames.com/achievements/awardedAchievements?title={achievement_id}" 248 | f"&platform=pc&rockstarId={self._http_client.get_rockstar_id()}") 249 | unlocked_achievements = await self._http_client.get_json_from_request_strict(url) 250 | achievements_dict = unlocked_achievements["awardedAchievements"] 251 | achievements_list = [] 252 | for key, value in achievements_dict.items(): 253 | # What if an achievement is added to the Social Club after the cache was already made? In this event, we 254 | # need to refresh the cache. 255 | achievement_num = key 256 | unlock_time = await get_unix_epoch_time_from_date(value["dateAchieved"]) 257 | achievements_list.append(Achievement(unlock_time, achievement_id=achievement_num)) 258 | return achievements_list 259 | 260 | async def get_friends(self) -> List[UserInfo]: 261 | # The Social Club website returns a list of the current user's friends through the url 262 | # https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&pageIndex=0&pageSize=30. 263 | # The nickname URL parameter is left blank because the website instead uses the bearer token to get the correct 264 | # information. The last two parameters are of great importance, however. The parameter pageSize determines the 265 | # number of friends given on that page's list, while pageIndex keeps track of the page that the information is 266 | # on. The maximum number for pageSize is 30, so that is what we will use to cut down the number of HTTP 267 | # requests. 268 | 269 | # We first need to get the number of friends. 270 | url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" 271 | "pageIndex=0&pageSize=30") 272 | try: 273 | current_page = await self._http_client.get_json_from_request_strict(url) 274 | except TimeoutError: 275 | log.warning("ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index 0 timed out. " 276 | "Returning the cached list...") 277 | return self.friends_cache 278 | if LOG_SENSITIVE_DATA: 279 | log.debug("ROCKSTAR_FRIENDS_REQUEST: " + str(current_page)) 280 | else: 281 | log.debug("ROCKSTAR_FRIENDS_REQUEST: ***") 282 | num_friends = current_page['rockstarAccountList']['totalFriends'] 283 | num_pages_required = num_friends / 30 if num_friends % 30 != 0 else (num_friends / 30) - 1 284 | 285 | # Now, we need to get the information about the friends. 286 | friends_list = current_page['rockstarAccountList']['rockstarAccounts'] 287 | return_list = await self._parse_friends(friends_list) 288 | 289 | # The first page is finished, but now we need to work on any remaining pages. 290 | if num_pages_required > 0: 291 | for i in range(1, int(num_pages_required + 1)): 292 | try: 293 | url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" 294 | "pageIndex=" + str(i) + "&pageSize=30") 295 | for friend in await self._get_friends(url): 296 | return_list.append(friend) 297 | except TimeoutError: 298 | log.warning(f"ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index {i} " 299 | f"timed out. Returning the cached list...") 300 | return self.friends_cache 301 | return return_list 302 | 303 | async def _get_friends(self, url: str) -> List[UserInfo]: 304 | try: 305 | current_page = await self._http_client.get_json_from_request_strict(url) 306 | except TimeoutError: 307 | raise 308 | friends_list = current_page['rockstarAccountList']['rockstarAccounts'] 309 | return await self._parse_friends(friends_list) 310 | 311 | async def _parse_friends(self, friends_list: dict) -> List[UserInfo]: 312 | return_list = [] 313 | for i in range(0, len(friends_list)): 314 | avatar_uri = f"https://a.rsg.sc/n/{friends_list[i]['displayName'].lower()}/l" 315 | profile_uri = f"https://socialclub.rockstargames.com/member/{friends_list[i]['displayName']}/" 316 | friend = UserInfo(user_id=str(friends_list[i]['rockstarId']), 317 | user_name=friends_list[i]['displayName'], 318 | avatar_url=avatar_uri, 319 | profile_url=profile_uri) 320 | return_list.append(friend) 321 | for cached_friend in self.friends_cache: 322 | if cached_friend.user_id == friend.user_id: 323 | break 324 | else: # An else-statement occurs after a for-statement if the latter finishes WITHOUT breaking. 325 | self.friends_cache.append(friend) 326 | if LOG_SENSITIVE_DATA: 327 | log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name + " (Rockstar ID: " + 328 | str(friend.user_id) + ")") 329 | else: 330 | log.debug(f"ROCKSTAR_FRIEND: Found {friend.user_name[:1]}*** (Rockstar ID: ***)") 331 | return return_list 332 | 333 | async def get_owned_games_online(self): 334 | # Get the list of games_played from https://socialclub.rockstargames.com/ajax/getGoogleTagManagerSetupData. 335 | owned_title_ids = [] 336 | online_check_success = True 337 | self.last_online_game_check = time() 338 | try: 339 | played_games = await self._http_client.get_played_games() 340 | for game in played_games: 341 | owned_title_ids.append(game) 342 | log.debug("ROCKSTAR_ONLINE_GAME: Found played game " + game + "!") 343 | except Exception as e: 344 | log.error("ROCKSTAR_PLAYED_GAMES_ERROR: The exception " + repr(e) + " was thrown when attempting to get" 345 | " the user's played games online. Falling back to log file check...") 346 | online_check_success = False 347 | return owned_title_ids, online_check_success 348 | 349 | async def get_owned_games(self, owned_title_ids=None, online_check_success=False): 350 | # Here is the actual implementation of getting the user's owned games: 351 | # -Get the list of games_played from rockstargames.com/auth/get-user.json. 352 | # -If possible, use the launcher log to confirm which games are actual launcher games and which are 353 | # Steam/Retail games. 354 | # -If it is not possible to use the launcher log, then just use the list provided by the website. 355 | if owned_title_ids is None: 356 | owned_title_ids = [] 357 | if not self.is_authenticated(): 358 | raise AuthenticationRequired() 359 | 360 | # The log is in the Documents folder. 361 | current_log_count = 0 362 | log_file = None 363 | log_file_append = "" 364 | # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are 365 | # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games, 366 | # we need to check the other log files, if possible. 367 | while current_log_count < 10: 368 | # We need to prevent the log file check for Mac users. 369 | if not IS_WINDOWS: 370 | break 371 | try: 372 | if current_log_count != 0: 373 | log_file_append = ".0" + str(current_log_count) 374 | log_file = os.path.join(self.documents_location, "Rockstar Games\\Launcher\\launcher" + log_file_append 375 | + ".log") 376 | if LOG_SENSITIVE_DATA: 377 | log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " + log_file + "...") 378 | else: 379 | log.debug("ROCKSTAR_LOG_LOCATION: Checking the file ***...") # The path to the Launcher log file 380 | # likely contains the user's PC profile name (C:\Users\[Name]\Documents...). 381 | owned_title_ids = await self.parse_log_file(log_file, owned_title_ids, online_check_success) 382 | break 383 | except NoGamesInLogException: 384 | log.warning("ROCKSTAR_LOG_WARNING: There are no owned games listed in " + str(log_file) + ". Moving to " 385 | "the next log file...") 386 | current_log_count += 1 387 | except NoLogFoundException: 388 | log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " 389 | "from. Assuming that the online list is correct...") 390 | break 391 | except Exception: 392 | # This occurs after ROCKSTAR_LOG_ERROR. 393 | break 394 | if current_log_count == 10: 395 | log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " 396 | "from. Assuming that the online list is correct...") 397 | 398 | for title_id in owned_title_ids: 399 | game = self.create_game_from_title_id(title_id) 400 | if game not in self.owned_games_cache: 401 | log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id + " to owned games cache...") 402 | self.owned_games_cache.append(game) 403 | 404 | return self.owned_games_cache 405 | 406 | if IS_WINDOWS: 407 | async def get_local_size(self, game_id: str, context: Any) -> Optional[int]: 408 | title_id = get_game_title_id_from_ros_title_id(game_id) 409 | return await self._local_client.get_game_size_in_bytes(title_id) 410 | 411 | @staticmethod 412 | async def parse_log_file(log_file, owned_title_ids, online_check_success): 413 | owned_title_ids_ = owned_title_ids 414 | checked_games_count = 0 415 | # We need to subtract 1 to account for the Launcher. 416 | total_games_count = len(games_cache) + len(ignore_game_title_ids_list) - 1 417 | 418 | if os.path.exists(log_file): 419 | with FileReadBackwards(log_file, encoding="utf-8") as frb: 420 | while checked_games_count < total_games_count: 421 | try: 422 | line = frb.readline() 423 | except UnicodeDecodeError: 424 | log.warning("ROCKSTAR_LOG_UNICODE_WARNING: An invalid Unicode character was found in the line " 425 | + line + ". Continuing to next line...") 426 | continue 427 | except Exception as e: 428 | log.error("ROCKSTAR_LOG_ERROR: Reading " + line + " from the log file resulted in the " 429 | "exception " + repr(e) + " being thrown. Using the online list... (Please report " 430 | "this issue on the plugin's GitHub page!)") 431 | raise 432 | if not line: 433 | log.error("ROCKSTAR_LOG_FINISHED_ERROR: The entire log file was read, but all of the games " 434 | "could not be accounted for. Proceeding to import the games that have been " 435 | "confirmed...") 436 | raise NoGamesInLogException() 437 | # We need to do two main things with the log file: 438 | # 1. If a game is present in owned_title_ids but not owned according to the log file, then it is 439 | # assumed to be a non-Launcher game, and is removed from the list. 440 | # 2. If a game is owned according to the log file but is not already present in owned_title_ids, 441 | # then it is assumed that the user has purchased the game on the Launcher, but has not yet played 442 | # it. In this case, the game will be added to owned_title_ids. 443 | if ("launcher" not in line) and ("on branch " in line): # Found a game! 444 | # Each log line for a title branch report describes the title id of the game starting at 445 | # character 65. From there, we search for the first occurrence of a colon starting from where 446 | # the title_id begins (character 65). 447 | end_index = line[65:].index(':') + 65 448 | title_id = line[65:end_index].strip() 449 | 450 | # Ignore title IDs which are present in the ignore_game_title_ids_list. 451 | if title_id in ignore_game_title_ids_list: 452 | log.debug("ROCKSTAR_IGNORE_GAME: Ignoring owned game " + title_id + "...") 453 | else: 454 | log.debug("ROCKSTAR_LOG_GAME: The game with title ID " + title_id + " is owned!") 455 | if title_id not in owned_title_ids_: 456 | if online_check_success is True: 457 | # Case 2: The game is owned, but has not been played. 458 | log.warning("ROCKSTAR_UNPLAYED_GAME: The game with title ID " + title_id + 459 | " is owned, but it has never been played!") 460 | owned_title_ids_.append(title_id) 461 | checked_games_count += 1 462 | 463 | elif "no branches!" in line: 464 | end_index = line[65:].index(':') + 65 465 | title_id = line[65:end_index].strip() 466 | 467 | # Ignore title IDs which are present in the ignore_game_title_ids_list. 468 | if title_id in ignore_game_title_ids_list: 469 | log.debug("ROCKSTAR_IGNORE_GAME: Ignoring owned game " + title_id + "...") 470 | else: 471 | if title_id in owned_title_ids_: 472 | # Case 1: The game is not actually owned on the launcher. 473 | log.warning("ROCKSTAR_FAKE_GAME: The game with title ID " + title_id + " is not owned on " 474 | "the Rockstar Games Launcher!") 475 | owned_title_ids_.remove(title_id) 476 | checked_games_count += 1 477 | if checked_games_count == total_games_count: 478 | break 479 | return owned_title_ids_ 480 | else: 481 | raise NoLogFoundException() 482 | 483 | async def get_game_time(self, game_id, context): 484 | # Although the Rockstar Games Launcher does track the played time for each game, there is currently no known 485 | # method for accessing this information. As such, game time will be recorded when games are launched through the 486 | # Galaxy 2.0 client. 487 | 488 | title_id = get_game_title_id_from_ros_title_id(game_id) 489 | if title_id in self.running_games_info_list: 490 | # The game is running (or has been running). 491 | start_time = self.running_games_info_list[title_id].get_start_time() 492 | self.running_games_info_list[title_id].update_start_time() 493 | current_time = datetime.datetime.now().timestamp() 494 | minutes_passed = (current_time - start_time) / 60 495 | if not self.running_games_info_list[title_id].get_pid(): 496 | # The PID has been set to None, which means that the game has exited (see self.check_game_status). Now 497 | # that the start time is recorded, the game can be safely removed from the list of running games. 498 | del self.running_games_info_list[title_id] 499 | if self.game_time_cache[title_id]['time_played']: 500 | # The game has been played before, so the time will need to be added to the existing cached time. 501 | total_time_played = self.game_time_cache[title_id]['time_played'] + minutes_passed 502 | self.game_time_cache[title_id]['time_played'] = total_time_played 503 | self.game_time_cache[title_id]['last_played'] = current_time 504 | return GameTime(game_id=game_id, time_played=int(total_time_played), last_played_time=int(current_time)) 505 | else: 506 | # The game has not been played before, so a new entry in the game_time_cache dictionary must be made. 507 | self.game_time_cache[title_id] = { 508 | 'time_played': minutes_passed, 509 | 'last_played': current_time 510 | } 511 | return GameTime(game_id=game_id, time_played=int(minutes_passed), last_played_time=int(current_time)) 512 | else: 513 | # The game is no longer running (and there is no relevant entry in self.running_games_info_list). 514 | if title_id not in self.game_time_cache: 515 | self.game_time_cache[title_id] = { 516 | 'time_played': None, 517 | 'last_played': None 518 | } 519 | return GameTime(game_id=game_id, time_played=self.game_time_cache[title_id]['time_played'], 520 | last_played_time=self.game_time_cache[title_id]['last_played']) 521 | 522 | def game_times_import_complete(self): 523 | log.debug("ROCKSTAR_GAME_TIME: Pushing the cache of played game times to the persistent cache...") 524 | self.persistent_cache['game_time_cache'] = pickle.dumps(self.game_time_cache).hex() 525 | self.push_cache() 526 | 527 | def get_friend_user_name_from_user_id(self, user_id): 528 | for friend in self.friends_cache: 529 | if friend.user_id == user_id: 530 | return friend.user_name 531 | return None 532 | 533 | async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any: 534 | if CONFIG_OPTIONS['user_presence_mode'] == 2 or CONFIG_OPTIONS['user_presence_mode'] == 3: 535 | game = "gtav" if CONFIG_OPTIONS['user_presence_mode'] == 2 else "rdr2" 536 | return await self._http_client.get_json_from_request_strict("https://scapi.rockstargames.com/friends/" 537 | f"getFriendsWhoPlay?title={game}&platform=pc") 538 | return None 539 | 540 | async def get_user_presence(self, user_id, context): 541 | # For user presence settings 2 and 3, we need to verify that the specified user owns the game to get their 542 | # stats. 543 | 544 | friend_name = self.get_friend_user_name_from_user_id(user_id) 545 | if LOG_SENSITIVE_DATA: 546 | log.debug(f"ROCKSTAR_PRESENCE_START: Getting user presence for {friend_name} (Rockstar ID: {user_id})...") 547 | if context: 548 | for player in context['onlineFriends']: 549 | if player['userId'] == user_id: 550 | # This user owns the specified game, so we can return this information. 551 | break 552 | else: 553 | # The user does not own the specified game, so we need to return their last played game. 554 | return await self._http_client.get_last_played_game(friend_name) 555 | if CONFIG_OPTIONS['user_presence_mode'] == 0: 556 | self.presence_cache[user_id] = UserPresence(presence_state=PresenceState.Unknown) 557 | # 0 - Disable User Presence 558 | else: 559 | switch = { 560 | 1: self._http_client.get_last_played_game(friend_name), 561 | # 1 - Get Last Played Game 562 | 2: self._http_client.get_gta_online_stats(user_id, friend_name), 563 | # 2 - Get GTA Online Character Stats 564 | 3: self._http_client.get_rdo_stats(user_id, friend_name) 565 | # 3 - Get Red Dead Online Character Stats 566 | } 567 | self.presence_cache[user_id] = await asyncio.create_task(switch[CONFIG_OPTIONS['user_presence_mode']]) 568 | return self.presence_cache[user_id] 569 | 570 | async def open_rockstar_browser(self): 571 | # This method allows the user to install the Rockstar Games Launcher, if it is not already installed. 572 | url = "https://www.rockstargames.com/downloads" 573 | 574 | log.info(f"Opening Rockstar website {url}") 575 | webbrowser.open(url) 576 | 577 | def check_game_status(self, title_id): 578 | state = LocalGameState.None_ 579 | 580 | game_installed = self._local_client.get_path_to_game(title_id) 581 | if game_installed: 582 | state |= LocalGameState.Installed 583 | 584 | if (title_id in self.running_games_info_list and 585 | check_if_process_exists(self.running_games_info_list[title_id].get_pid())): 586 | state |= LocalGameState.Running 587 | elif title_id in self.running_games_info_list: 588 | # We will leave the info in the list, because it still contains the game start time for game time 589 | # tracking. However, we will set the PID to None to indicate that the game has been closed. 590 | self.running_games_info_list[title_id].clear_pid() 591 | 592 | return LocalGame(str(self.games_cache[title_id]["rosTitleId"]), state) 593 | 594 | if IS_WINDOWS: 595 | async def get_local_games(self): 596 | # Since the API requires that get_local_games returns a list of LocalGame objects, local_list is the value 597 | # that needs to be returned. However, for internal use (the self.local_games_cache field), the dictionary 598 | # local_games is used for greater flexibility. 599 | local_games = {} 600 | local_list = [] 601 | state = LocalGameState.None_ 602 | for game in self.total_games_cache: 603 | title_id = get_game_title_id_from_ros_title_id(str(game.game_id)) 604 | game_installed = self._local_client.get_path_to_game(title_id) 605 | if title_id != "launcher" and game_installed: 606 | state |= LocalGameState.Installed 607 | local_game = self.check_game_status(title_id) 608 | local_games[title_id] = local_game 609 | local_list.append(local_game) 610 | else: 611 | continue 612 | self.local_games_cache = local_games 613 | log.debug(f"ROCKSTAR_INSTALLED_GAMES: {local_games}") 614 | return local_list 615 | 616 | async def check_for_new_games(self): 617 | self.checking_for_new_games = True 618 | # The Social Club prevents the user from making too many requests in a given time span to prevent a denial of 619 | # service attack. As such, we need to limit online checking to every 5 minutes. For Windows devices, log file 620 | # checks will still occur every minute, but for other users, checking games only happens every 5 minutes. 621 | owned_title_ids = None 622 | online_check_success = False 623 | if not self.last_online_game_check or time() >= self.last_online_game_check + 300: 624 | owned_title_ids, online_check_success = await self.get_owned_games_online() 625 | elif IS_WINDOWS: 626 | log.debug("ROCKSTAR_SC_ONLINE_GAMES_SKIP: No attempt has been made to scrape the user's games from the " 627 | "Social Club, as it has not been 5 minutes since the last check.") 628 | await self.get_owned_games(owned_title_ids, online_check_success) 629 | await asyncio.sleep(60 if IS_WINDOWS else 300) 630 | self.checking_for_new_games = False 631 | 632 | async def check_game_statuses(self): 633 | self.updating_game_statuses = True 634 | 635 | for title_id, current_local_game in self.local_games_cache.items(): 636 | new_local_game = self.check_game_status(title_id) 637 | if new_local_game != current_local_game: 638 | log.debug(f"ROCKSTAR_LOCAL_CHANGE: The status for {title_id} has changed from: {current_local_game} to " 639 | f"{new_local_game}.") 640 | self.update_local_game_status(new_local_game) 641 | self.local_games_cache[title_id] = new_local_game 642 | 643 | await asyncio.sleep(5) 644 | self.updating_game_statuses = False 645 | 646 | def list_running_game_pids(self): 647 | info_list = [] 648 | for key, value in self.running_games_info_list.items(): 649 | info_list.append(value.get_pid()) 650 | return str(info_list) 651 | 652 | if IS_WINDOWS: 653 | async def launch_platform_client(self): 654 | if not self._local_client.get_local_launcher_path(): 655 | await self.open_rockstar_browser() 656 | return 657 | 658 | pid = await self._local_client.launch_game_from_title_id("launcher") 659 | if not pid: 660 | log.warning("ROCKSTAR_LAUNCHER_FAILED: The Rockstar Games Launcher could not be launched!") 661 | 662 | if IS_WINDOWS: 663 | async def shutdown_platform_client(self): 664 | if not self._local_client.get_local_launcher_path(): 665 | await self.open_rockstar_browser() 666 | return 667 | 668 | await self._local_client.kill_launcher() 669 | 670 | if IS_WINDOWS: 671 | async def launch_game(self, game_id): 672 | if not self._local_client.get_local_launcher_path(): 673 | await self.open_rockstar_browser() 674 | return 675 | 676 | title_id = get_game_title_id_from_ros_title_id(game_id) 677 | game_pid = await self._local_client.launch_game_from_title_id(title_id) 678 | if game_pid: 679 | self.running_games_info_list[title_id] = RunningGameInfo() 680 | self.running_games_info_list[title_id].set_info(game_pid) 681 | log.debug(f"ROCKSTAR_PIDS: {self.list_running_game_pids()}") 682 | local_game = LocalGame(game_id, LocalGameState.Running | LocalGameState.Installed) 683 | self.update_local_game_status(local_game) 684 | self.local_games_cache[title_id] = local_game 685 | else: 686 | log.error(f'cannot start game: {title_id}') 687 | 688 | if IS_WINDOWS: 689 | async def install_game(self, game_id): 690 | if not self._local_client.get_local_launcher_path(): 691 | await self.open_rockstar_browser() 692 | return 693 | 694 | title_id = get_game_title_id_from_ros_title_id(game_id) 695 | log.debug("ROCKSTAR_INSTALL_REQUEST: Requesting to install " + title_id + "...") 696 | # There is no need to check if the game is a pre-order, since the InstallLocation registry key will be 697 | # unavailable if it is. 698 | self._local_client.install_game_from_title_id(title_id) 699 | 700 | if IS_WINDOWS: 701 | async def uninstall_game(self, game_id): 702 | if not self._local_client.get_local_launcher_path(): 703 | await self.open_rockstar_browser() 704 | return 705 | 706 | title_id = get_game_title_id_from_ros_title_id(game_id) 707 | log.debug("ROCKSTAR_UNINSTALL_REQUEST: Requesting to uninstall " + title_id + "...") 708 | self._local_client.uninstall_game_from_title_id(title_id) 709 | 710 | def create_game_from_title_id(self, title_id): 711 | return Game(str(self.games_cache[title_id]["rosTitleId"]), self.games_cache[title_id]["friendlyName"], None, 712 | self.games_cache[title_id]["licenseInfo"]) 713 | 714 | def tick(self): 715 | if not self.is_authenticated(): 716 | return 717 | if not self.checking_for_new_games: 718 | log.debug("Checking for new games...") 719 | asyncio.create_task(self.check_for_new_games()) 720 | if not self.updating_game_statuses and IS_WINDOWS: 721 | log.debug("Checking local game statuses...") 722 | asyncio.create_task(self.check_game_statuses()) 723 | 724 | 725 | def main(): 726 | create_and_run_plugin(RockstarPlugin, sys.argv) 727 | 728 | 729 | if __name__ == "__main__": 730 | main() 731 | -------------------------------------------------------------------------------- /src/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.5.13" 2 | --------------------------------------------------------------------------------