File: main.py

if __name__ == "__main__":
    from ghunt import ghunt; ghunt.main()


File: ghunt/config.py

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0',
    'Connection': 'Keep-Alive'
}

android_headers = {
    'User-Agent': '{}/323710070 (Linux; U; Android 11; fr_FR; Pixel 5; Build/RD2A.211001.002; Cronet/97.0.4692.70) grpc-java-cronet/1.44.0-SNAPSHOT', # android package name
    'Connection': 'Keep-Alive'
}

templates = {
    "gmaps_pb":{
        "stats": "!1s{}!2m3!1sYE3rYc2rEsqOlwSHx534DA!7e81!15i14416!6m2!4b1!7b1!9m0!16m4!1i100!4b1!5b1!6BQ0FFU0JrVm5TVWxEenc9PQ!17m28!1m6!1m2!1i0!2i0!2m2!1i458!2i736!1m6!1m2!1i1868!2i0!2m2!1i1918!2i736!1m6!1m2!1i0!2i0!2m2!1i1918!2i20!1m6!1m2!1i0!2i716!2m2!1i1918!2i736!18m12!1m3!1d806313.5865720833!2d150.19484835!3d-34.53825215!2m3!1f0!2f0!3f0!3m2!1i1918!2i736!4f13.1",
        "reviews": {
            "first": "!1s{}!2m5!1soViSYcvVG6iJytMPk6amiA8%3A1!2zMWk6NCx0OjE0MzIzLGU6MCxwOm9WaVNZY3ZWRzZpSnl0TVBrNmFtaUE4OjE!4m1!2i14323!7e81!6m2!4b1!7b1!9m0!10m6!1b1!2b1!5b1!8b1!9m1!1e3!14m69!1m57!1m4!1m3!1e3!1e2!1e4!3m5!2m4!3m3!1m2!1i260!2i365!4m1!3i10!10b1!11m42!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!2b1!4b1!2m5!1e1!1e4!1e3!1e5!1e2!3b0!4b1!5m1!1e1!7b1!16m3!1i10!4b1!5b1!17m0!18m9!1m3!1d2567.508024970022!2d-78.667885!3d35.7546725!2m0!3m2!1i537!2i609!4f13.1",
            "page": "!1s{}!2m3!1sYE3rYc2rEsqOlwSHx534DA!7e81!15i14416!6m2!4b1!7b1!9m0!16m4!1i100!4b1!5b1!6B{}!17m28!1m6!1m2!1i0!2i0!2m2!1i458!2i736!1m6!1m2!1i1868!2i0!2m2!1i1918!2i736!1m6!1m2!1i0!2i0!2m2!1i1918!2i20!1m6!1m2!1i0!2i716!2m2!1i1918!2i736!18m12!1m3!1d806313.5865720833!2d150.19484835!3d-34.53825215!2m3!1f0!2f0!3f0!3m2!1i1918!2i736!4f13.1"
        },
        "photos": {
            "first": "!1s{}!2m3!1spQUAYoPQLcOTlwT9u6-gDA!7e81!15i18404!9m0!14m69!1m57!1m4!1m3!1e3!1e2!1e4!3m5!2m4!3m3!1m2!1i260!2i365!4m1!3i10!10b1!11m42!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!2b1!4b1!2m5!1e1!1e4!1e3!1e5!1e2!3b1!4b1!5m1!1e1!7b1",
            "page": "!1s{}!2m3!1spQUAYoPQLcOTlwT9u6-gDA!7e81!15i14415!9m0!14m68!1m58!1m4!1m3!1e3!1e2!1e4!3m5!2m4!3m3!1m2!1i260!2i365!4m2!2s{}!3i100!10b1!11m42!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!2b1!4b1!2m5!1e1!1e4!1e3!1e5!1e2!5m1!1e1!7b1!17m28!1m6!1m2!1i0!2i0!2m2!1i458!2i595!1m6!1m2!1i950!2i0!2m2!1i1000!2i595!1m6!1m2!1i0!2i0!2m2!1i1000!2i20!1m6!1m2!1i0!2i575!2m2!1i1000!2i595!18m12!1m3!1d1304345.2752527467!2d149.32871599857805!3d-34.496155324132545!2m3!1f0!2f0!3f0!3m2!1i1000!2i595!4f13.1"
        }
    }
}

gmaps_radius     = 30 # in km. The radius distance to create groups of gmaps reviews.

# Cookies
default_consent_cookie = "YES+cb.20220118-08-p0.fr+FX+510"
default_pref_cookie = "tz=Europe.Paris&f6=40000000&hl=en" # To set the lang settings to english


File: ghunt/version.py

metadata = {
    "version": "2.2.0",
    "name": "Wardriving Edition"
}


File: ghunt/globals.py

# This file is only intended to serve global variables at a project-wide level.


def init_globals():
    from ghunt.objects.utils import TMPrinter
    from rich.console import Console

    global config, tmprinter, rc
    
    from ghunt import config
    tmprinter = TMPrinter()
    rc = Console(highlight=False) # Rich Console


File: ghunt/__init__.py

from ghunt import globals as gb; gb.init_globals()


File: ghunt/cli.py

from rich_argparse import RichHelpFormatter

import argparse
from typing import *
import sys
from pathlib import Path


def parse_and_run():
    RichHelpFormatter.styles["argparse.groups"] = "misty_rose1"
    RichHelpFormatter.styles["argparse.metavar"] = "light_cyan1"
    RichHelpFormatter.styles["argparse.args"] = "light_steel_blue1"
    RichHelpFormatter.styles["argparse.prog"] = "light_pink1 bold italic"


    parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
    subparsers = parser.add_subparsers(dest="module")

    ### Login module
    parser_login = subparsers.add_parser('login', help="Authenticate GHunt to Google.", formatter_class=RichHelpFormatter)
    parser_login.add_argument('--clean', action='store_true', help="Clear credentials local file.")

    ### Email module
    parser_email = subparsers.add_parser('email', help="Get information on an email address.", formatter_class=RichHelpFormatter)
    parser_email.add_argument("email_address")
    parser_email.add_argument('--json', type=Path, help="File to write the JSON output to.")

    ### Gaia module
    parser_gaia = subparsers.add_parser('gaia', help="Get information on a Gaia ID.", formatter_class=RichHelpFormatter)
    parser_gaia.add_argument("gaia_id")
    parser_gaia.add_argument('--json', type=Path, help="File to write the JSON output to.")

    ### Drive module
    parser_drive = subparsers.add_parser('drive', help="Get information on a Drive file or folder.", formatter_class=RichHelpFormatter)
    parser_drive.add_argument("file_id", help="Example: 1N__vVu4c9fCt4EHxfthUNzVOs_tp8l6tHcMBnpOZv_M")
    parser_drive.add_argument('--json', type=Path, help="File to write the JSON output to.")

    ### Geolocate module
    parser_geolocate = subparsers.add_parser('geolocate', help="Geolocate a BSSID.", formatter_class=RichHelpFormatter)
    geolocate_group = parser_geolocate.add_mutually_exclusive_group(required=True)
    geolocate_group.add_argument("-b", "--bssid", help="Example: 30:86:2d:c4:29:d0")
    geolocate_group.add_argument("-f", "--file", type=Path,  help="File containing a raw request body, useful to put many BSSIDs. ([italic light_steel_blue1][link=https://developers.google.com/maps/documentation/geolocation/requests-geolocation?#sample-requests]Reference format[/link][/italic light_steel_blue1])")
    parser_geolocate.add_argument('--json', type=Path, help="File to write the JSON output to.")

    ### Parsing
    args = None
    if not sys.argv[1:]:
        parser.parse_args(["--help"])
    else:
        for mod in ["email", "gaia", "drive", "geolocate"]:
            if sys.argv[1] == mod and not sys.argv[2:]:
                parser.parse_args([mod, "--help"])

    args = parser.parse_args(args)
    process_args(args)

def process_args(args: argparse.Namespace):
    import asyncio
    match args.module:
        case "login":
            from ghunt.modules import login
            asyncio.run(login.check_and_login(None, args.clean))
        case "email":
            from ghunt.modules import email
            asyncio.run(email.hunt(None, args.email_address, args.json))
        case "gaia":
            from ghunt.modules import gaia
            asyncio.run(gaia.hunt(None, args.gaia_id, args.json))
        case "drive":
            from ghunt.modules import drive
            asyncio.run(drive.hunt(None, args.file_id, args.json))
        case "geolocate":
            from ghunt.modules import geolocate
            asyncio.run(geolocate.main(None, args.bssid, args.file, args.json))


File: ghunt/errors.py

class GHuntKnowledgeError(BaseException):
    pass

class GHuntCorruptedHeadersError(BaseException):
    pass

class GHuntUnknownVerbError(BaseException):
    pass

class GHuntUnknownRequestDataTypeError(BaseException):
    pass

class GHuntInsufficientCreds(BaseException):
    pass

class GHuntParamsTemplateError(BaseException):
    pass

class GHuntParamsInputError(BaseException):
    pass

class GHuntAPIResponseParsingError(BaseException):
    pass

class GHuntObjectsMergingError(BaseException):
    pass

class GHuntAndroidMasterAuthError(BaseException):
    pass

class GHuntAndroidAppOAuth2Error(BaseException):
    pass

class GHuntOSIDAuthError(BaseException):
    pass

class GHuntCredsNotLoaded(BaseException):
    pass

class GHuntInvalidSession(BaseException):
    pass

class GHuntNotAuthenticated(BaseException):
    pass

class GHuntInvalidTarget(BaseException):
    pass

class GHuntLoginError(BaseException):
    pass


File: ghunt/ghunt.py

import sys


def main():
    version = sys.version_info
    if (version < (3, 10)):
        print('[-] GHunt only works with Python 3.10+.')
        print(f'Your current Python version : {version.major}.{version.minor}.{version.micro}')
        sys.exit(1)

    from ghunt.cli import parse_and_run
    from ghunt.helpers.banner import show_banner
    from ghunt.helpers.utils import show_version

    show_banner()
    show_version()
    print()
    parse_and_run()


File: ghunt/parsers/playgames.py

from typing import *
from datetime import datetime

from ghunt.objects.apis import Parser


### Profile

class PlayerProfile(Parser):
    def __init__(self):
        self.display_name: str = ""
        self.id: str = ""
        self.avatar_url: str = ""
        self.banner_url_portrait: str = ""
        self.banner_url_landscape: str = ""
        self.gamertag: str = ""
        self.last_played_app: PlayerPlayedApp = PlayerPlayedApp()
        self.profile_settings: PlayerProfileSettings = PlayerProfileSettings()
        self.experience_info: PlayerExperienceInfo = PlayerExperienceInfo()
        self.title: str = ""

    def _scrape(self, player_data: Dict[str, any]):
        self.display_name = player_data.get("playerId")
        self.display_name = player_data.get("displayName")
        self.avatar_url = player_data.get("avatarImageUrl")
        self.banner_url_portrait = player_data.get("bannerUrlPortrait")
        self.banner_url_landscape = player_data.get("bannerUrlLandscape")
        self.gamertag = player_data.get("gamerTag")
        if (last_played_app_data := player_data.get("lastPlayedApp")):
            self.last_played_app._scrape(last_played_app_data)
        if (profile_settings_data := player_data.get("profileSettings")):
            self.profile_settings._scrape(profile_settings_data)
        if (experience_data := player_data.get("experienceInfo")):
            self.experience_info._scrape(experience_data)
        self.title = player_data.get("title")

class PlayerPlayedApp(Parser):
    def __init__(self):
        self.app_id: str = ""
        self.icon_url: str = ""
        self.featured_image_url: str = ""
        self.app_name: str = ""
        self.timestamp_millis: str = ""

    def _scrape(self, played_app_data: Dict[str, any]):
        self.app_id = played_app_data.get("applicationId")
        self.icon_url = played_app_data.get("applicationIconUrl")
        self.featured_image_url = played_app_data.get("featuredImageUrl")
        self.app_name = played_app_data.get("applicationName")
        if (timestamp := played_app_data.get("timeMillis")):
            self.timestamp_millis = datetime.utcfromtimestamp(float(timestamp[:10]))

class PlayerExperienceInfo(Parser):
    def __init__(self):
        self.current_xp: str = ""
        self.last_level_up_timestamp_millis: str = ""
        self.current_level: PlayerLevel = PlayerLevel()
        self.next_level: PlayerLevel = PlayerLevel()
        self.total_unlocked_achievements: int = 0

    def _scrape(self, experience_data: Dict[str, any]):
        self.current_xp = experience_data.get("currentExperiencePoints")
        if (timestamp := experience_data.get("lastLevelUpTimestampMillis")):
            self.last_level_up_timestamp_millis = datetime.utcfromtimestamp(float(timestamp[:10]))
        if (current_level_data := experience_data.get("currentLevel")):
            self.current_level._scrape(current_level_data)
        if (next_level_data := experience_data.get("nextLevel")):
            self.next_level._scrape(next_level_data)
        self.total_unlocked_achievements = experience_data.get("totalUnlockedAchievements")

class PlayerLevel(Parser):
    def __init__(self):
        self.level: int = 0
        self.min_xp: str = ""
        self.max_xp: str = ""

    def _scrape(self, level_data: Dict[str, any]):
        self.level = level_data.get("level")
        self.min_xp = level_data.get("minExperiencePoints")
        self.max_xp = level_data.get("maxExperiencePoints")

class PlayerProfileSettings(Parser):
    def __init__(self):
        self.profile_visible: bool = False

    def _scrape(self, profile_settings_data: Dict[str, any]):
        self.profile_visible = profile_settings_data.get("profileVisible")

### Played Applications

class PlayedGames(Parser):
    def __init__(self):
        self.games: List[PlayGame] = []

    def _scrape(self, games_data: Dict[str, any]):
        for game_data in games_data:
            play_game = PlayGame()
            play_game._scrape(game_data)
            self.games.append(play_game)

class PlayGame(Parser):
    def __init__(self):
        self.game_data: PlayGameData = PlayGameData()
        self.market_data: PlayGameMarketData = PlayGameMarketData()
        self.formatted_last_played_time: str = ""
        self.last_played_time_millis: str = ""
        self.unlocked_achievement_count: int = 0

    def _scrape(self, game_data: Dict[str, any]):
        if (games_data := game_data.get("gamesData")):
            self.game_data._scrape(games_data)
        if (market_data := game_data.get("marketData")):
            self.market_data._scrape(market_data)
        self.formatted_last_played_time = game_data.get("formattedLastPlayedTime")
        if (timestamp := game_data.get("lastPlayedTimeMillis")):
            self.last_played_time_millis = datetime.utcfromtimestamp(float(timestamp[:10]))
        self.unlocked_achievement_count = game_data.get("unlockedAchievementCount")

class PlayGameMarketData(Parser):
    def __init__(self):
        self.instances: List[PlayGameMarketInstance] = []

    def _scrape(self, market_data: Dict[str, any]):
        if (instances_data := market_data.get("instances")):
            for instance_data in instances_data:
                instance = PlayGameMarketInstance()
                instance._scrape(instance_data)
                self.instances.append(instance)

class PlayGameMarketInstance(Parser):
    def __init__(self):
        self.id: str = ""
        self.title: str = ""
        self.description: str = ""
        self.images: List[PlayGameImageAsset] = []
        self.developer_name: str = ""
        self.categories: List[str] = []
        self.formatted_price: str = ""
        self.price_micros: str = ""
        self.badges: List[PlayGameMarketBadge] = []
        self.is_owned: bool = False
        self.enabled_features: List[str] = []
        self.description_snippet: str = ""
        self.rating: PlayGameMarketRating = PlayGameMarketRating()
        self.last_updated_timestamp_millis: str = ""
        self.availability: str = ""

    def _scrape(self, instance_data: Dict[str, any]):
        self.id = instance_data.get("id")
        self.title = instance_data.get("title")
        self.description = instance_data.get("description")
        if (images_data := instance_data.get("images")):
            for image_data in images_data:
                image = PlayGameImageAsset()
                image._scrape(image_data)
                self.images.append(image)
        self.developer_name = instance_data.get("developerName")
        self.categories = instance_data.get("categories", [])
        self.formatted_price = instance_data.get("formattedPrice")
        self.price_micros = instance_data.get("priceMicros")
        if (badges_data := instance_data.get("badges")):
            for badge_data in badges_data:
                badge = PlayGameMarketBadge()
                badge._scrape(badge_data)
                self.badges.append(badge)
        self.is_owned = instance_data.get("isOwned")
        self.enabled_features = instance_data.get("enabledFeatures", [])
        self.description_snippet = instance_data.get("descriptionSnippet")
        if (rating_data := instance_data.get("rating")):
            self.rating._scrape(rating_data)
        if (timestamp := instance_data.get("lastUpdatedTimestampMillis")):
            self.last_updated_timestamp_millis = datetime.utcfromtimestamp(float(timestamp[:10]))
        self.availability = instance_data.get("availability")

class PlayGameMarketRating(Parser):
    def __init__(self):
        self.star_rating: float = 0.0
        self.ratings_count: str = ""

    def _scrape(self, rating_data: Dict[str, any]):
        self.star_rating = rating_data.get("starRating")
        self.ratings_count = rating_data.get("ratingsCount")

class PlayGameMarketBadge(Parser):
    def __init__(self):
        self.badge_type: str = ""
        self.title: str = ""
        self.description: str = ""
        self.images: List[PlayGameImageAsset] = []

    def _scrape(self, badge_data: Dict[str, any]):
        self.badge_type = badge_data.get("badgeType")
        self.title = badge_data.get("title")
        self.description = badge_data.get("description")
        if (images_data := badge_data.get("images")):
            for image_data in images_data:
                image = PlayGameImageAsset()
                image._scrape(image_data)
                self.images.append(image)

class PlayGameData(Parser):
    def __init__(self):
        self.id: str = ""
        self.name: str = ""
        self.author: str = ""
        self.description: str = ""
        self.category: PlayGameCategory = PlayGameCategory()
        self.assets: List[PlayGameImageAsset] = []
        self.instances: List[PlayGameInstance] = []
        self.last_updated_timestamp: str = ""
        self.achievement_count: int = 0,
        self.leaderboard_count: int = 0,
        self.enabled_features: List[str] = []
        self.theme_color: str = ""

    def _scrape(self, game_data: Dict[str, any]):
        self.id = game_data.get("id")
        self.name = game_data.get("name")
        self.author = game_data.get("author")
        self.description = game_data.get("description")
        if (category_data := game_data.get("category")):
            self.category._scrape(category_data)
        if (assets_data := game_data.get("assets")):
            for asset_data in assets_data:
                asset = PlayGameImageAsset()
                asset._scrape(asset_data)
                self.assets.append(asset)
        if (instances_data := game_data.get("instances")):
            for instance_data in instances_data:
                instance = PlayGameInstance()
                instance._scrape(instance_data)
                self.instances.append(instance)
        if (timestamp := game_data.get("lastUpdatedTimestamp")):
            self.last_updated_timestamp = datetime.utcfromtimestamp(float(timestamp[:10]))
        self.achievement_count = game_data.get("achievement_count")
        self.leaderboard_count = game_data.get("leaderboard_count")
        self.enabled_features = game_data.get("enabledFeatures", [])
        self.theme_color = game_data.get("themeColor")

class PlayGameInstance(Parser):
    def __init__(self):
        self.platform_type: str = ""
        self.name: str = ""
        self.turn_based_play: bool = False
        self.realtime_play: bool = False
        self.android_instance: List[PlayGameAndroidInstance] = []
        self.acquisition_uri: str = ""

    def _scrape(self, instance_data: Dict[str, any]):
        self.platform_type = instance_data.get("plateformType")
        self.name = instance_data.get("name")
        self.turn_based_play = instance_data.get("turnBasedPlay")
        self.realtime_play = instance_data.get("realtimePlay")
        if (android_instance_data := instance_data.get("androidInstance")):
            android_instance = PlayGameAndroidInstance()
            android_instance._scrape(android_instance_data)
            self.android_instance.append(android_instance_data)

class PlayGameAndroidInstance(Parser):
    def __init__(self):
        self.package_name: str = ""
        self.enable_piracy_check: bool = False
        self.preferred: bool = False

    def _scrape(self, android_instance_data: Dict[str, any]):
        self.package_name = android_instance_data.get("packageName")
        self.enable_piracy_check = android_instance_data.get("enablePiracyCheck")
        self.preferred = android_instance_data.get("preferred")

class PlayGameImageAsset(Parser):
    def __init__(self):
        self.name: str = ""
        self.width: str = ""
        self.height: str = ""
        self.url: str = ""

    def _scrape(self, image_data: Dict[str, any]):
        self.name = image_data.get("name")
        self.width = image_data.get("width")
        self.height = image_data.get("height")
        self.url = image_data.get("url")

class PlayGameCategory(Parser):
    def __init__(self):
        self.primary: str = ""

    def _scrape(self, category_data: Dict[str, any]):
        self.primary = category_data.get("primary")

### Achievements

class PlayerAchievements(Parser):
    def __init__(self):
        self.achievements: List[PlayerAchievement] = []

    def _scrape(self, achievements_data: Dict[str, any]):
        achievements_defs : List[PlayerAchievementDefinition] = []
        if (achievement_defs_data := achievements_data.get("definitions")):
            for achievement_def_data in achievement_defs_data:
                achievement_def = PlayerAchievementDefinition()
                achievement_def._scrape(achievement_def_data)
                achievements_defs.append(achievement_def)
        if (achievements_items_data := achievements_data.get("items")):
            for achievement_item_data in achievements_items_data:
                achievement = PlayerAchievement()
                achievement._scrape(achievement_item_data)
                for achievement_def in achievements_defs:
                    if achievement_def.id == achievement.id:
                        achievement.definition = achievement_def
                self.achievements.append(achievement)

class PlayerAchievement(Parser):
    def __init__(self):
        self.id: str = ""
        self.achievement_state: str = ""
        self.last_updated_timestamp: datetime = 0
        self.app_id: str = 0
        self.xp: str = ""
        self.definition: PlayerAchievementDefinition = PlayerAchievementDefinition()

    def _scrape(self, achievement_item_data: Dict[str, any]):
        self.id = achievement_item_data.get("id")
        self.achievement_state = achievement_item_data.get("achievementState")
        if (timestamp := achievement_item_data.get("lastUpdatedTimestamp")):
            self.last_updated_timestamp = datetime.utcfromtimestamp(float(timestamp[:10]))
        self.app_id = achievement_item_data.get("application_id")
        self.xp = achievement_item_data.get("experiencePoints")

class PlayerAchievementDefinition(Parser):
    def __init__(self):
        self.id: str = ""
        self.name: str = ""
        self.description: str = ""
        self.achievement_type: str = ""
        self.xp: str = ""
        self.revealed_icon_url: str = ""
        self.unlocked_icon_url: str = ""
        self.initial_state: str = ""
        self.is_revealed_icon_url_default: bool = False
        self.is_unlocked_icon_url_default: bool = False
        self.rarity_percent: float = 0.0

    def _scrape(self, achievement_def_data: Dict[str, any]):
        self.id = achievement_def_data.get("id")
        self.name = achievement_def_data.get("name")
        self.description = achievement_def_data.get("description")
        self.achievement_type = achievement_def_data.get("achievementType")
        self.xp = achievement_def_data.get("experiencePoints")
        self.revealed_icon_url = achievement_def_data.get("revealedIconUrl")
        self.unlocked_icon_url = achievement_def_data.get("unlockedIconUrl")
        self.initial_state = achievement_def_data.get("initialState")
        self.is_revealed_icon_url_default = achievement_def_data.get("isRevealedIconUrlDefault")
        self.is_unlocked_icon_url_default = achievement_def_data.get("isUnlockedIconUrlDefault")
        self.rarity_percent = achievement_def_data.get("rarityParcent")

### Global

class Player(Parser):
    def __init__(self, profile: PlayerProfile = PlayerProfile(),
                played_games: List[PlayGame] = [],
                achievements: List[PlayerAchievement] = []):
        self.profile: PlayerProfile = profile
        self.played_games: List[PlayGame] = played_games
        self.achievements: List[PlayerAchievement] = achievements


File: ghunt/parsers/playgateway.py

from typing import *

from ghunt.protos.playgatewaypa.search_player_results_pb2 import PlayerSearchResultsProto
from ghunt.protos.playgatewaypa.get_player_response_pb2 import GetPlayerResponseProto
from ghunt.objects.apis import Parser


class PlayerSearchResult(Parser):
    def __init__(self):
        self.name: str = ""
        self.id: str = ""
        self.avatar_url: str = ""

    def _scrape(self, player_result_data):
        self.name = player_result_data.account.name
        self.id = player_result_data.account.id
        self.avatar_url = player_result_data.avatar.url

class PlayerSearchResults(Parser):
    def __init__(self):
        self.results: List[PlayerSearchResult] = []

    def _scrape(self, proto_results: PlayerSearchResultsProto):
        for player_result_data in proto_results.field1.results.field1.field1.player:
            player_search_result = PlayerSearchResult()
            player_search_result._scrape(player_result_data)
            self.results.append(player_search_result)

class PlayerProfile(Parser):
    """
        This parsing is not complete at all, we only use it
        in GHunt to dump total played games & achievements.
    """
    def __init__(self):
        self.achievements_count: int = 0
        self.played_games_count: int = 0

    def _scrape(self, proto_results: GetPlayerResponseProto):
        for section in proto_results.field1.results.section:
            if section.field3.section_name == "Games":
                self.played_games_count = int(section.counter.number)
            elif section.field3.section_name == "Achievements":
                self.achievements_count = int(section.counter.number)


File: ghunt/parsers/vision.py

from ghunt.objects.apis import Parser

from typing import *


class VisionPosition(Parser):
    def __init__(self):
        self.x: int = None
        self.y: int = None
        self.z: int = None
    
    def _scrape(self, position_data: Dict[str, int]):
        self.x = position_data.get("x")
        self.y = position_data.get("y")
        self.z = position_data.get("z")

class VisionLandmark(Parser):
    def __init__(self):
        self.type: str = ""
        self.position: VisionPosition = VisionPosition()

    def _scrape(self, landmark_data: Dict[str, any]):
        self.type = landmark_data["type"]
        self.position._scrape(landmark_data["position"])

class VisionVertice(Parser):
    def __init__(self):
        self.x: int = None
        self.y: int = None

    def _scrape(self, vertice_data: Dict[str, int]):
        self.x = vertice_data.get("x")
        self.y = vertice_data.get("y")

class VisionVertices(Parser):
    def __init__(self):
        self.vertices: List[VisionVertice] = []

    def _scrape(self, vertices_data: List[Dict[str, int]]):
        for vertice_data in vertices_data:
            vertice = VisionVertice()
            vertice._scrape(vertice_data)
            self.vertices.append(vertice)

class VisionFaceAnnotation(Parser):
    def __init__(self):
        self.bounding_poly: VisionVertices = VisionVertices()
        self.fd_bounding_poly: VisionVertices = VisionVertices()
        self.landmarks: List[VisionLandmark] = []
        self.roll_angle: int = 0,
        self.pan_angle: int = 0,
        self.tilt_angle: int = 0,
        self.detection_confidence: int = 0,
        self.landmarking_confidence: int = 0,
        self.joy_likelihood: str = "",
        self.sorrow_likelihood: str = "",
        self.anger_likelihood: str = "",
        self.surprise_likelihood: str = "",
        self.under_exposed_likelihood: str = "",
        self.blurred_likelihood: str = "",
        self.headwear_likelihood: str = ""

    def _scrape(self, face_data: Dict[str, any]):
        if (vertices_data := face_data.get("boundingPoly", {}).get("vertices")):
            self.bounding_poly._scrape(vertices_data)
        if (vertices_data := face_data.get("fdBoundingPoly", {}).get("vertices")):
            self.fd_bounding_poly._scrape(vertices_data)
        if (landmarks_data := face_data.get("landmarks")):
            for landmark_data in landmarks_data:
                landmark = VisionLandmark()
                landmark._scrape(landmark_data)
                self.landmarks.append(landmark)
        self.roll_angle = face_data.get("rollAngle")
        self.pan_angle = face_data.get("panAngle")
        self.tilt_angle = face_data.get("tiltAngle")
        self.detection_confidence = face_data.get("detectionConfidence")
        self.landmarking_confidence = face_data.get("landmarkingConfidence")
        self.joy_likelihood = face_data.get("joyLikelihood")
        self.sorrow_likelihood = face_data.get("sorrowLikelihood")
        self.anger_likelihood = face_data.get("angerLikelihood")
        self.surprise_likelihood = face_data.get("surpriseLikelihood")
        self.under_exposed_likelihood = face_data.get("underExposedLikelihood")
        self.blurred_likelihood = face_data.get("blurredLikelihood")
        self.headwear_likelihood = face_data.get("headwearLikelihood")

class VisionFaceDetection(Parser):
    def __init__(self):
        self.face_annotations: List[VisionFaceAnnotation] = []

    def _scrape(self, vision_data: Dict[str, any]):
        for face_data in vision_data["faceAnnotations"]:
            face_annotation = VisionFaceAnnotation()
            face_annotation._scrape(face_data)
            self.face_annotations.append(face_annotation)


File: ghunt/parsers/__init__.py




File: ghunt/parsers/clientauthconfig.py

from typing import *
from ghunt.objects.apis import Parser


class CacBrand(Parser):
	def __init__(self):
		self.brand_id: str = ""
		self.project_ids: List[list] = []
		self.project_numbers: List[str] = []
		self.display_name: str = ""
		self.icon_url: str = ""
		self.stored_icon_url: str = ""
		self.support_email: str = ""
		self.home_page_url: str = ""
		self.terms_of_service_urls: List[list] = []
		self.privacy_policy_urls: List[list] = []
		self.direct_notice_to_parents_url: str = ""
		self.brand_state: CacBrandState = CacBrandState()
		self.clients: List[list] = []
		self.review: CacReview = CacReview()
		self.is_org_internal: bool = False
		self.risc_configuration: CacRiscConfiguration = CacRiscConfiguration()
		self.consistency_token: str = ""
		self.creation_time: str = ""
		self.verified_brand: CacVerifiedBrand = CacVerifiedBrand()

	def _scrape(self, base_model_data: Dict[str, any]):
		self.brand_id = base_model_data.get('brandId')
		self.project_ids = base_model_data.get('projectIds')
		self.project_numbers = base_model_data.get('projectNumbers')
		self.display_name = base_model_data.get('displayName')
		self.icon_url = base_model_data.get('iconUrl')
		self.stored_icon_url = base_model_data.get('storedIconUrl')
		self.support_email = base_model_data.get('supportEmail')
		self.home_page_url = base_model_data.get('homePageUrl')
		self.terms_of_service_urls = base_model_data.get('termsOfServiceUrls')
		self.privacy_policy_urls = base_model_data.get('privacyPolicyUrls')
		self.direct_notice_to_parents_url = base_model_data.get('directNoticeToParentsUrl')
		if (brand_state_data := base_model_data.get('brandState')):
			self.brand_state._scrape(brand_state_data)
		self.clients = base_model_data.get('clients')
		if (review_data := base_model_data.get('review')):
			self.review._scrape(review_data)
		self.is_org_internal = base_model_data.get('isOrgInternal')
		if (risc_configuration_data := base_model_data.get('riscConfiguration')):
			self.risc_configuration._scrape(risc_configuration_data)
		self.consistency_token = base_model_data.get('consistencyToken')
		self.creation_time = base_model_data.get('creationTime')
		if (verified_brand_data := base_model_data.get('verifiedBrand')):
			self.verified_brand._scrape(verified_brand_data)

class CacBrandState(Parser):
	def __init__(self):
		self.state: str = ""
		self.admin_id: str = ""
		self.reason: str = ""
		self.limits: CacLimits = CacLimits()
		self.brand_setup: str = ""
		self.creation_flow: str = ""
		self.update_timestamp: str = ""

	def _scrape(self, brand_state_data: Dict[str, any]):
		self.state = brand_state_data.get('state')
		self.admin_id = brand_state_data.get('adminId')
		self.reason = brand_state_data.get('reason')
		if (limits_data := brand_state_data.get('limits')):
			self.limits._scrape(limits_data)
		self.brand_setup = brand_state_data.get('brandSetup')
		self.creation_flow = brand_state_data.get('creationFlow')
		self.update_timestamp = brand_state_data.get('updateTimestamp')

class CacLimits(Parser):
	def __init__(self):
		self.approval_quota_multiplier: int = 0
		self.max_domain_count: int = 0
		self.default_max_client_count: int = 0

	def _scrape(self, limits_data: Dict[str, int]):
		self.approval_quota_multiplier = limits_data.get('approvalQuotaMultiplier')
		self.max_domain_count = limits_data.get('maxDomainCount')
		self.default_max_client_count = limits_data.get('defaultMaxClientCount')

class CacReview(Parser):
	def __init__(self):
		self.has_abuse_verdict: bool = False
		self.is_published: bool = False
		self.review_state: str = ""
		self.high_risk_scopes_privilege: str = ""
		self.low_risk_scopes: List[list] = []
		self.pending_scopes: List[list] = []
		self.exempt_scopes: List[list] = []
		self.approved_scopes: List[list] = []
		self.historical_approved_scopes: List[list] = []
		self.pending_domains: List[str] = []
		self.approved_domains: List[list] = []
		self.enforce_request_scopes: bool = False
		self.category: List[list] = []
		self.decision_timestamp: str = ""

	def _scrape(self, review_data: Dict[str, any]):
		self.has_abuse_verdict = review_data.get('hasAbuseVerdict')
		self.is_published = review_data.get('isPublished')
		self.review_state = review_data.get('reviewState')
		self.high_risk_scopes_privilege = review_data.get('highRiskScopesPrivilege')
		self.low_risk_scopes = review_data.get('lowRiskScopes')
		self.pending_scopes = review_data.get('pendingScopes')
		self.exempt_scopes = review_data.get('exemptScopes')
		self.approved_scopes = review_data.get('approvedScopes')
		self.historical_approved_scopes = review_data.get('historicalApprovedScopes')
		self.pending_domains = review_data.get('pendingDomains')
		self.approved_domains = review_data.get('approvedDomains')
		self.enforce_request_scopes = review_data.get('enforceRequestScopes')
		self.category = review_data.get('category')
		self.decision_timestamp = review_data.get('decisionTimestamp')

class CacRiscConfiguration(Parser):
	def __init__(self):
		self.enabled: bool = False
		self.delivery_method: str = ""
		self.receiver_supported_event_type: List[list] = []
		self.legal_agreement: List[str] = []

	def _scrape(self, risc_configuration_data: Dict[str, any]):
		self.enabled = risc_configuration_data.get('enabled')
		self.delivery_method = risc_configuration_data.get('deliveryMethod')
		self.receiver_supported_event_type = risc_configuration_data.get('receiverSupportedEventType')
		self.legal_agreement = risc_configuration_data.get('legalAgreement')

class CacVerifiedBrand(Parser):
	def __init__(self):
		self.display_name: CacDisplayName = CacDisplayName()
		self.stored_icon_url: CacStoredIconUrl = CacStoredIconUrl()
		self.support_email: CacSupportEmail = CacSupportEmail()
		self.home_page_url: CacHomePageUrl = CacHomePageUrl()
		self.privacy_policy_url: CacPrivacyPolicyUrl = CacPrivacyPolicyUrl()
		self.terms_of_service_url: CacTermsOfServiceUrl = CacTermsOfServiceUrl()

	def _scrape(self, verified_brand_data: Dict[str, any]):
		if (display_name_data := verified_brand_data.get('displayName')):
			self.display_name._scrape(display_name_data)
		if (stored_icon_url_data := verified_brand_data.get('storedIconUrl')):
			self.stored_icon_url._scrape(stored_icon_url_data)
		if (support_email_data := verified_brand_data.get('supportEmail')):
			self.support_email._scrape(support_email_data)
		if (home_page_url_data := verified_brand_data.get('homePageUrl')):
			self.home_page_url._scrape(home_page_url_data)
		if (privacy_policy_url_data := verified_brand_data.get('privacyPolicyUrl')):
			self.privacy_policy_url._scrape(privacy_policy_url_data)
		if (terms_of_service_url_data := verified_brand_data.get('termsOfServiceUrl')):
			self.terms_of_service_url._scrape(terms_of_service_url_data)

class CacDisplayName(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, display_name_data: Dict[str, str]):
		self.value = display_name_data.get('value')
		self.reason = display_name_data.get('reason')

class CacStoredIconUrl(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, stored_icon_url_data: Dict[str, str]):
		self.value = stored_icon_url_data.get('value')
		self.reason = stored_icon_url_data.get('reason')

class CacSupportEmail(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, support_email_data: Dict[str, str]):
		self.value = support_email_data.get('value')
		self.reason = support_email_data.get('reason')

class CacHomePageUrl(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, home_page_url_data: Dict[str, str]):
		self.value = home_page_url_data.get('value')
		self.reason = home_page_url_data.get('reason')

class CacPrivacyPolicyUrl(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, privacy_policy_url_data: Dict[str, str]):
		self.value = privacy_policy_url_data.get('value')
		self.reason = privacy_policy_url_data.get('reason')

class CacTermsOfServiceUrl(Parser):
	def __init__(self):
		self.value: str = ""
		self.reason: str = ""

	def _scrape(self, terms_of_service_url_data: Dict[str, str]):
		self.value = terms_of_service_url_data.get('value')
		self.reason = terms_of_service_url_data.get('reason')




File: ghunt/parsers/calendar.py

from datetime import datetime
from typing import *

from ghunt.helpers.utils import get_datetime_utc
from ghunt.objects.apis import Parser


class ConferenceProperties(Parser):
    def __init__(self):
        self.allowed_conference_solution_types: List[str] = []

    def _scrape(self, conference_props_data: Dict[str, any]):
        if (types := conference_props_data.get("allowedConferenceSolutionTypes")):
            self.allowed_conference_solution_types = types

class Calendar(Parser):
    def __init__(self):
        self.id: str = ""
        self.summary: str = ""
        self.time_zone: str = ""
        self.conference_properties: ConferenceProperties = ConferenceProperties()

    def _scrape(self, calendar_data: Dict[str, any]):
        self.id = calendar_data.get("id")
        self.summary = calendar_data.get("summary")
        self.time_zone = calendar_data.get("timeZone")
        conference_props_data = calendar_data.get("conferenceProperties")
        if conference_props_data:
            self.conference_properties._scrape(conference_props_data)

class CalendarReminder(Parser):
    def __init__(self):
        self.method: str = ""
        self.minutes: int = 0

    def _scrape(self, reminder_data: Dict[str, any]):
        self.method = reminder_data.get("method")
        self.minutes = reminder_data.get("minutes")

class CalendarPerson(Parser):
    def __init__(self):
        self.email: str = ""
        self.display_name: str = ""
        self.self: bool = None

    def _scrape(self, person_data: Dict[str, any]):
        self.email = person_data.get("email")
        self.display_name = person_data.get("displayName")
        self.self = person_data.get("self")

class CalendarTime(Parser):
    def __init__(self):
        self.date_time: datetime = None # ISO Format
        self.time_zone: str = ""

    def _scrape(self, time_data: Dict[str, any]):
        if (date_time := time_data.get("dateTime")):
            try:
                self.date_time = get_datetime_utc(date_time)
            except ValueError:
                self.date_time = None
        self.time_zone = time_data.get("timeZone")

class CalendarReminders(Parser):
    def __init__(self):
        self.use_default: int = 0
        self.overrides: List[CalendarReminder] = []

    def _scrape(self, reminders_data: Dict[str, any]):
        self.use_default = reminders_data.get("useDefault")
        if (overrides := reminders_data.get("overrides")):
            for reminder_data in overrides:
                reminder = CalendarReminder()
                reminder._scrape(reminder_data)
                self.overrides.append(reminder)

class CalendarEvent(Parser):
    def __init__(self):
        self.id: str = ""
        self.status: str = ""
        self.html_link: str = ""
        self.created: datetime = "" # ISO Format
        self.updated: datetime = "" # ISO Format
        self.summary: str = ""
        self.description: str = ""
        self.location: str = ""
        self.creator: CalendarPerson = CalendarPerson()
        self.organizer: CalendarPerson = CalendarPerson()
        self.start: CalendarTime = CalendarTime()
        self.end: CalendarTime = CalendarTime()
        self.recurring_event_id: str = ""
        self.original_start_time: CalendarTime = CalendarTime()
        self.visibility: str = ""
        self.ical_uid: str = ""
        self.sequence: int = 0
        self.guest_can_invite_others: bool = None
        self.reminders: CalendarReminders = CalendarReminders()
        self.event_type: str = ""

    def _scrape(self, event_data: Dict[str, any]):
        self.id = event_data.get("id")
        self.status = event_data.get("status")
        self.html_link = event_data.get("htmlLink")
        if (date_time := event_data.get("created")):
            try:
                self.created = get_datetime_utc(date_time)
            except ValueError:
                self.created = None
        if (date_time := event_data.get("updated")):
            try:
                self.updated = get_datetime_utc(date_time)
            except ValueError:
                self.updated = None
        self.summary = event_data.get("summary")
        self.description = event_data.get("description")
        self.location = event_data.get("location")
        if (creator_data := event_data.get("creator")):
            self.creator._scrape(creator_data)
        if (organizer_data := event_data.get("organizer")):
            self.organizer._scrape(organizer_data)
        if (start_data := event_data.get("start")):
            self.start._scrape(start_data)
        if (end_data := event_data.get("end")):
            self.end._scrape(end_data)
        self.recurring_event_id = event_data.get("recurringEventId")
        if (original_start_data := event_data.get("originalStartTime")):
            self.original_start_time._scrape(original_start_data)
        self.visibility = event_data.get("visibility")
        self.ical_uid = event_data.get("iCalUID")
        self.sequence = event_data.get("sequence")
        self.guest_can_invite_others = event_data.get("guestsCanInviteOthers")
        if (reminders_data := event_data.get("reminders")):
            self.reminders._scrape(reminders_data)
        self.event_type = event_data.get("eventType")

class CalendarEvents(Parser):
    def __init__(self):
        self.summary: str = ""
        self.updated: datetime = "" # ISO Format
        self.time_zone: str = ""
        self.access_role: str = ""
        self.default_reminders: List[CalendarReminder] = []
        self.next_page_token: str = ""
        self.items: List[CalendarEvent] = []

    def _scrape(self, events_data: Dict[str, any]):
        self.summary = events_data.get("summary")
        if (date_time := events_data.get("updated")):
            try:
                self.updated = get_datetime_utc(date_time)
            except ValueError:
                self.updated = None
        self.time_zone = events_data.get("timeZone")
        self.access_role = events_data.get("accessRole")
        if (reminders_data := events_data.get("defaultReminders")):
            for reminder_data in reminders_data:
                reminder = CalendarReminder()
                reminder._scrape(reminder_data)
                self.default_reminders.append(reminder)
        self.next_page_token = events_data.get("nextPageToken")
        if (items_data := events_data.get("items")):
            for item_data in items_data:
                event = CalendarEvent()
                event._scrape(item_data)
                self.items.append(event)


File: ghunt/parsers/people.py

from typing import *
from datetime import datetime

from ghunt.errors import *
from ghunt.helpers.utils import is_default_profile_pic, unicode_patch
from ghunt.objects.apis import Parser

import httpx
import imagehash


class PersonGplusExtendedData(Parser):
    def __init__(self):
        self.contentRestriction: str = ""
        self.isEntrepriseUser: bool = False

    def _scrape(self, gplus_data):
        self.contentRestriction = gplus_data.get("contentRestriction")
        
        if (isEnterpriseUser := gplus_data.get("isEnterpriseUser")):
            self.isEntrepriseUser = isEnterpriseUser

class PersonDynamiteExtendedData(Parser):
    def __init__(self):
        self.presence: str = ""
        self.entityType: str = ""
        self.dndState: str = ""
        self.customerId: str = ""

    def _scrape(self, dynamite_data):
        self.presence = dynamite_data.get("presence")
        self.entityType = dynamite_data.get("entityType")
        self.dndState = dynamite_data.get("dndState")
        if (customerId := dynamite_data.get("organizationInfo", {}).get("customerInfo", {}).
            get("customerId", {}).get("customerId")):
            self.customerId = customerId

class PersonExtendedData(Parser):
    def __init__(self):
         self.dynamiteData: PersonDynamiteExtendedData = PersonDynamiteExtendedData()
         self.gplusData: PersonGplusExtendedData = PersonGplusExtendedData()

    def _scrape(self, extended_data: Dict[str, any]):
        if (dynamite_data := extended_data.get("dynamiteExtendedData")):
            self.dynamiteData._scrape(dynamite_data)

        if (gplus_data := extended_data.get("gplusExtendedData")):
            self.gplusData._scrape(gplus_data)

class PersonPhoto(Parser):
    def __init__(self):
        self.url: str = ""
        self.isDefault: bool = False
        self.flathash: str = None

    async def _scrape(self, as_client: httpx.AsyncClient, photo_data: Dict[str, any], photo_type: str):
        if photo_type == "profile_photo":
            self.url = photo_data.get("url")

            self.isDefault, self.flathash = await is_default_profile_pic(as_client, self.url)
            
        elif photo_type == "cover_photo":
            self.url = '='.join(photo_data.get("imageUrl").split("=")[:-1])
            if (isDefault := photo_data.get("isDefault")):
                self.isDefault = isDefault
        else:
            raise GHuntAPIResponseParsingError(f'The provided photo type "{photo_type}" weren\'t recognized.')

class PersonEmail(Parser):
    def __init__(self):
        self.value: str = ""
    
    def _scrape(self, email_data: Dict[str, any]):
        self.value = email_data.get("value")

class PersonName(Parser):
    def __init__(self):
        self.fullname: str = ""
        self.firstName: str = ""
        self.lastName: str = ""

    def _scrape(self, name_data: Dict[str, any]):
        # self.fullname = unicode_patch(x) if (x := name_data.get("displayName")) else None
        # self.firstName = unicode_patch(x) if (x := name_data.get("givenName")) else None
        # self.lastName = unicode_patch(x) if (x := name_data.get("familyName")) else None
        pass # Google patched the names :/ very sad

class PersonProfileInfo(Parser):
    def __init__(self):
        self.userTypes: List[str] = []

    def _scrape(self, profile_data: Dict[str, any]):
        if "ownerUserType" in profile_data:
            self.userTypes += profile_data.get("ownerUserType")

class PersonSourceIds(Parser):
    def __init__(self):
        self.lastUpdated: datetime = None

    def _scrape(self, source_ids_data: Dict[str, any]):
        if (timestamp := source_ids_data.get("lastUpdatedMicros")):
            self.lastUpdated = datetime.utcfromtimestamp(float(timestamp[:10]))

class PersonInAppReachability(Parser):
    def __init__(self):
        self.apps: List[str] = []

    def _scrape(self, apps_data, container_name: str):
        for app in apps_data:
            if app["metadata"]["container"] == container_name:
                self.apps.append(app["appType"].title())

class PersonContainers(dict):
    pass

class Person(Parser):
    def __init__(self):
        self.personId: str = ""
        self.sourceIds: Dict[str, PersonSourceIds] = PersonContainers() # All the fetched containers
        self.emails: Dict[str, PersonEmail] = PersonContainers()
        self.names: Dict[str, PersonName] = PersonContainers()
        self.profileInfos: Dict[str, PersonProfileInfo] = PersonContainers()
        self.profilePhotos: Dict[str, PersonPhoto] = PersonContainers()
        self.coverPhotos: Dict[str, PersonPhoto] = PersonContainers()
        self.inAppReachability: Dict[str, PersonInAppReachability] = PersonContainers()
        self.extendedData: PersonExtendedData = PersonExtendedData()

    async def _scrape(self, as_client: httpx.AsyncClient, person_data: Dict[str, any]):
        self.personId = person_data.get("personId")
        if person_data.get("email"):
            for email_data in person_data["email"]:
                person_email = PersonEmail()
                person_email._scrape(email_data)
                self.emails[email_data["metadata"]["container"]] = person_email

        if person_data.get("name"):
            for name_data in person_data["name"]:
                person_name = PersonName()
                person_name._scrape(name_data)
                self.names[name_data["metadata"]["container"]] = person_name

        if person_data.get("readOnlyProfileInfo"):
            for profile_data in person_data["readOnlyProfileInfo"]:
                person_profile = PersonProfileInfo()
                person_profile._scrape(profile_data)
                self.profileInfos[profile_data["metadata"]["container"]] = person_profile

                if person_data.get("photo"):
                    for photo_data in person_data["photo"]:
                        person_photo = PersonPhoto()
                        await person_photo._scrape(as_client, photo_data, "profile_photo")
                        self.profilePhotos[profile_data["metadata"]["container"]] = person_photo

        if (source_ids := person_data.get("metadata", {}).get("identityInfo", {}).get("sourceIds")):
            for source_ids_data in source_ids:
                person_source_ids = PersonSourceIds()
                person_source_ids._scrape(source_ids_data)
                self.sourceIds[source_ids_data["container"]] = person_source_ids

        if person_data.get("coverPhoto"):
            for cover_photo_data in person_data["coverPhoto"]:
                person_cover_photo = PersonPhoto()
                await person_cover_photo._scrape(as_client, cover_photo_data, "cover_photo")
                self.coverPhotos[cover_photo_data["metadata"]["container"]] = person_cover_photo

        if (apps_data := person_data.get("inAppReachability")):
            containers_names = set()
            for app_data in person_data["inAppReachability"]:
                containers_names.add(app_data["metadata"]["container"])

            for container_name in containers_names:
                person_app_reachability = PersonInAppReachability()
                person_app_reachability._scrape(apps_data, container_name)
                self.inAppReachability[container_name] = person_app_reachability

        if (extended_data := person_data.get("extendedData")):
            self.extendedData._scrape(extended_data)


File: ghunt/parsers/drive.py

from typing import *
from datetime import datetime

from ghunt.helpers.utils import get_datetime_utc
from ghunt.objects.apis import Parser


class DriveFile(Parser):
	def __init__(self):
		self.kind: str = ""
		self.id: str = ""
		self.thumbnail_version: str = ""
		self.title: str = ""
		self.mime_type: str = ""
		self.labels: DriveLabels = DriveLabels()
		self.created_date: datetime = None
		self.modified_date: datetime = None
		self.last_viewed_by_me_date: datetime = None
		self.marked_viewed_by_me_date: datetime = None
		self.shared_with_me_date: datetime = None
		self.recency: datetime = None
		self.recency_reason: str = ""
		self.version: str = ""
		self.parents: List[DriveParentReference] = []
		self.user_permission: DrivePermission = DrivePermission()
		self.file_extension: str = ""
		self.file_size: str = ""
		self.quota_bytes_used: str = ""
		self.owners: List[DriveUser] = []
		self.last_modifying_user: DriveUser = DriveUser()
		self.capabilities: DriveCapabilities = DriveCapabilities()
		self.copyable: bool = False
		self.shared: bool = False
		self.explicitly_trashed: bool = False
		self.authorized_app_ids: List[str] = []
		self.primary_sync_parent_id: str = ""
		self.subscribed: bool = False
		self.passively_subscribed: bool = False
		self.flagged_for_abuse: bool = False
		self.abuse_is_appealable: bool = False
		self.source_app_id: str = ""
		self.spaces: List[str] = []
		self.has_thumbnail: bool = False
		self.contains_unsubscribed_children: bool = False
		self.alternate_link: str = ""
		self.icon_link: str = ""
		self.copy_requires_writer_permission: bool = False
		self.permissions: List[DrivePermission] = []
		self.head_revision_id: str = ""
		self.video_media_metadata: DriveVideoMediaMetadata = DriveVideoMediaMetadata()
		self.has_legacy_blob_comments: bool = False
		self.label_info: DriveLabelInfo = DriveLabelInfo()
		self.web_content_link: str = ""
		self.thumbnail_link: str = ""
		self.description: str = ""
		self.original_filename: str = ""
		self.permissions_summary: DrivePermissionsSummary = DrivePermissionsSummary()
		self.full_file_extension: str = ""
		self.md5_checksum: str = ""
		self.owned_by_me: bool = False
		self.writers_can_share: bool = False
		self.image_media_metadata: DriveImageMediaMetadata = DriveImageMediaMetadata()
		self.is_app_authorized: bool = False
		self.link_share_metadata: DriveLinkShareMetadata = DriveLinkShareMetadata()
		self.etag: str = ""
		self.self_link: str = ""
		self.embed_link: str = ""
		self.open_with_links: DriveOpenWithLinks = DriveOpenWithLinks()
		self.default_open_with_link: str = ""
		self.has_child_folders: bool = False
		self.owner_names: List[str] = []
		self.last_modifying_user_name: str = ""
		self.editable: bool = False
		self.app_data_contents: bool = False
		self.drive_source: DriveSource = DriveSource()
		self.source: DriveSource = DriveSource()
		self.descendant_of_root: bool = False
		self.folder_color: str = ""
		self.folder_properties: DriveFolderProperties = DriveFolderProperties()
		self.resource_key: str = ""
		self.has_augmented_permissions: bool = False
		self.ancestor_has_augmented_permissions: bool = False
		self.has_visitor_permissions: bool = False
		self.primary_domain_name: str = ""
		self.organization_display_name: str = ""
		self.customer_id: str = ""
		self.team_drive_id: str = ""
		self.folder_color_rgb: str = ""

	def _scrape(self, file_data: Dict[str, any]):
		self.kind = file_data.get('kind')
		self.id = file_data.get('id')
		self.thumbnail_version = file_data.get('thumbnailVersion')
		self.title = file_data.get('title')
		self.mime_type = file_data.get('mimeType')
		if (labels_data := file_data.get('labels')):
			self.labels._scrape(labels_data)
		if (isodate := file_data.get("createdDate")):
			self.created_date = get_datetime_utc(isodate)
		if (isodate := file_data.get("modifiedDate")):
			self.modified_date = get_datetime_utc(isodate)
		if (isodate := file_data.get("lastViewedByMeDate")):
			self.last_viewed_by_me_date = get_datetime_utc(isodate)
		if (isodate := file_data.get("markedViewedByMeDate")):
			self.marked_viewed_by_me_date = get_datetime_utc(isodate)
		if (isodate := file_data.get("sharedWithMeDate")):
			self.shared_with_me_date = get_datetime_utc(isodate)
		if (isodate := file_data.get("recency")):
			self.recency = get_datetime_utc(isodate)
		self.recency_reason = file_data.get('recencyReason')
		self.version = file_data.get('version')
		if (parents_data := file_data.get('parents')):
			for parents_data_item in parents_data:
				parents_item = DriveParentReference()
				parents_item._scrape(parents_data_item)
				self.parents.append(parents_item)
		if (user_permission_data := file_data.get('userPermission')):
			self.user_permission._scrape(user_permission_data)
		self.file_extension = file_data.get('fileExtension')
		self.file_size = file_data.get('fileSize')
		self.quota_bytes_used = file_data.get('quotaBytesUsed')
		if (owners_data := file_data.get('owners')):
			for owners_data_item in owners_data:
				owners_item = DriveUser()
				owners_item._scrape(owners_data_item)
				self.owners.append(owners_item)
		if (last_modifying_user_data := file_data.get('lastModifyingUser')):
			self.last_modifying_user._scrape(last_modifying_user_data)
		if (capabilities_data := file_data.get('capabilities')):
			self.capabilities._scrape(capabilities_data)
		self.copyable = file_data.get('copyable')
		self.shared = file_data.get('shared')
		self.explicitly_trashed = file_data.get('explicitlyTrashed')
		self.authorized_app_ids = file_data.get('authorizedAppIds')
		self.primary_sync_parent_id = file_data.get('primarySyncParentId')
		self.subscribed = file_data.get('subscribed')
		self.passively_subscribed = file_data.get('passivelySubscribed')
		self.flagged_for_abuse = file_data.get('flaggedForAbuse')
		self.abuse_is_appealable = file_data.get('abuseIsAppealable')
		self.source_app_id = file_data.get('sourceAppId')
		self.spaces = file_data.get('spaces')
		self.has_thumbnail = file_data.get('hasThumbnail')
		self.contains_unsubscribed_children = file_data.get('containsUnsubscribedChildren')
		self.alternate_link = file_data.get('alternateLink')
		self.icon_link = file_data.get('iconLink')
		self.copy_requires_writer_permission = file_data.get('copyRequiresWriterPermission')
		if (permissions_data := file_data.get('permissions')):
			for permissions_data_item in permissions_data:
				permissions_item = DrivePermission()
				permissions_item._scrape(permissions_data_item)
				self.permissions.append(permissions_item)
		self.head_revision_id = file_data.get('headRevisionId')
		if (video_media_metadata_data := file_data.get('videoMediaMetadata')):
			self.video_media_metadata._scrape(video_media_metadata_data)
		self.has_legacy_blob_comments = file_data.get('hasLegacyBlobComments')
		if (label_info_data := file_data.get('labelInfo')):
			self.label_info._scrape(label_info_data)
		self.web_content_link = file_data.get('webContentLink')
		self.thumbnail_link = file_data.get('thumbnailLink')
		self.description = file_data.get('description')
		self.original_filename = file_data.get('originalFilename')
		if (permissions_summary_data := file_data.get('permissionsSummary')):
			self.permissions_summary._scrape(permissions_summary_data)
		self.full_file_extension = file_data.get('fullFileExtension')
		self.md5_checksum = file_data.get('md5Checksum')
		self.owned_by_me = file_data.get('ownedByMe')
		self.writers_can_share = file_data.get('writersCanShare')
		if (image_media_metadata_data := file_data.get('imageMediaMetadata')):
			self.image_media_metadata._scrape(image_media_metadata_data)
		self.is_app_authorized = file_data.get('isAppAuthorized')
		if (link_share_metadata_data := file_data.get('linkShareMetadata')):
			self.link_share_metadata._scrape(link_share_metadata_data)
		self.etag = file_data.get('etag')
		self.self_link = file_data.get('selfLink')
		self.embed_link = file_data.get('embedLink')
		if (open_with_links_data := file_data.get('openWithLinks')):
			self.open_with_links._scrape(open_with_links_data)
		self.default_open_with_link = file_data.get('defaultOpenWithLink')
		self.has_child_folders = file_data.get('hasChildFolders')
		self.owner_names = file_data.get('ownerNames')
		self.last_modifying_user_name = file_data.get('lastModifyingUserName')
		self.editable = file_data.get('editable')
		self.app_data_contents = file_data.get('appDataContents')
		if (drive_source_data := file_data.get('driveSource')):
			self.drive_source._scrape(drive_source_data)
		if (source_data := file_data.get('source')):
			self.source._scrape(source_data)
		self.descendant_of_root = file_data.get('descendantOfRoot')
		self.folder_color = file_data.get('folderColor')
		if (folder_properties_data := file_data.get('folderProperties')):
			self.folder_properties._scrape(folder_properties_data)
		self.resource_key = file_data.get('resourceKey')
		self.has_augmented_permissions = file_data.get('hasAugmentedPermissions')
		self.ancestor_has_augmented_permissions = file_data.get('ancestorHasAugmentedPermissions')
		self.has_visitor_permissions = file_data.get('hasVisitorPermissions')
		self.primary_domain_name = file_data.get('primaryDomainName')
		self.organization_display_name = file_data.get('organizationDisplayName')
		self.customer_id = file_data.get('customerId')
		self.team_drive_id = file_data.get('teamDriveId')
		self.folder_color_rgb = file_data.get('folderColorRgb')

class DriveLabels(Parser):
	def __init__(self):
		self.starred: bool = False
		self.trashed: bool = False
		self.restricted: bool = False
		self.viewed: bool = False
		self.hidden: bool = False
		self.modified: bool = False

	def _scrape(self, labels_data: Dict[str, bool]):
		self.starred = labels_data.get('starred')
		self.trashed = labels_data.get('trashed')
		self.restricted = labels_data.get('restricted')
		self.viewed = labels_data.get('viewed')
		self.hidden = labels_data.get('hidden')
		self.modified = labels_data.get('modified')

class DriveUserPermission(Parser):
	def __init__(self):
		self.role: str = ""
		self.id: str = ""
		self.type: str = ""

	def _scrape(self, user_permission_data: Dict[str, str]):
		self.role = user_permission_data.get('role')
		self.id = user_permission_data.get('id')
		self.type = user_permission_data.get('type')

class DriveUser(Parser):
	def __init__(self):
		self.kind: str = ""
		self.id: str = ""
		self.permission_id: str = ""
		self.email_address_from_account: str = ""
		self.display_name: str = ""
		self.picture: DrivePicture = DrivePicture()
		self.is_authenticated_user: bool = False
		self.email_address: str = ""

	def _scrape(self, user_data: Dict[str, any]):
		self.kind = user_data.get('kind')
		self.id = user_data.get('id')
		self.permission_id = user_data.get('permissionId')
		self.email_address_from_account = user_data.get('emailAddressFromAccount')
		self.display_name = user_data.get('displayName')
		if (picture_data := user_data.get('picture')):
			self.picture._scrape(picture_data)
		self.is_authenticated_user = user_data.get('isAuthenticatedUser')
		self.email_address = user_data.get('emailAddress')

class DriveCapabilities(Parser):
	def __init__(self):
		self.can_add_children: bool = False
		self.can_add_my_drive_parent: bool = False
		self.can_block_owner: bool = False
		self.can_change_security_update_enabled: bool = False
		self.can_copy: bool = False
		self.can_delete: bool = False
		self.can_download: bool = False
		self.can_edit: bool = False
		self.can_edit_category_metadata: bool = False
		self.can_request_approval: bool = False
		self.can_move_children_within_drive: bool = False
		self.can_move_item_into_team_drive: bool = False
		self.can_move_item_within_drive: bool = False
		self.can_read: bool = False
		self.can_read_category_metadata: bool = False
		self.can_remove_children: bool = False
		self.can_remove_my_drive_parent: bool = False
		self.can_rename: bool = False
		self.can_share: bool = False
		self.can_share_child_files: bool = False
		self.can_share_child_folders: bool = False
		self.can_trash: bool = False
		self.can_untrash: bool = False
		self.can_comment: bool = False
		self.can_move_item_out_of_drive: bool = False
		self.can_add_as_owner: bool = False
		self.can_add_as_organizer: bool = False
		self.can_add_as_file_organizer: bool = False
		self.can_add_as_writer: bool = False
		self.can_add_as_commenter: bool = False
		self.can_add_as_reader: bool = False
		self.can_change_to_owner: bool = False
		self.can_change_to_organizer: bool = False
		self.can_change_to_file_organizer: bool = False
		self.can_change_to_writer: bool = False
		self.can_change_to_commenter: bool = False
		self.can_change_to_reader: bool = False
		self.can_change_to_reader_on_published_view: bool = False
		self.can_remove: bool = False
		self.can_accept_ownership: bool = False
		self.can_add_encrypted_children: bool = False
		self.can_change_copy_requires_writer_permission: bool = False
		self.can_change_permission_expiration: bool = False
		self.can_change_restricted_download: bool = False
		self.can_change_writers_can_share: bool = False
		self.can_create_decrypted_copy: bool = False
		self.can_create_encrypted_copy: bool = False
		self.can_list_children: bool = False
		self.can_manage_members: bool = False
		self.can_manage_visitors: bool = False
		self.can_modify_content: bool = False
		self.can_modify_content_restriction: bool = False
		self.can_modify_labels: bool = False
		self.can_print: bool = False
		self.can_read_all_permissions: bool = False
		self.can_read_labels: bool = False
		self.can_read_revisions: bool = False
		self.can_set_missing_required_fields: bool = False
		self.can_share_as_commenter: bool = False
		self.can_share_as_file_organizer: bool = False
		self.can_share_as_organizer: bool = False
		self.can_share_as_owner: bool = False
		self.can_share_as_reader: bool = False
		self.can_share_as_writer: bool = False
		self.can_share_published_view_as_reader: bool = False
		self.can_share_to_all_users: bool = False
		self.can_add_folder_from_another_drive: bool = False
		self.can_delete_children: bool = False
		self.can_move_item_out_of_team_drive: bool = False
		self.can_move_item_within_team_drive: bool = False
		self.can_move_team_drive_item: bool = False
		self.can_read_team_drive: bool = False
		self.can_trash_children: bool = False

	def _scrape(self, capabilities_data: Dict[str, bool]):
		self.can_add_children = capabilities_data.get('canAddChildren')
		self.can_add_my_drive_parent = capabilities_data.get('canAddMyDriveParent')
		self.can_block_owner = capabilities_data.get('canBlockOwner')
		self.can_change_security_update_enabled = capabilities_data.get('canChangeSecurityUpdateEnabled')
		self.can_copy = capabilities_data.get('canCopy')
		self.can_delete = capabilities_data.get('canDelete')
		self.can_download = capabilities_data.get('canDownload')
		self.can_edit = capabilities_data.get('canEdit')
		self.can_edit_category_metadata = capabilities_data.get('canEditCategoryMetadata')
		self.can_request_approval = capabilities_data.get('canRequestApproval')
		self.can_move_children_within_drive = capabilities_data.get('canMoveChildrenWithinDrive')
		self.can_move_item_into_team_drive = capabilities_data.get('canMoveItemIntoTeamDrive')
		self.can_move_item_within_drive = capabilities_data.get('canMoveItemWithinDrive')
		self.can_read = capabilities_data.get('canRead')
		self.can_read_category_metadata = capabilities_data.get('canReadCategoryMetadata')
		self.can_remove_children = capabilities_data.get('canRemoveChildren')
		self.can_remove_my_drive_parent = capabilities_data.get('canRemoveMyDriveParent')
		self.can_rename = capabilities_data.get('canRename')
		self.can_share = capabilities_data.get('canShare')
		self.can_share_child_files = capabilities_data.get('canShareChildFiles')
		self.can_share_child_folders = capabilities_data.get('canShareChildFolders')
		self.can_trash = capabilities_data.get('canTrash')
		self.can_untrash = capabilities_data.get('canUntrash')
		self.can_comment = capabilities_data.get('canComment')
		self.can_move_item_out_of_drive = capabilities_data.get('canMoveItemOutOfDrive')
		self.can_add_as_owner = capabilities_data.get('canAddAsOwner')
		self.can_add_as_organizer = capabilities_data.get('canAddAsOrganizer')
		self.can_add_as_file_organizer = capabilities_data.get('canAddAsFileOrganizer')
		self.can_add_as_writer = capabilities_data.get('canAddAsWriter')
		self.can_add_as_commenter = capabilities_data.get('canAddAsCommenter')
		self.can_add_as_reader = capabilities_data.get('canAddAsReader')
		self.can_change_to_owner = capabilities_data.get('canChangeToOwner')
		self.can_change_to_organizer = capabilities_data.get('canChangeToOrganizer')
		self.can_change_to_file_organizer = capabilities_data.get('canChangeToFileOrganizer')
		self.can_change_to_writer = capabilities_data.get('canChangeToWriter')
		self.can_change_to_commenter = capabilities_data.get('canChangeToCommenter')
		self.can_change_to_reader = capabilities_data.get('canChangeToReader')
		self.can_change_to_reader_on_published_view = capabilities_data.get('canChangeToReaderOnPublishedView')
		self.can_remove = capabilities_data.get('canRemove')
		self.can_accept_ownership = capabilities_data.get('canAcceptOwnership')
		self.can_add_encrypted_children = capabilities_data.get('canAddEncryptedChildren')
		self.can_change_copy_requires_writer_permission = capabilities_data.get('canChangeCopyRequiresWriterPermission')
		self.can_change_permission_expiration = capabilities_data.get('canChangePermissionExpiration')
		self.can_change_restricted_download = capabilities_data.get('canChangeRestrictedDownload')
		self.can_change_writers_can_share = capabilities_data.get('canChangeWritersCanShare')
		self.can_create_decrypted_copy = capabilities_data.get('canCreateDecryptedCopy')
		self.can_create_encrypted_copy = capabilities_data.get('canCreateEncryptedCopy')
		self.can_list_children = capabilities_data.get('canListChildren')
		self.can_manage_members = capabilities_data.get('canManageMembers')
		self.can_manage_visitors = capabilities_data.get('canManageVisitors')
		self.can_modify_content = capabilities_data.get('canModifyContent')
		self.can_modify_content_restriction = capabilities_data.get('canModifyContentRestriction')
		self.can_modify_labels = capabilities_data.get('canModifyLabels')
		self.can_print = capabilities_data.get('canPrint')
		self.can_read_all_permissions = capabilities_data.get('canReadAllPermissions')
		self.can_read_labels = capabilities_data.get('canReadLabels')
		self.can_read_revisions = capabilities_data.get('canReadRevisions')
		self.can_set_missing_required_fields = capabilities_data.get('canSetMissingRequiredFields')
		self.can_share_as_commenter = capabilities_data.get('canShareAsCommenter')
		self.can_share_as_file_organizer = capabilities_data.get('canShareAsFileOrganizer')
		self.can_share_as_organizer = capabilities_data.get('canShareAsOrganizer')
		self.can_share_as_owner = capabilities_data.get('canShareAsOwner')
		self.can_share_as_reader = capabilities_data.get('canShareAsReader')
		self.can_share_as_writer = capabilities_data.get('canShareAsWriter')
		self.can_share_published_view_as_reader = capabilities_data.get('canSharePublishedViewAsReader')
		self.can_share_to_all_users = capabilities_data.get('canShareToAllUsers')
		self.can_add_folder_from_another_drive = capabilities_data.get('canAddFolderFromAnotherDrive')
		self.can_delete_children = capabilities_data.get('canDeleteChildren')
		self.can_move_item_out_of_team_drive = capabilities_data.get('canMoveItemOutOfTeamDrive')
		self.can_move_item_within_team_drive = capabilities_data.get('canMoveItemWithinTeamDrive')
		self.can_move_team_drive_item = capabilities_data.get('canMoveTeamDriveItem')
		self.can_read_team_drive = capabilities_data.get('canReadTeamDrive')
		self.can_trash_children = capabilities_data.get('canTrashChildren')

class DriveVideoMediaMetadata(Parser):
	def __init__(self):
		self.width: int = 0
		self.height: int = 0
		self.duration_millis: str = ""

	def _scrape(self, video_media_metadata_data: Dict[str, any]):
		self.width = video_media_metadata_data.get('width')
		self.height = video_media_metadata_data.get('height')
		self.duration_millis = video_media_metadata_data.get('durationMillis')

class DriveLabelInfo(Parser):
	def __init__(self):
		self.label_count: int = 0
		self.incomplete: bool = False

	def _scrape(self, label_info_data: Dict[str, any]):
		self.label_count = label_info_data.get('labelCount')
		self.incomplete = label_info_data.get('incomplete')

class DrivePermission(Parser):
	def __init__(self):
		self.kind: str = ""
		self.id: str = ""
		self.self_link: str = ""
		self.role: str = ""
		self.additional_roles: List[str] = []
		self.type: str = ""
		self.selectable_roles: List[list] = []
		self.pending_owner: bool = False
		self.with_link: bool = False
		self.capabilities: DriveCapabilities = DriveCapabilities()
		self.user_id: str = ""
		self.name: str = ""
		self.email_address: str = ""
		self.domain: str = ""
		self.photo_link: str = ""
		self.deleted: bool = False
		self.is_collaborator_account: bool = False

	def _scrape(self, permission_data: Dict[str, any]):
		self.kind = permission_data.get('kind')
		self.id = permission_data.get('id')
		self.self_link = permission_data.get('selfLink')
		self.role = permission_data.get('role')
		self.additional_roles = permission_data.get('additionalRoles', [])
		self.type = permission_data.get('type')
		self.selectable_roles = permission_data.get('selectableRoles')
		self.pending_owner = permission_data.get('pendingOwner')
		self.with_link = permission_data.get('withLink')
		if (capabilities_data := permission_data.get('capabilities')):
			self.capabilities._scrape(capabilities_data)
		self.user_id = permission_data.get('userId')
		self.name = permission_data.get('name')
		self.email_address = permission_data.get('emailAddress')
		self.domain = permission_data.get('domain')
		self.photo_link = permission_data.get('photoLink')
		self.deleted = permission_data.get('deleted')
		self.is_collaborator_account = permission_data.get('isCollaboratorAccount')

class DrivePermissionsSummary(Parser):
	def __init__(self):
		self.entry_count: int = 0
		self.visibility: List[DriveMiniPermission] = []
		self.select_permissions: List[DrivePermission] = []

	def _scrape(self, permissions_summary_data: Dict[str, any]):
		self.entry_count = permissions_summary_data.get('entryCount')
		if (visibility_data := permissions_summary_data.get('visibility')):
			for visibility_data_item in visibility_data:
				visibility_item = DriveMiniPermission()
				visibility_item._scrape(visibility_data_item)
				self.visibility.append(visibility_item)
		if (select_permissions_data := permissions_summary_data.get('selectPermissions')):
			for select_permissions_data_item in select_permissions_data:
				select_permissions_item = DrivePermission()
				select_permissions_item._scrape(select_permissions_data_item)
				self.select_permissions.append(select_permissions_item)

class DriveMiniPermission(Parser):
	def __init__(self):
		self.permission_id: str = ""
		self.role: str = ""
		self.type: str = ""
		self.with_link: bool = False

	def _scrape(self, unknown_model4_data: Dict[str, any]):
		self.permission_id = unknown_model4_data.get('permissionId')
		self.role = unknown_model4_data.get('role')
		self.type = unknown_model4_data.get('type')
		self.with_link = unknown_model4_data.get('withLink')

class DrivePicture(Parser):
	def __init__(self):
		self.url: str = ""

	def _scrape(self, picture_data: Dict[str, str]):
		self.url = picture_data.get('url')

class DriveImageMediaMetadata(Parser):
	def __init__(self):
		self.width: int = 0
		self.height: int = 0
		self.rotation: int = 0

	def _scrape(self, image_media_metadata_data: Dict[str, int]):
		self.width = image_media_metadata_data.get('width')
		self.height = image_media_metadata_data.get('height')
		self.rotation = image_media_metadata_data.get('rotation')

class DriveLinkShareMetadata(Parser):
	def __init__(self):
		self.security_update_eligible: bool = False
		self.security_update_enabled: bool = False
		self.security_update_change_disabled_reason: str = ""
		self.security_update_explicitly_set: bool = False

	def _scrape(self, link_share_metadata_data: Dict[str, any]):
		self.security_update_eligible = link_share_metadata_data.get('securityUpdateEligible')
		self.security_update_enabled = link_share_metadata_data.get('securityUpdateEnabled')
		self.security_update_change_disabled_reason = link_share_metadata_data.get('securityUpdateChangeDisabledReason')
		self.security_update_explicitly_set = link_share_metadata_data.get('securityUpdateExplicitlySet')

class DriveChildList(Parser):
	def __init__(self):
		self.kind: str = ""
		self.etag: str = ""
		self.self_link: str = ""
		self.items: List[DriveChildReference] = []

	def _scrape(self, child_list_data: Dict[str, any]):
		self.kind = child_list_data.get('kind')
		self.etag = child_list_data.get('etag')
		self.self_link = child_list_data.get('selfLink')
		if (items_data := child_list_data.get('items')):
			for items_data_item in items_data:
				items_item = DriveChildReference()
				items_item._scrape(items_data_item)
				self.items.append(items_item)

class DriveChildReference(Parser):
	def __init__(self):
		self.id: str = ""
		self.self_link: str = ""
		self.kind: str = ""
		self.child_link: str = ""

	def _scrape(self, child_reference_data: Dict[str, str]):
		self.id = child_reference_data.get('id')
		self.self_link = child_reference_data.get('selfLink')
		self.kind = child_reference_data.get('kind')
		self.child_link = child_reference_data.get('childLink')

class DriveApp(Parser):
	def __init__(self):
		self.kind: str = ""
		self.id: str = ""
		self.name: str = ""
		self.type: str = ""
		self.short_description: str = ""
		self.long_description: str = ""
		self.supports_create: bool = False
		self.supports_import: bool = False
		self.supports_multi_open: bool = False
		self.supports_offline_create: bool = False
		self.supports_mobile_browser: bool = False
		self.installed: bool = False
		self.authorized: bool = False
		self.drive_branded_app: bool = False
		self.drive_branded: bool = False
		self.hidden: bool = False
		self.removable: bool = False
		self.has_drive_wide_scope: bool = False
		self.use_by_default: bool = False
		self.primary_mime_types: List[str] = []
		self.requires_authorization_before_open_with: bool = False
		self.supports_team_drives: bool = False
		self.supports_all_drives: bool = False

	def _scrape(self, app_data: Dict[str, any]):
		self.kind = app_data.get('kind')
		self.id = app_data.get('id')
		self.name = app_data.get('name')
		self.type = app_data.get('type')
		self.short_description = app_data.get('shortDescription')
		self.long_description = app_data.get('longDescription')
		self.supports_create = app_data.get('supportsCreate')
		self.supports_import = app_data.get('supportsImport')
		self.supports_multi_open = app_data.get('supportsMultiOpen')
		self.supports_offline_create = app_data.get('supportsOfflineCreate')
		self.supports_mobile_browser = app_data.get('supportsMobileBrowser')
		self.installed = app_data.get('installed')
		self.authorized = app_data.get('authorized')
		self.drive_branded_app = app_data.get('driveBrandedApp')
		self.drive_branded = app_data.get('driveBranded')
		self.hidden = app_data.get('hidden')
		self.removable = app_data.get('removable')
		self.has_drive_wide_scope = app_data.get('hasDriveWideScope')
		self.use_by_default = app_data.get('useByDefault')
		self.primary_mime_types = app_data.get('primaryMimeTypes')
		self.requires_authorization_before_open_with = app_data.get('requiresAuthorizationBeforeOpenWith')
		self.supports_team_drives = app_data.get('supportsTeamDrives')
		self.supports_all_drives = app_data.get('supportsAllDrives')

class DriveOpenWithLinks(Parser):
	def __init__(self):
		self.digitsfield: str = ""

	def _scrape(self, open_with_links_data: Dict[str, str]):
		self.digitsfield = open_with_links_data.get('digits_field')

class DriveParentReference(Parser):
	def __init__(self):
		self.kind: str = ""
		self.id: str = ""
		self.self_link: str = ""
		self.parent_link: str = ""
		self.is_root: bool = False

	def _scrape(self, parent_reference_data: Dict[str, any]):
		self.kind = parent_reference_data.get('kind')
		self.id = parent_reference_data.get('id')
		self.self_link = parent_reference_data.get('selfLink')
		self.parent_link = parent_reference_data.get('parentLink')
		self.is_root = parent_reference_data.get('isRoot')

class DriveSource(Parser):
	def __init__(self):
		self.client_service_id: str = ""
		self.value: str = ""

	def _scrape(self, source_data: Dict[str, str]):
		self.client_service_id = source_data.get('clientServiceId')
		self.value = source_data.get('value')

class DriveFolderProperties(Parser):
	def __init__(self):
		self.psyncho_root: bool = False
		self.psyncho_folder: bool = False
		self.machine_root: bool = False
		self.arbitrary_sync_folder: bool = False
		self.external_media: bool = False
		self.photos_and_videos_only: bool = False

	def _scrape(self, folder_properties_data: Dict[str, bool]):
		self.psyncho_root = folder_properties_data.get('psynchoRoot')
		self.psyncho_folder = folder_properties_data.get('psynchoFolder')
		self.machine_root = folder_properties_data.get('machineRoot')
		self.arbitrary_sync_folder = folder_properties_data.get('arbitrarySyncFolder')
		self.external_media = folder_properties_data.get('externalMedia')
		self.photos_and_videos_only = folder_properties_data.get('photosAndVideosOnly')

class DriveCommentList(Parser):
	def __init__(self):
		self.kind: str = ""
		self.self_link: str = ""
		self.items: List[DriveComment] = []

	def _scrape(self, comment_list_data: Dict[str, any]):
		self.kind = comment_list_data.get('kind')
		self.self_link = comment_list_data.get('selfLink')
		if (items_data := comment_list_data.get('items')):
			for items_data_item in items_data:
				items_item = DriveComment()
				items_item._scrape(items_data_item)
				self.items.append(items_item)

class DriveComment(Parser):
	def __init__(self):
		self.comment_id: str = ""
		self.kind: str = ""
		self.created_date: str = ""
		self.modified_date: str = ""
		self.file_id: str = ""
		self.status: str = ""
		self.anchor: str = ""
		self.replies: List[DriveCommentReply] = []
		self.author: DriveUser = DriveUser()
		self.deleted: bool = False
		self.html_content: str = ""
		self.content: str = ""
		self.context: DriveCommentContext = DriveCommentContext()
		self.file_title: str = ""

	def _scrape(self, comment_data: Dict[str, any]):
		self.comment_id = comment_data.get('commentId')
		self.kind = comment_data.get('kind')
		self.created_date = comment_data.get('createdDate')
		self.modified_date = comment_data.get('modifiedDate')
		self.file_id = comment_data.get('fileId')
		self.status = comment_data.get('status')
		self.anchor = comment_data.get('anchor')
		if (replies_data := comment_data.get('replies')):
			for replies_data_item in replies_data:
				replies_item = DriveCommentReply()
				replies_item._scrape(replies_data_item)
				self.replies.append(replies_item)
		if (author_data := comment_data.get('author')):
			self.author._scrape(author_data)
		self.deleted = comment_data.get('deleted')
		self.html_content = comment_data.get('htmlContent')
		self.content = comment_data.get('content')
		if (context_data := comment_data.get('context')):
			self.context._scrape(context_data)
		self.file_title = comment_data.get('fileTitle')

class DriveCommentContext(Parser):
	def __init__(self):
		self.type: str = ""
		self.value: str = ""

	def _scrape(self, context_data: Dict[str, str]):
		self.type = context_data.get('type')
		self.value = context_data.get('value')

class DriveCommentReply(Parser):
	def __init__(self):
		self.reply_id: str = ""
		self.kind: str = ""
		self.created_date: str = ""
		self.modified_date: str = ""
		self.author: DriveUser = DriveUser()
		self.deleted: bool = False
		self.html_content: str = ""
		self.content: str = ""

	def _scrape(self, comment_reply_data: Dict[str, any]):
		self.reply_id = comment_reply_data.get('replyId')
		self.kind = comment_reply_data.get('kind')
		self.created_date = comment_reply_data.get('createdDate')
		self.modified_date = comment_reply_data.get('modifiedDate')
		if (author_data := comment_reply_data.get('author')):
			self.author._scrape(author_data)
		self.deleted = comment_reply_data.get('deleted')
		self.html_content = comment_reply_data.get('htmlContent')
		self.content = comment_reply_data.get('content')


File: ghunt/parsers/geolocate.py

from ghunt.objects.apis import Parser
from ghunt.objects.base import Position

from typing import *


class GeolocationResponse(Parser):
    def __init__(self):
        self.accuracy: int = 0
        self.location: Position = Position()

    def _scrape(self, base_model_data: dict[str, any]):
        self.accuracy = base_model_data.get("accuracy")

        location = base_model_data.get("location")
        self.location.longitude = location.get("lng")
        self.location.latitude = location.get("lat")


File: ghunt/apis/geolocation.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.geolocate import GeolocationResponse

import httpx

from typing import *
import inspect
import json


class GeolocationHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        self.hostname = "www.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = None # sapisidhash, cookies_only, oauth or None
        self.require_key = "geolocation" # key name, or None

        self._load_api(creds, headers)

    async def geolocate(self, as_client: httpx.AsyncClient, bssid: str, body: dict) -> Tuple[bool, GeolocationResponse]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "POST"
        base_url = f"/geolocation/v1/geolocate"
        data_type = "json" # json, data or None

        if bssid:
            payload = {
                "considerIp": False,
                "wifiAccessPoints": [
                    {
                        "macAddress": "00:25:9c:cf:1c:ad"
                    },
                    {
                        "macAddress": bssid
                    },
                ]
            }
        else:
            payload = body

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, payload, data_type)

        # Parsing
        data = json.loads(req.text)

        resp = GeolocationResponse()
        if "error" in data:
            return False, resp
        
        resp._scrape(data)

        return True, resp


File: ghunt/apis/playgames.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.playgames import PlayedGames, PlayerAchievements, PlayerProfile

import httpx

from typing import *
import inspect
import json


class PlayGames(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        # Android OAuth fields
        self.api_name = "playgames"
        self.package_name = "com.google.android.play.games"
        self.scopes = [
            "https://www.googleapis.com/auth/games.firstparty",
            "https://www.googleapis.com/auth/googleplay"
        ]
        
        self.hostname = "www.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = "oauth" # sapisidhash, cookies_only, oauth or None
        self.require_key = None # key name, or None

        self._load_api(creds, headers)

    async def get_profile(self, as_client: httpx.AsyncClient, player_id: str) -> Tuple[bool, PlayerProfile]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/games/v1whitelisted/players/{player_id}"
        data_type = None # json, data or None

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, None, data_type)

        # Parsing
        data = json.loads(req.text)
        player_profile = PlayerProfile()
        if not "displayPlayer" in data:
            return False, player_profile

        player_profile._scrape(data["displayPlayer"])
        player_profile.id = player_id

        return True, player_profile

    async def get_played_games(self, as_client: httpx.AsyncClient, player_id: str, page_token: str="") -> Tuple[bool, str, PlayedGames]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/games/v1whitelisted/players/{player_id}/applications/played"
        data_type = None # json, data or None

        params = {}
        if page_token:
            params = {"pageToken": page_token}

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)
        played_games = PlayedGames()
        if not "items" in data:
            print(req)
            print(req.text)
            return False, "", played_games

        next_page_token = data.get("nextPageToken", "")

        played_games._scrape(data["items"])

        return True, next_page_token, played_games

    async def get_achievements(self, as_client: httpx.AsyncClient, player_id: str, page_token: str="") -> Tuple[bool, str, PlayerAchievements]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "POST"
        base_url = f"/games/v1whitelisted/players/{player_id}/achievements"
        data_type = "json" # json, data or None

        params = {
            "state": "UNLOCKED",
            "returnDefinitions": True,
            "sortOrder": "RECENT_FIRST"
        }

        data = {}

        if page_token:
            params["pageToken"] = page_token

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, data, data_type)

        # Parsing
        data = json.loads(req.text)
        achievements = PlayerAchievements()
        if not "items" in data:
            print(req)
            print(req.text)
            return False, "", achievements
        
        next_page_token = ""
        if "nextPageToken" in data:
            next_page_token = data["nextPageToken"]

        achievements._scrape(data)

        return True, next_page_token, achievements


File: ghunt/apis/playgateway.py

from ghunt.objects.apis import GAPI
from ghunt.objects.base import GHuntCreds
from ghunt import globals as gb
from ghunt.protos.playgatewaypa.search_player_pb2 import PlayerSearchProto
from ghunt.protos.playgatewaypa.search_player_results_pb2 import PlayerSearchResultsProto
from ghunt.protos.playgatewaypa.get_player_pb2 import GetPlayerProto
from ghunt.protos.playgatewaypa.get_player_response_pb2 import GetPlayerResponseProto
from ghunt.parsers.playgateway import PlayerSearchResults
from ghunt.parsers.playgateway import PlayerProfile

import httpx

from typing import *
from struct import pack
import inspect


class PlayGatewayPaGrpc(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()

        # Android OAuth fields
        self.api_name = "playgames"
        self.package_name = "com.google.android.play.games"
        self.scopes = [
            "https://www.googleapis.com/auth/games.firstparty",
            "https://www.googleapis.com/auth/googleplay"
        ]

        if not headers:
            headers = gb.config.android_headers
            headers["User-Agent"] = headers["User-Agent"].format(self.package_name)

        headers = {**headers, **{
            "Content-Type": "application/grpc",
            "Te": "trailers"
        }}

        # Normal fields

        self.hostname = "playgateway-pa.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = "oauth" # sapisidhash, cookies_only, oauth or None
        self.require_key = None # key name, or None

        self._load_api(creds, headers)

    async def search_player(self, as_client: httpx.AsyncClient, query: str) -> PlayerSearchResults:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "POST"
        base_url = "/play.gateway.adapter.interplay.v1.PlayGatewayInterplayService/GetPage"
        data_type = "data"

        ext_metadata = {
            "bin": {
                "158709649": "CggaBgj22K2aARo4EgoI+aKnlZf996E/GhcQHhoPUkQyQS4yMTEwMDEuMDAyIgIxMToICgZJZ0pHVWdCB1BpeGVsIDU",
                "173715354": "CgEx"
            }
        }

        player_search = PlayerSearchProto()
        player_search.search_form.query.text = query
        payload = player_search.SerializeToString()

        prefix = bytes(1) + pack(">i", len(payload))
        data = prefix + payload

        self._load_endpoint(endpoint_name, {}, ext_metadata)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, data, data_type)

        # Parsing
        player_search_results = PlayerSearchResultsProto()
        player_search_results.ParseFromString(req.content[5:])

        parser = PlayerSearchResults()
        parser._scrape(player_search_results)

        return parser

    async def get_player_stats(self, as_client: httpx.AsyncClient, player_id: str) -> PlayerProfile:
        """
            This endpoint client isn't finished, it is only used to get total played applications & achievements count.
            To get all the details about a player, please use get_player method of PlayGames (HTTP API).
        """

        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "POST"
        base_url = "/play.gateway.adapter.interplay.v1.PlayGatewayInterplayService/GetPage"
        data_type = "data"

        ext_metadata = {
            "bin": {
                "158709649": "CggaBgj22K2aARo4EgoI+aKnlZf996E/GhcQHhoPUkQyQS4yMTEwMDEuMDAyIgIxMToICgZJZ0pHVWdCB1BpeGVsIDU",
                "173715354": "CgEx"
            }
        }

        player_profile = GetPlayerProto()
        player_profile.form.query.id = player_id
        payload = player_profile.SerializeToString()

        prefix = bytes(1) + pack(">i", len(payload))
        data = prefix + payload

        self._load_endpoint(endpoint_name, {}, ext_metadata)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, data, data_type)

        # Parsing
        player_profile = GetPlayerResponseProto()
        player_profile.ParseFromString(req.content[5:])

        parser = PlayerProfile()
        parser._scrape(player_profile)

        return parser


File: ghunt/apis/vision.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.vision import VisionFaceDetection

import httpx

from typing import *
import inspect
import json

        
class VisionHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers
        
        base_headers = {
            "X-Origin": "https://explorer.apis.google.com"
        }

        headers = {**headers, **base_headers}

        self.hostname = "content-vision.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = None # sapisidhash, cookies_only, oauth or None
        self.require_key = "apis_explorer" # key name, or None
        self.key_origin = "https://content-vision.googleapis.com"

        self._load_api(creds, headers)

    async def detect_faces(self, as_client: httpx.AsyncClient, image_url: str = "", image_content: str = "",
                            params_template="default") -> Tuple[bool, bool, VisionFaceDetection]:
        endpoint_name = inspect.currentframe().f_code.co_name

        # image_url can cause errors with vision_api, so we prefer using image_content
        # See => https://cloud.google.com/vision/docs/detecting-faces?#detect_faces_in_a_remote_image

        verb = "POST"
        base_url = "/v1/images:annotate"
        data_type = "json" # json, data or None
        params_templates = {
            "default": {
                "requests":[
                    {
                        "features": [
                            {
                                "maxResults":100,
                                "type":"FACE_DETECTION"
                            }
                        ],
                        "image": {}
                    }
                ]
            }
        }

        if not params_templates.get(params_template):
            raise GHuntParamsTemplateError(f"The asked template {params_template} for the endpoint {endpoint_name} wasn't recognized by GHunt.")

        # Inputs checks
        if image_url and image_content:
            raise GHuntParamsInputError("[Vision API faces detection] image_url and image_content can't be both put at the same time.")
        elif not image_url and not image_content:
            raise GHuntParamsInputError("[Vision API faces detection] Please choose at least one parameter between image_url and image_content.")

        if image_url:
            params_templates["default"]["requests"][0]["image"] = {
                "source": {
                    "imageUri": image_url
                }
            }
        elif image_content:
            params_templates["default"]["requests"][0]["image"] = {
                "content": image_content
            }

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, params_templates[params_template], data_type)
        rate_limited = req.status_code == 429 # API Explorer sometimes rate-limit because they set their DefaultRequestsPerMinutePerProject to 1800

        vision_face_detection = VisionFaceDetection()
        if rate_limited:
            return rate_limited, False, vision_face_detection

        # Parsing
        data = json.loads(req.text)
        if not data["responses"][0]:
            return rate_limited, False, vision_face_detection
        
        vision_data = data["responses"][0]
        vision_face_detection._scrape(vision_data)

        return rate_limited, True, vision_face_detection


File: ghunt/apis/accounts.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI

import httpx

from typing import *
import inspect


class Accounts(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        # Android OAuth fields
        self.api_name = "chrome"
        self.package_name = "com.android.chrome"
        self.scopes = [
            "https://www.google.com/accounts/OAuthLogin"
        ]
        
        self.hostname = "accounts.google.com"
        self.scheme = "https"

        self.authentication_mode = "oauth" # sapisidhash, cookies_only, oauth or None
        self.require_key = None # key name, or None

        self._load_api(creds, headers)

    async def OAuthLogin(self, as_client: httpx.AsyncClient) -> str:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/OAuthLogin"
        data_type = None # json, data or None

        params = {
            "source": "ChromiumBrowser",
            "issueuberauth": 1
        }

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        uber_auth = req.text

        return True, uber_auth


File: ghunt/apis/__init__.py




File: ghunt/apis/clientauthconfig.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.clientauthconfig import CacBrand

import httpx

from typing import *
import inspect
import json


class ClientAuthConfigHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        self.hostname = "clientauthconfig.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = None # sapisidhash, cookies_only, oauth or None
        self.require_key = "pantheon" # key name, or None

        self._load_api(creds, headers)

    async def get_brand(self, as_client: httpx.AsyncClient, project_number: int) -> Tuple[bool, CacBrand]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/v1/brands/lookupkey/brand/{project_number}"
        data_type = None # json, data or None

        params = {
            "readMask": "*",
            "$outputDefaults": True
        }

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)

        brand = CacBrand()
        if "error" in data:
            return False, brand
        
        brand._scrape(data)

        return True, brand


File: ghunt/apis/calendar.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.calendar import Calendar, CalendarEvents

import httpx

from typing import *
import inspect
import json
from datetime import datetime, timezone


class CalendarHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        self.hostname = "clients6.google.com"
        self.scheme = "https"

        self.authentication_mode = "sapisidhash" # sapisidhash, cookies_only, oauth or None
        self.require_key = "calendar" # key name, or None

        self._load_api(creds, headers)

    async def get_calendar(self, as_client: httpx.AsyncClient, calendar_id: str) -> Tuple[bool, Calendar]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/calendar/v3/calendars/{calendar_id}"
        data_type = None # json, data or None

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, None, None, data_type)

        # Parsing
        data = json.loads(req.text)

        calendar = Calendar()
        if "error" in data:
            return False, calendar
        
        calendar._scrape(data)

        return True, calendar

    async def get_events(self, as_client: httpx.AsyncClient, calendar_id: str, params_template="next_events",
                        time_min=datetime.today().replace(tzinfo=timezone.utc).isoformat(), max_results=250, page_token="") -> Tuple[bool, CalendarEvents]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/calendar/v3/calendars/{calendar_id}/events"
        data_type = None # json, data or None
        params_templates = {
            "next_events": {
                "calendarId": calendar_id,
                "singleEvents": True,
                "maxAttendees": 1,
                "maxResults": max_results,
                "timeMin": time_min # ISO Format
            },
            "from_beginning": {
                "calendarId": calendar_id,
                "singleEvents": True,
                "maxAttendees": 1,
                "maxResults": max_results
            },
            "max_from_beginning": {
                "calendarId": calendar_id,
                "singleEvents": True,
                "maxAttendees": 1,
                "maxResults": 2500 # Max
            }
        }

        if not params_templates.get(params_template):
            raise GHuntParamsTemplateError(f"The asked template {params_template} for the endpoint {endpoint_name} wasn't recognized by GHunt.")

        params = params_templates[params_template]
        if page_token:
            params["pageToken"] = page_token

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)

        events = CalendarEvents()
        if not data:
            return False, events
        
        events._scrape(data)

        return True, events


File: ghunt/apis/peoplepa.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.people import Person

import httpx

from typing import *
import inspect
import json


class PeoplePaHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        self.hostname = "people-pa.clients6.google.com"
        self.scheme = "https"

        self.authentication_mode = "sapisidhash" # sapisidhash, cookies_only, oauth or None
        self.require_key = "photos" # key name, or None

        self._load_api(creds, headers)

    async def people_lookup(self, as_client: httpx.AsyncClient, email: str, params_template="just_gaia_id") -> Tuple[bool, Person]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = "/v2/people/lookup"
        data_type = None # json, data or None
        params_templates = {
            "just_gaia_id": {
                "id": email,
                "type": "EMAIL",
                "match_type": "EXACT",
                "request_mask.include_field.paths": "person.metadata",
                "request_mask.include_container": [
                    "PROFILE",
                    "DOMAIN_PROFILE",
                ],
            },
            "just_name": {
                "id": email,
                "type": "EMAIL",
                "match_type": "EXACT",
                "request_mask.include_field.paths": "person.name",
                "request_mask.include_container": [
                    "PROFILE",
                    "DOMAIN_PROFILE",
                ],
                "core_id_params.enable_private_names": True
            },
            "max_details": {
                "id": email,
                "type": "EMAIL",
                "match_type": "EXACT",
                "extension_set.extension_names": [
                    "DYNAMITE_ADDITIONAL_DATA",
                    "DYNAMITE_ORGANIZATION_INFO",
                    # "GPLUS_ADDITIONAL_DATA"
                ],
                "request_mask.include_field.paths": [
                    "person.metadata.best_display_name",
                    "person.photo",
                    "person.cover_photo",
                    "person.interaction_settings",
                    "person.legacy_fields",
                    "person.metadata",
                    # "person.in_app_reachability",
                    "person.name",
                    "person.read_only_profile_info",
                    "person.sort_keys",
                    "person.email"
                ],
                "request_mask.include_container": [
                    "AFFINITY",
                    "PROFILE",
                    "DOMAIN_PROFILE",
                    "ACCOUNT",
                    "EXTERNAL_ACCOUNT",
                    "CIRCLE",
                    "DOMAIN_CONTACT",
                    "DEVICE_CONTACT",
                    "GOOGLE_GROUP",
                    "CONTACT"
                ],
                "core_id_params.enable_private_names": True
            }
        }

        if not params_templates.get(params_template):
            raise GHuntParamsTemplateError(f"The asked template {params_template} for the endpoint {endpoint_name} wasn't recognized by GHunt.")

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params_templates[params_template], None, data_type)

        # Parsing
        data = json.loads(req.text)
        person = Person()
        if not data:
            return False, person
        
        person_data = list(data["people"].values())[0]
        await person._scrape(as_client, person_data)

        return True, person

    async def people(self, as_client: httpx.AsyncClient, gaia_id: str, params_template="just_name") -> Tuple[bool, Person]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = "/v2/people"
        data_type = None # json, data or None
        params_templates = {
            "just_name": {
                "person_id": gaia_id,
                "request_mask.include_field.paths": "person.name",
                "request_mask.include_container": [
                    "PROFILE",
                    "DOMAIN_PROFILE",
                ],
                "core_id_params.enable_private_names": True
            },
            "max_details": {
                "person_id": gaia_id,
                "extension_set.extension_names": [
                    "DYNAMITE_ADDITIONAL_DATA",
                    "DYNAMITE_ORGANIZATION_INFO",
                    # "GPLUS_ADDITIONAL_DATA"
                ],
                "request_mask.include_field.paths": [
                    "person.metadata.best_display_name",
                    "person.photo",
                    "person.cover_photo",
                    "person.interaction_settings",
                    "person.legacy_fields",
                    "person.metadata",
                    # "person.in_app_reachability",
                    "person.name",
                    "person.read_only_profile_info",
                    "person.sort_keys",
                    "person.email"
                ],
                "request_mask.include_container": [
                    "AFFINITY",
                    "PROFILE",
                    "DOMAIN_PROFILE",
                    "ACCOUNT",
                    "EXTERNAL_ACCOUNT",
                    "CIRCLE",
                    "DOMAIN_CONTACT",
                    "DEVICE_CONTACT",
                    "GOOGLE_GROUP",
                    "CONTACT"
                ],
                "core_id_params.enable_private_names": True
            }
        }

        if not params_templates.get(params_template):
            raise GHuntParamsTemplateError(f"The asked template {params_template} for the endpoint {endpoint_name} wasn't recognized by GHunt.")

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params_templates[params_template], None, data_type)

        # Parsing
        data = json.loads(req.text)
        person = Person()
        if data["personResponse"][0]["status"] == "NOT_FOUND":
            return False, person
        
        person_data = data["personResponse"][0]["person"]
        await person._scrape(as_client, person_data)

        return True, person


File: ghunt/apis/drive.py

from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
import ghunt.globals as gb
from ghunt.objects.apis import GAPI
from ghunt.parsers.drive import DriveCommentList, DriveFile, DriveChildList
from ghunt.knowledge import drive as drive_knowledge

import httpx

from typing import *
import inspect
import json


class DriveHttp(GAPI):
    def __init__(self, creds: GHuntCreds, headers: Dict[str, str] = {}):
        super().__init__()
        
        if not headers:
            headers = gb.config.headers

        base_headers = {}

        headers = {**headers, **base_headers}

        # Android OAuth fields
        self.api_name = "drive"
        self.package_name = "com.google.android.apps.docs"
        self.scopes = [
            "https://www.googleapis.com/auth/drive",
            "https://www.googleapis.com/auth/drive.file"
        ]

        self.hostname = "www.googleapis.com"
        self.scheme = "https"

        self.authentication_mode = "oauth" # sapisidhash, cookies_only, oauth or None
        self.require_key = None # key name, or None

        self._load_api(creds, headers)

    async def get_file(self, as_client: httpx.AsyncClient, file_id: str) -> Tuple[bool, DriveFile]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/drive/v2internal/files/{file_id}"
        data_type = None # json, data or None

        params = {
            "fields": ','.join(drive_knowledge.request_fields),
            "supportsAllDrives": True
        }

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)
        drive_file = DriveFile()
        if "error" in data:
            return False, drive_file

        drive_file._scrape(data)

        return True, drive_file

    async def get_comments(self, as_client: httpx.AsyncClient, file_id: str, page_token: str="") -> Tuple[bool, str, DriveCommentList]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/drive/v2internal/files/{file_id}/comments"
        data_type = None # json, data or None

        params = {
            "supportsAllDrives": True,
            "maxResults": 100
        }

        if page_token:
            params["pageToken"] = page_token

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)
        drive_comments = DriveCommentList()
        if "error" in data:
            return False, "", drive_comments

        next_page_token = data.get("nextPageToken", "")

        drive_comments._scrape(data)

        return True, next_page_token, drive_comments

    async def get_childs(self, as_client: httpx.AsyncClient, file_id: str, page_token: str="") -> Tuple[bool, str, DriveChildList]:
        endpoint_name = inspect.currentframe().f_code.co_name

        verb = "GET"
        base_url = f"/drive/v2internal/files/{file_id}/children"
        data_type = None # json, data or None

        params = {
            "supportsAllDrives": True,
            "maxResults": 1000
        }

        if page_token:
            params["pageToken"] = page_token

        self._load_endpoint(endpoint_name)
        req = await self._query(as_client, verb, endpoint_name, base_url, params, None, data_type)

        # Parsing
        data = json.loads(req.text)
        drive_childs = DriveChildList()
        if "error" in data:
            return False, "", drive_childs

        next_page_token = data.get("nextPageToken", "")

        drive_childs._scrape(data)

        return True, next_page_token, drive_childs


File: ghunt/protos/__init__.py




File: ghunt/protos/playgatewaypa/search_player_results_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: definitions/search_player_results.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'definitions/search_player_results.proto\"\xe6\x05\n\x18PlayerSearchResultsProto\x12\x35\n\x06\x66ield1\x18\x01 \x01(\x0b\x32%.PlayerSearchResultsProto.field1_type\x1a\x92\x05\n\x0b\x66ield1_type\x12\x41\n\x07results\x18\xc6\xa2\xe5[ \x01(\x0b\x32-.PlayerSearchResultsProto.field1_type.Results\x1a\xbf\x04\n\x07Results\x12I\n\x06\x66ield1\x18\x01 \x01(\x0b\x32\x39.PlayerSearchResultsProto.field1_type.Results.field1_type\x1a\xe8\x03\n\x0b\x66ield1_type\x12U\n\x06\x66ield1\x18\x01 \x01(\x0b\x32\x45.PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type\x1a\x81\x03\n\x0b\x66ield1_type\x12_\n\x06player\x18\xda\xe7\x8bo \x03(\x0b\x32L.PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player\x1a\x90\x02\n\x06Player\x12\x63\n\x06\x61vatar\x18\x03 \x01(\x0b\x32S.PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Avatar\x12\x65\n\x07\x61\x63\x63ount\x18\x06 \x01(\x0b\x32T.PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Account\x1a\x15\n\x06\x41vatar\x12\x0b\n\x03url\x18\x01 \x01(\t\x1a#\n\x07\x41\x63\x63ount\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\tb\x06proto3')



_PLAYERSEARCHRESULTSPROTO = DESCRIPTOR.message_types_by_name['PlayerSearchResultsProto']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE = _PLAYERSEARCHRESULTSPROTO.nested_types_by_name['field1_type']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE.nested_types_by_name['Results']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS.nested_types_by_name['field1_type']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE.nested_types_by_name['field1_type']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE.nested_types_by_name['Player']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_AVATAR = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER.nested_types_by_name['Avatar']
_PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_ACCOUNT = _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER.nested_types_by_name['Account']
PlayerSearchResultsProto = _reflection.GeneratedProtocolMessageType('PlayerSearchResultsProto', (_message.Message,), {

  'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

    'Results' : _reflection.GeneratedProtocolMessageType('Results', (_message.Message,), {

      'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

        'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

          'Player' : _reflection.GeneratedProtocolMessageType('Player', (_message.Message,), {

            'Avatar' : _reflection.GeneratedProtocolMessageType('Avatar', (_message.Message,), {
              'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_AVATAR,
              '__module__' : 'definitions.search_player_results_pb2'
              # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Avatar)
              })
            ,

            'Account' : _reflection.GeneratedProtocolMessageType('Account', (_message.Message,), {
              'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_ACCOUNT,
              '__module__' : 'definitions.search_player_results_pb2'
              # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Account)
              })
            ,
            'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER,
            '__module__' : 'definitions.search_player_results_pb2'
            # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player)
            })
          ,
          'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE,
          '__module__' : 'definitions.search_player_results_pb2'
          # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type)
          })
        ,
        'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE,
        '__module__' : 'definitions.search_player_results_pb2'
        # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results.field1_type)
        })
      ,
      'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS,
      '__module__' : 'definitions.search_player_results_pb2'
      # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type.Results)
      })
    ,
    'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE,
    '__module__' : 'definitions.search_player_results_pb2'
    # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto.field1_type)
    })
  ,
  'DESCRIPTOR' : _PLAYERSEARCHRESULTSPROTO,
  '__module__' : 'definitions.search_player_results_pb2'
  # @@protoc_insertion_point(class_scope:PlayerSearchResultsProto)
  })
_sym_db.RegisterMessage(PlayerSearchResultsProto)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results.field1_type)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Avatar)
_sym_db.RegisterMessage(PlayerSearchResultsProto.field1_type.Results.field1_type.field1_type.Player.Account)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _PLAYERSEARCHRESULTSPROTO._serialized_start=44
  _PLAYERSEARCHRESULTSPROTO._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE._serialized_start=128
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS._serialized_start=211
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE._serialized_start=298
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE._serialized_start=401
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER._serialized_start=514
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER._serialized_end=786
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_AVATAR._serialized_start=728
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_AVATAR._serialized_end=749
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_ACCOUNT._serialized_start=751
  _PLAYERSEARCHRESULTSPROTO_FIELD1_TYPE_RESULTS_FIELD1_TYPE_FIELD1_TYPE_PLAYER_ACCOUNT._serialized_end=786
# @@protoc_insertion_point(module_scope)



File: ghunt/protos/playgatewaypa/__init__.py




File: ghunt/protos/playgatewaypa/get_player_response_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: definitions/get_player_response.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%definitions/get_player_response.proto\"\xa4\"\n\x16GetPlayerResponseProto\x12\x33\n\x06\x66ield1\x18\x01 \x01(\x0b\x32#.GetPlayerResponseProto.field1_type\x1a\xd4!\n\x0b\x66ield1_type\x12@\n\x07results\x18\x85\xf2\xc3\x90\x01 \x01(\x0b\x32+.GetPlayerResponseProto.field1_type.Results\x1a\x82!\n\x07Results\x12\x42\n\x06player\x18\x01 \x01(\x0b\x32\x32.GetPlayerResponseProto.field1_type.Results.Player\x12\x44\n\x07section\x18\x02 \x03(\x0b\x32\x33.GetPlayerResponseProto.field1_type.Results.Section\x1a\xd7\x01\n\x06Player\x12N\n\x06\x66ield1\x18\x01 \x01(\x0b\x32>.GetPlayerResponseProto.field1_type.Results.Player.field1_type\x12I\n\x06\x61vatar\x18\x02 \x01(\x0b\x32\x39.GetPlayerResponseProto.field1_type.Results.Player.Avatar\x1a\x1b\n\x0b\x66ield1_type\x12\x0c\n\x04name\x18\x01 \x01(\t\x1a\x15\n\x06\x41vatar\x12\x0b\n\x03url\x18\x01 \x01(\t\x1a\x92\x1e\n\x07Section\x12L\n\x07\x63ounter\x18\x02 \x01(\x0b\x32;.GetPlayerResponseProto.field1_type.Results.Section.Counter\x12O\n\x06\x66ield3\x18\x03 \x01(\x0b\x32?.GetPlayerResponseProto.field1_type.Results.Section.field3_type\x12U\n\x0cplayed_games\x18\x05 \x01(\x0b\x32?.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames\x12V\n\x0c\x61\x63hievements\x18\x06 \x01(\x0b\x32@.GetPlayerResponseProto.field1_type.Results.Section.Achievements\x1a\x19\n\x07\x43ounter\x12\x0e\n\x06number\x18\x01 \x01(\t\x1a#\n\x0b\x66ield3_type\x12\x14\n\x0csection_name\x18\x01 \x01(\t\x1a\xe1\r\n\x0bPlayedGames\x12[\n\x06\x66ield1\x18\x01 \x01(\x0b\x32K.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type\x1a\xf4\x0c\n\x0b\x66ield1_type\x12g\n\x06\x66ield1\x18\x01 \x01(\x0b\x32W.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type\x12\x61\n\x04game\x18\xea\xbd\x9e\x63 \x03(\x0b\x32P.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game\x12z\n\x0e\x66ield234686954\x18\xea\x93\xf4o \x01(\x0b\x32_.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type\x1a\xbd\x01\n\x0b\x66ield1_type\x12\x86\x01\n\x0e\x66ield203130867\x18\xf3\x8f\xee` \x01(\x0b\x32k.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type.field203130867_type\x1a%\n\x13\x66ield203130867_type\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x1a\xfe\x05\n\x04Game\x12\x63\n\x04icon\x18\x03 \x01(\x0b\x32U.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.Icon\x12l\n\x06\x66ield6\x18\x06 \x01(\x0b\x32\\.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type\x1a\x13\n\x04Icon\x12\x0b\n\x03url\x18\x01 \x01(\t\x1a\x8d\x04\n\x0b\x66ield6_type\x12x\n\x06\x66ield1\x18\x01 \x01(\x0b\x32h.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type\x1a\x83\x03\n\x0b\x66ield1_type\x12\x84\x01\n\x07\x64\x65tails\x18\xb6\xaa\xa8^ \x01(\x0b\x32p.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details\x1a\xec\x01\n\x07\x44\x65tails\x12\x8c\x01\n\x06\x66ield1\x18\x01 \x01(\x0b\x32|.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details.field1_type\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0e\n\x06\x65\x64itor\x18\x03 \x01(\t\x1a\x33\n\x0b\x66ield1_type\x12\x0e\n\x06\x61pp_id\x18\x01 \x01(\t\x12\x14\n\x0cpackage_name\x18\x02 \x01(\t\x1a\xdb\x02\n\x13\x66ield234686954_type\x12{\n\x06\x66ield1\x18\x01 \x01(\x0b\x32k.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type\x1a\xc6\x01\n\x0b\x66ield1_type\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12\x87\x01\n\x06\x66ield2\x18\x02 \x01(\x0b\x32w.GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type.field2_type\x1a\x1d\n\x0b\x66ield2_type\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\x03\x1a\x94\r\n\x0c\x41\x63hievements\x12\\\n\x06\x66ield1\x18\x01 \x01(\x0b\x32L.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type\x1a\xa5\x0c\n\x0b\x66ield1_type\x12h\n\x06\x66ield3\x18\x03 \x01(\x0b\x32X.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type\x12\x0e\n\x06\x66ield4\x18\x04 \x01(\x03\x1a\x9b\x0b\n\x0b\x66ield3_type\x12t\n\x06\x66ield1\x18\x01 \x01(\x0b\x32\x64.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type\x12t\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x64.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type\x1a\xbe\x06\n\x0b\x66ield1_type\x12\x89\x01\n\x0b\x61\x63hievement\x18\xb8\x81\xf8\x90\x01 \x03(\x0b\x32p.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement\x1a\xa2\x05\n\x0b\x41\x63hievement\x12\x8c\x01\n\x06\x66ield7\x18\x07 \x01(\x0b\x32|.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type\x1a\x83\x04\n\x0b\x66ield7_type\x12\x99\x01\n\x06\x66ield1\x18\x01 \x01(\x0b\x32\x88\x01.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type\x1a\xd7\x02\n\x0b\x66ield1_type\x12\xa6\x01\n\x07\x64\x65tails\x18\xf2\xa2\x86\x92\x01 \x01(\x0b\x32\x90\x01.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type.Details\x1a\x9e\x01\n\x07\x44\x65tails\x12\r\n\x05title\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\n\n\x02xp\x18\x03 \x01(\x03\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x10\n\x08icon_url\x18\x05 \x01(\t\x12\x0f\n\x07game_id\x18\x06 \x01(\t\x12\x11\n\ttimestamp\x18\x07 \x01(\x03\x12\x0e\n\x06\x66ield8\x18\x08 \x01(\x03\x12\x11\n\tis_secret\x18\x0c \x01(\x03\x1a\xde\x02\n\x0b\x66ield2_type\x12\x80\x01\n\x06\x66ield1\x18\x01 \x01(\x0b\x32p.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type\x1a\xcb\x01\n\x0b\x66ield1_type\x12\x87\x01\n\x04page\x18\xcf\xa7\xd3\x90\x01 \x01(\x0b\x32u.GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type.Page\x1a\x32\n\x04Page\x12\x11\n\tplayer_id\x18\x01 \x01(\t\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\tb\x06proto3')



_GETPLAYERRESPONSEPROTO = DESCRIPTOR.message_types_by_name['GetPlayerResponseProto']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE.nested_types_by_name['Results']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS.nested_types_by_name['Player']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_AVATAR = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER.nested_types_by_name['Avatar']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS.nested_types_by_name['Section']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_COUNTER = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION.nested_types_by_name['Counter']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_FIELD3_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION.nested_types_by_name['field3_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION.nested_types_by_name['PlayedGames']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE_FIELD203130867_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE.nested_types_by_name['field203130867_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE.nested_types_by_name['Game']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_ICON = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME.nested_types_by_name['Icon']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME.nested_types_by_name['field6_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE.nested_types_by_name['Details']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE.nested_types_by_name['field234686954_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE_FIELD2_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE.nested_types_by_name['field2_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION.nested_types_by_name['Achievements']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE.nested_types_by_name['field3_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE.nested_types_by_name['Achievement']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT.nested_types_by_name['field7_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE_DETAILS = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE.nested_types_by_name['Details']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE.nested_types_by_name['field2_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE.nested_types_by_name['field1_type']
_GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE_PAGE = _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE.nested_types_by_name['Page']
GetPlayerResponseProto = _reflection.GeneratedProtocolMessageType('GetPlayerResponseProto', (_message.Message,), {

  'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

    'Results' : _reflection.GeneratedProtocolMessageType('Results', (_message.Message,), {

      'Player' : _reflection.GeneratedProtocolMessageType('Player', (_message.Message,), {

        'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_FIELD1_TYPE,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Player.field1_type)
          })
        ,

        'Avatar' : _reflection.GeneratedProtocolMessageType('Avatar', (_message.Message,), {
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_AVATAR,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Player.Avatar)
          })
        ,
        'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER,
        '__module__' : 'definitions.get_player_response_pb2'
        # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Player)
        })
      ,

      'Section' : _reflection.GeneratedProtocolMessageType('Section', (_message.Message,), {

        'Counter' : _reflection.GeneratedProtocolMessageType('Counter', (_message.Message,), {
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_COUNTER,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Counter)
          })
        ,

        'field3_type' : _reflection.GeneratedProtocolMessageType('field3_type', (_message.Message,), {
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_FIELD3_TYPE,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.field3_type)
          })
        ,

        'PlayedGames' : _reflection.GeneratedProtocolMessageType('PlayedGames', (_message.Message,), {

          'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

            'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

              'field203130867_type' : _reflection.GeneratedProtocolMessageType('field203130867_type', (_message.Message,), {
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE_FIELD203130867_TYPE,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type.field203130867_type)
                })
              ,
              'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE,
              '__module__' : 'definitions.get_player_response_pb2'
              # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type)
              })
            ,

            'Game' : _reflection.GeneratedProtocolMessageType('Game', (_message.Message,), {

              'Icon' : _reflection.GeneratedProtocolMessageType('Icon', (_message.Message,), {
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_ICON,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.Icon)
                })
              ,

              'field6_type' : _reflection.GeneratedProtocolMessageType('field6_type', (_message.Message,), {

                'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

                  'Details' : _reflection.GeneratedProtocolMessageType('Details', (_message.Message,), {

                    'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {
                      'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS_FIELD1_TYPE,
                      '__module__' : 'definitions.get_player_response_pb2'
                      # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details.field1_type)
                      })
                    ,
                    'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS,
                    '__module__' : 'definitions.get_player_response_pb2'
                    # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details)
                    })
                  ,
                  'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE,
                  '__module__' : 'definitions.get_player_response_pb2'
                  # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type)
                  })
                ,
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type)
                })
              ,
              'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME,
              '__module__' : 'definitions.get_player_response_pb2'
              # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game)
              })
            ,

            'field234686954_type' : _reflection.GeneratedProtocolMessageType('field234686954_type', (_message.Message,), {

              'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

                'field2_type' : _reflection.GeneratedProtocolMessageType('field2_type', (_message.Message,), {
                  'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE_FIELD2_TYPE,
                  '__module__' : 'definitions.get_player_response_pb2'
                  # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type.field2_type)
                  })
                ,
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type)
                })
              ,
              'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE,
              '__module__' : 'definitions.get_player_response_pb2'
              # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type)
              })
            ,
            'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE,
            '__module__' : 'definitions.get_player_response_pb2'
            # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type)
            })
          ,
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.PlayedGames)
          })
        ,

        'Achievements' : _reflection.GeneratedProtocolMessageType('Achievements', (_message.Message,), {

          'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

            'field3_type' : _reflection.GeneratedProtocolMessageType('field3_type', (_message.Message,), {

              'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

                'Achievement' : _reflection.GeneratedProtocolMessageType('Achievement', (_message.Message,), {

                  'field7_type' : _reflection.GeneratedProtocolMessageType('field7_type', (_message.Message,), {

                    'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

                      'Details' : _reflection.GeneratedProtocolMessageType('Details', (_message.Message,), {
                        'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE_DETAILS,
                        '__module__' : 'definitions.get_player_response_pb2'
                        # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type.Details)
                        })
                      ,
                      'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE,
                      '__module__' : 'definitions.get_player_response_pb2'
                      # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type)
                      })
                    ,
                    'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE,
                    '__module__' : 'definitions.get_player_response_pb2'
                    # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type)
                    })
                  ,
                  'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT,
                  '__module__' : 'definitions.get_player_response_pb2'
                  # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement)
                  })
                ,
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type)
                })
              ,

              'field2_type' : _reflection.GeneratedProtocolMessageType('field2_type', (_message.Message,), {

                'field1_type' : _reflection.GeneratedProtocolMessageType('field1_type', (_message.Message,), {

                  'Page' : _reflection.GeneratedProtocolMessageType('Page', (_message.Message,), {
                    'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE_PAGE,
                    '__module__' : 'definitions.get_player_response_pb2'
                    # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type.Page)
                    })
                  ,
                  'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE,
                  '__module__' : 'definitions.get_player_response_pb2'
                  # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type)
                  })
                ,
                'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE,
                '__module__' : 'definitions.get_player_response_pb2'
                # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type)
                })
              ,
              'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE,
              '__module__' : 'definitions.get_player_response_pb2'
              # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type)
              })
            ,
            'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE,
            '__module__' : 'definitions.get_player_response_pb2'
            # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type)
            })
          ,
          'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS,
          '__module__' : 'definitions.get_player_response_pb2'
          # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section.Achievements)
          })
        ,
        'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION,
        '__module__' : 'definitions.get_player_response_pb2'
        # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results.Section)
        })
      ,
      'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS,
      '__module__' : 'definitions.get_player_response_pb2'
      # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type.Results)
      })
    ,
    'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO_FIELD1_TYPE,
    '__module__' : 'definitions.get_player_response_pb2'
    # @@protoc_insertion_point(class_scope:GetPlayerResponseProto.field1_type)
    })
  ,
  'DESCRIPTOR' : _GETPLAYERRESPONSEPROTO,
  '__module__' : 'definitions.get_player_response_pb2'
  # @@protoc_insertion_point(class_scope:GetPlayerResponseProto)
  })
_sym_db.RegisterMessage(GetPlayerResponseProto)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Player)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Player.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Player.Avatar)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Counter)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.field3_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field1_type.field203130867_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.Icon)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.Game.field6_type.field1_type.Details.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.PlayedGames.field1_type.field234686954_type.field1_type.field2_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field1_type.Achievement.field7_type.field1_type.Details)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type)
_sym_db.RegisterMessage(GetPlayerResponseProto.field1_type.Results.Section.Achievements.field1_type.field3_type.field2_type.field1_type.Page)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _GETPLAYERRESPONSEPROTO._serialized_start=42
  _GETPLAYERRESPONSEPROTO._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE._serialized_start=122
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS._serialized_start=204
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER._serialized_start=354
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER._serialized_end=569
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_FIELD1_TYPE._serialized_start=519
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_FIELD1_TYPE._serialized_end=546
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_AVATAR._serialized_start=548
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_PLAYER_AVATAR._serialized_end=569
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION._serialized_start=572
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_COUNTER._serialized_start=917
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_COUNTER._serialized_end=942
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_FIELD3_TYPE._serialized_start=944
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_FIELD3_TYPE._serialized_end=979
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES._serialized_start=982
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES._serialized_end=2743
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE._serialized_start=1091
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE._serialized_end=2743
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE._serialized_start=1435
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE._serialized_end=1624
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE_FIELD203130867_TYPE._serialized_start=1587
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD1_TYPE_FIELD203130867_TYPE._serialized_end=1624
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME._serialized_start=1627
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME._serialized_end=2393
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_ICON._serialized_start=1846
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_ICON._serialized_end=1865
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE._serialized_start=1868
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE._serialized_end=2393
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE._serialized_start=2006
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE._serialized_end=2393
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS._serialized_start=2157
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS._serialized_end=2393
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS_FIELD1_TYPE._serialized_start=2342
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_GAME_FIELD6_TYPE_FIELD1_TYPE_DETAILS_FIELD1_TYPE._serialized_end=2393
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE._serialized_start=2396
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE._serialized_end=2743
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE._serialized_start=2545
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE._serialized_end=2743
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE_FIELD2_TYPE._serialized_start=2714
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_PLAYEDGAMES_FIELD1_TYPE_FIELD234686954_TYPE_FIELD1_TYPE_FIELD2_TYPE._serialized_end=2743
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS._serialized_start=2746
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE._serialized_start=2857
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE._serialized_start=2995
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE._serialized_start=3247
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE._serialized_end=4077
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT._serialized_start=3403
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT._serialized_end=4077
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE._serialized_start=3562
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE._serialized_end=4077
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE._serialized_start=3734
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE._serialized_end=4077
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE_DETAILS._serialized_start=3919
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD1_TYPE_ACHIEVEMENT_FIELD7_TYPE_FIELD1_TYPE_DETAILS._serialized_end=4077
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE._serialized_start=4080
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE._serialized_start=4227
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE._serialized_end=4430
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE_PAGE._serialized_start=4380
  _GETPLAYERRESPONSEPROTO_FIELD1_TYPE_RESULTS_SECTION_ACHIEVEMENTS_FIELD1_TYPE_FIELD3_TYPE_FIELD2_TYPE_FIELD1_TYPE_PAGE._serialized_end=4430
# @@protoc_insertion_point(module_scope)



File: ghunt/protos/playgatewaypa/get_player_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: definitions/get_player.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x64\x65\x66initions/get_player.proto\"\x80\x01\n\x0eGetPlayerProto\x12\"\n\x04\x66orm\x18\x02 \x01(\x0b\x32\x14.GetPlayerProto.Form\x1aJ\n\x04\x46orm\x12-\n\x05query\x18\x85\xf2\xc3\x90\x01 \x01(\x0b\x32\x1a.GetPlayerProto.Form.Query\x1a\x13\n\x05Query\x12\n\n\x02id\x18\x01 \x01(\tb\x06proto3')



_GETPLAYERPROTO = DESCRIPTOR.message_types_by_name['GetPlayerProto']
_GETPLAYERPROTO_FORM = _GETPLAYERPROTO.nested_types_by_name['Form']
_GETPLAYERPROTO_FORM_QUERY = _GETPLAYERPROTO_FORM.nested_types_by_name['Query']
GetPlayerProto = _reflection.GeneratedProtocolMessageType('GetPlayerProto', (_message.Message,), {

  'Form' : _reflection.GeneratedProtocolMessageType('Form', (_message.Message,), {

    'Query' : _reflection.GeneratedProtocolMessageType('Query', (_message.Message,), {
      'DESCRIPTOR' : _GETPLAYERPROTO_FORM_QUERY,
      '__module__' : 'definitions.get_player_pb2'
      # @@protoc_insertion_point(class_scope:GetPlayerProto.Form.Query)
      })
    ,
    'DESCRIPTOR' : _GETPLAYERPROTO_FORM,
    '__module__' : 'definitions.get_player_pb2'
    # @@protoc_insertion_point(class_scope:GetPlayerProto.Form)
    })
  ,
  'DESCRIPTOR' : _GETPLAYERPROTO,
  '__module__' : 'definitions.get_player_pb2'
  # @@protoc_insertion_point(class_scope:GetPlayerProto)
  })
_sym_db.RegisterMessage(GetPlayerProto)
_sym_db.RegisterMessage(GetPlayerProto.Form)
_sym_db.RegisterMessage(GetPlayerProto.Form.Query)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _GETPLAYERPROTO._serialized_start=33
  _GETPLAYERPROTO._serialized_end=161
  _GETPLAYERPROTO_FORM._serialized_start=87
  _GETPLAYERPROTO_FORM._serialized_end=161
  _GETPLAYERPROTO_FORM_QUERY._serialized_start=142
  _GETPLAYERPROTO_FORM_QUERY._serialized_end=161
# @@protoc_insertion_point(module_scope)



File: ghunt/protos/playgatewaypa/search_player_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: definitions/search_player.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x64\x65\x66initions/search_player.proto\"\xa3\x01\n\x11PlayerSearchProto\x12\x32\n\x0bsearch_form\x18\x02 \x01(\x0b\x32\x1d.PlayerSearchProto.SearchForm\x1aZ\n\nSearchForm\x12\x35\n\x05query\x18\x9c\xe7\xa7o \x01(\x0b\x32#.PlayerSearchProto.SearchForm.Query\x1a\x15\n\x05Query\x12\x0c\n\x04text\x18\x01 \x01(\tb\x06proto3')



_PLAYERSEARCHPROTO = DESCRIPTOR.message_types_by_name['PlayerSearchProto']
_PLAYERSEARCHPROTO_SEARCHFORM = _PLAYERSEARCHPROTO.nested_types_by_name['SearchForm']
_PLAYERSEARCHPROTO_SEARCHFORM_QUERY = _PLAYERSEARCHPROTO_SEARCHFORM.nested_types_by_name['Query']
PlayerSearchProto = _reflection.GeneratedProtocolMessageType('PlayerSearchProto', (_message.Message,), {

  'SearchForm' : _reflection.GeneratedProtocolMessageType('SearchForm', (_message.Message,), {

    'Query' : _reflection.GeneratedProtocolMessageType('Query', (_message.Message,), {
      'DESCRIPTOR' : _PLAYERSEARCHPROTO_SEARCHFORM_QUERY,
      '__module__' : 'definitions.search_player_pb2'
      # @@protoc_insertion_point(class_scope:PlayerSearchProto.SearchForm.Query)
      })
    ,
    'DESCRIPTOR' : _PLAYERSEARCHPROTO_SEARCHFORM,
    '__module__' : 'definitions.search_player_pb2'
    # @@protoc_insertion_point(class_scope:PlayerSearchProto.SearchForm)
    })
  ,
  'DESCRIPTOR' : _PLAYERSEARCHPROTO,
  '__module__' : 'definitions.search_player_pb2'
  # @@protoc_insertion_point(class_scope:PlayerSearchProto)
  })
_sym_db.RegisterMessage(PlayerSearchProto)
_sym_db.RegisterMessage(PlayerSearchProto.SearchForm)
_sym_db.RegisterMessage(PlayerSearchProto.SearchForm.Query)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _PLAYERSEARCHPROTO._serialized_start=36
  _PLAYERSEARCHPROTO._serialized_end=199
  _PLAYERSEARCHPROTO_SEARCHFORM._serialized_start=109
  _PLAYERSEARCHPROTO_SEARCHFORM._serialized_end=199
  _PLAYERSEARCHPROTO_SEARCHFORM_QUERY._serialized_start=178
  _PLAYERSEARCHPROTO_SEARCHFORM_QUERY._serialized_end=199
# @@protoc_insertion_point(module_scope)



File: ghunt/objects/__init__.py




File: ghunt/objects/encoders.py

import json
from datetime import datetime


class GHuntEncoder(json.JSONEncoder):
    """
        Converts non-default types when exporting to JSON.
    """
    def default(self, o: object) -> dict:
        if isinstance(o, set):
            return list(o)
        elif isinstance(o, datetime):
            return f"{o.strftime('%Y/%m/%d %H:%M:%S')} (UTC)"
        elif type(o) not in [str, list, int, float, bool, dict]:
            if hasattr(o, "__dict__"):
                return o.__dict__
            else:
                return {x:getattr(o, x) for x in o.__slots__}


File: ghunt/objects/apis.py

from ghunt.errors import GHuntCorruptedHeadersError
from ghunt.helpers.knowledge import get_origin_of_key, get_api_key
from ghunt.objects.base import GHuntCreds, SmartObj
from ghunt.helpers.utils import *
from ghunt.errors import *
from ghunt.helpers.auth import *

import httpx

from datetime import datetime, timezone
from typing import *
import asyncio


# APIs objects

class EndpointConfig(SmartObj):
    def __init__(self, headers: Dict[str, str], cookies: str):
        self.headers = headers
        self.cookies = cookies

class GAPI(SmartObj):
    def __init__(self):
        self.loaded_endpoints: Dict[str, EndpointConfig] = {}
        self.creds: GHuntCreds = None
        self.headers: Dict[str, str] = {}
        self.cookies: Dict[str, str] = {}
        self.gen_token_lock: asyncio.Semaphore = None

        self.authentication_mode: str = ""
        self.require_key: str = ""
        self.key_origin: str = ""

    def _load_api(self, creds: GHuntCreds, headers: Dict[str, str]):
        if not creds.are_creds_loaded():
            raise GHuntInsufficientCreds(f"This API requires a loaded GHuntCreds object, but it is not.")

        if not is_headers_syntax_good(headers):
            raise GHuntCorruptedHeadersError(f"The provided headers when loading the endpoint seems corrupted, please check it : {headers}")

        if self.authentication_mode == "oauth":
            self.gen_token_lock = asyncio.Semaphore(1)

        cookies = {}
        if self.authentication_mode in ["sapisidhash", "cookies_only"]:
            if not (cookies := creds.cookies):
                raise GHuntInsufficientCreds(f"This endpoint requires the cookies in the GHuntCreds object, but they aren't loaded.")

        if (key_name := self.require_key):
            if not (api_key := get_api_key(key_name)):
                raise GHuntInsufficientCreds(f"This API requires the {key_name} API key in the GHuntCreds object, but it isn't loaded.")
            if not self.key_origin:
                self.key_origin = get_origin_of_key(key_name)
            headers = {**headers, "X-Goog-Api-Key": api_key, **headers, "Origin": self.key_origin, "Referer": self.key_origin}

        if self.authentication_mode == "sapisidhash":
            if not (sapisidhash := creds.cookies.get("SAPISID")):
                raise GHuntInsufficientCreds(f"This endpoint requires the SAPISID cookie in the GHuntCreds object, but it isn't loaded.")

            headers = {**headers, "Authorization": f"SAPISIDHASH {gen_sapisidhash(sapisidhash, self.key_origin)}"}

        self.creds = creds
        self.headers = headers
        self.cookies = cookies

    def _load_endpoint(self, endpoint_name: str,
                        headers: Dict[str, str]={}, ext_metadata: Dict[str, str]={}):
        if endpoint_name in self.loaded_endpoints:
            return

        headers = {**headers, **self.headers}

        # https://github.com/googleapis/googleapis/blob/f8a290120b3a67e652742a221f73778626dc3081/google/api/context.proto#L43
        for ext_type,ext_value in ext_metadata.items():
            ext_bin_headers = {f"X-Goog-Ext-{k}-{ext_type.title()}":v for k,v in ext_value.items()}
            headers = {**headers, **ext_bin_headers}
        
        if not is_headers_syntax_good(headers):
            raise GHuntCorruptedHeadersError(f"The provided headers when loading the endpoint seems corrupted, please check it : {headers}")

        self.loaded_endpoints[endpoint_name] = EndpointConfig(headers, self.cookies)

    async def _check_and_gen_authorization_token(self, as_client: httpx.AsyncClient, creds: GHuntCreds):
        async with self.gen_token_lock:
            present = False
            if self.api_name in creds.android.authorization_tokens:
                present = True
                token = creds.android.authorization_tokens[self.api_name]["token"]
                expiry_date = datetime.utcfromtimestamp(creds.android.authorization_tokens[self.api_name]["expiry"]).replace(tzinfo=timezone.utc)

            # If there are no registered authorization token for the current API, or if the token has expired
            if (not self.api_name in creds.android.authorization_tokens) or (present and datetime.now(timezone.utc) > expiry_date):
                token, _, expiry_timestamp = await android_oauth_app(as_client, creds.android.master_token, self.package_name, self.scopes)
                creds.android.authorization_tokens[self.api_name] = {
                    "token": token,
                    "expiry": expiry_timestamp
                }
                creds.save_creds(silent=True)
                gb.rc.print(f"\n[+] New token for {self.api_name} has been generated", style="italic")
            return token

    async def _query(self, as_client: httpx.AsyncClient, verb: str, endpoint_name: str, base_url: str, params: Dict[str, Any], data: Any, data_type: str) -> httpx.Response:
        endpoint = self.loaded_endpoints[endpoint_name]
        headers = endpoint.headers
        if self.authentication_mode == "oauth":
            token = await self._check_and_gen_authorization_token(as_client, self.creds)
            headers = {**headers, "Authorization": f"OAuth {token}"}

        if verb == "GET":
            req = await as_client.get(f"{self.scheme}://{self.hostname}{base_url}",
                params=params, headers=headers, cookies=endpoint.cookies)
        elif verb == "POST":
            if data_type == "data":
                req = await as_client.post(f"{self.scheme}://{self.hostname}{base_url}",
                    params=params, data=data, headers=headers, cookies=endpoint.cookies)
            elif data_type == "json":
                req = await as_client.post(f"{self.scheme}://{self.hostname}{base_url}",
                    params=params, json=data, headers=headers, cookies=endpoint.cookies)
            else:
                raise GHuntUnknownRequestDataTypeError(f"The provided data type {data_type} wasn't recognized by GHunt.")
        else:
            raise GHuntUnknownVerbError(f"The provided verb {verb} wasn't recognized by GHunt.")

        return req

# Others

class Parser(SmartObj):
    def _merge(self, obj) -> any:
        """Merging two objects of the same class."""

        def recursive_merge(obj1, obj2, module_name: str) -> any:
            directions = [(obj1, obj2), (obj2, obj1)]
            for direction in directions:
                from_obj, target_obj = direction
                for attr_name, attr_value in from_obj.__dict__.items():
                    class_name = get_class_name(attr_value)
                    if class_name.startswith(module_name) and attr_name in target_obj.__dict__:
                        merged_obj = recursive_merge(attr_value, target_obj.__dict__[attr_name], module_name)
                        target_obj.__dict__[attr_name] = merged_obj

                    elif not attr_name in target_obj.__dict__ or \
                        (attr_value and not target_obj.__dict__.get(attr_name)):
                        target_obj.__dict__[attr_name] = attr_value
            return obj1

        class_name = get_class_name(self)
        module_name = self.__module__
        if not get_class_name(obj).startswith(class_name):
            raise GHuntObjectsMergingError("The two objects being merged aren't from the same class.")

        self = recursive_merge(self, obj, module_name)


File: ghunt/objects/utils.py

from ghunt.helpers.utils import *
from ghunt.errors import *
from ghunt.objects.base import SmartObj

from typing import *


class TMPrinter(SmartObj):
    def __init__(self):
        self.max_len = 0

    def out(self, text: str):
        if len(text) > self.max_len:
            self.max_len = len(text)
        else:
            text += (" " * (self.max_len - len(text)))
        print(text, end='\r')

    def clear(self):
    	print(" " * self.max_len, end="\r")


File: ghunt/objects/base.py

from typing import *
from pathlib import Path
import json
from dateutil.relativedelta import relativedelta
from datetime import datetime
import base64

from autoslot import Slots

from ghunt.errors import GHuntInvalidSession


class SmartObj(Slots):
    pass

class AndroidCreds(SmartObj):
    def __init__(self) -> None:
        self.master_token: str = ""
        self.authorization_tokens: Dict = {}

class GHuntCreds(SmartObj):
    """
        This object stores all the needed credentials that GHunt uses,
        such as cookies, OSIDs, keys and tokens.
    """
    
    def __init__(self, creds_path: str = "") -> None:
        self.cookies: Dict[str, str] = {}
        self.osids: Dict[str, str] = {}
        self.android: AndroidCreds = AndroidCreds()

        if not creds_path:
            cwd_path = Path().home()
            ghunt_folder = cwd_path / ".malfrats/ghunt"
            if not ghunt_folder.is_dir():
                ghunt_folder.mkdir(parents=True, exist_ok=True)
            creds_path = ghunt_folder / "creds.m"
        self.creds_path: str = creds_path

    def are_creds_loaded(self) -> bool:
        return all([self.cookies, self.osids, self.android.master_token])

    def load_creds(self, silent=False) -> None:
        """Loads cookies, OSIDs and tokens if they exist"""
        if Path(self.creds_path).is_file():
            try:
                with open(self.creds_path, "r", encoding="utf-8") as f:
                    raw = f.read()
                data = json.loads(base64.b64decode(raw).decode())

                self.cookies = data["cookies"]
                self.osids = data["osids"]

                self.android.master_token = data["android"]["master_token"]
                self.android.authorization_tokens = data["android"]["authorization_tokens"]

            except Exception:
                raise GHuntInvalidSession("Stored session is corrupted.")
        else:
            raise GHuntInvalidSession("No stored session found.")
        
        if not self.are_creds_loaded():
            raise GHuntInvalidSession("Stored session is incomplete.")
        if not silent:
            print("[+] Stored session loaded !")

    def save_creds(self, silent=False):
        """Save cookies, OSIDs and tokens to the specified file."""
        data = {
            "cookies": self.cookies,
            "osids": self.osids,
            "android": {
                "master_token": self.android.master_token,
                "authorization_tokens": self.android.authorization_tokens
            }
        }

        with open(self.creds_path, "w", encoding="utf-8") as f:
            f.write(base64.b64encode(json.dumps(data, indent=2).encode()).decode())

        if not silent:
            print(f"\n[+] Creds have been saved in {self.creds_path} !")

### Maps

class Position(SmartObj):
    def __init__(self):
        self.latitude: float = 0.0
        self.longitude: float = 0.0

class MapsLocation(SmartObj):
    def __init__(self):
        self.id: str = ""
        self.name: str = ""
        self.address: str = ""
        self.position: Position = Position()
        self.tags: List[str] = []
        self.types: List[str] = []
        self.cost_level: int = 0 # 1-4

class MapsReview(SmartObj):
    def __init__(self):
        self.id: str = ""
        self.comment: str = ""
        self.rating: int = 0
        self.location: MapsLocation = MapsLocation()
        self.date: datetime = None

class MapsPhoto(SmartObj):
    def __init__(self):
        self.id: str = ""
        self.url: str = ""
        self.location: MapsLocation = MapsLocation()
        self.date: datetime = None

### Drive
class DriveExtractedUser(SmartObj):
    def __init__(self):
        self.gaia_id: str = ""
        self.name: str = ""
        self.email_address: str = ""
        self.role: str = ""
        self.is_last_modifying_user: bool = False


File: ghunt/knowledge/maps.py

types_translations = {
    'airport': 'Airport',
    'atm': 'ATM',
    'bar': 'Bar',
    'bank_intl': 'Bank',
    'bus': 'Bus',
    'cafe': 'Café',
    'camping': 'Camping',
    'cemetery': 'Cemetery',
    'civic_bldg': 'Civic building',
    'ferry': 'Ferry',
    'gas': 'Gas',
    'generic': 'Generic',
    'golf': 'Golf',
    'hospital_H': 'Hospital H',
    'library': 'Library',
    'lodging': 'Lodging',
    'monument': 'Monument',
    'movie': 'Movie',
    'museum': 'Museum',
    'parking': 'Parking',
    'police': 'Police',
    'postoffice': 'Post office',
    'restaurant': 'Restaurant',
    'school': 'School',
    'shoppingbag': 'Shopping bag',
    'shoppingcart': 'Shopping cart',
    'train': 'Train',
    'tram': 'Tram',
    'tree': 'Park',
    'worship_buddhist': 'Worship Buddhist',
    'worship_christian': 'Worship Christian',
    'worship_hindu': 'Worship Hindu',
    'worship_islam': 'Worship Islam',
    'worship_jewish': 'Worship Jewish',
    'worship_sikh': 'Worship Sikh',
    'worship_jain': 'Worship Jain'
}



File: ghunt/knowledge/services.py

services_baseurls = {
    "cloudconsole": "console.cloud.google.com",
    "cl": "calendar.google.com"
}


File: ghunt/knowledge/sig.py

sigs = {
    "com.google.android.play.games": "38918a453d07199354f8b19af05ec6562ced5788",
    "com.google.android.apps.docs": "38918a453d07199354f8b19af05ec6562ced5788",
    "com.google.android.youtube": "24bb24c05e47e0aefa68a58a766179d9b613a600",
    "com.android.chrome": "38918a453d07199354f8b19af05ec6562ced5788"
}


File: ghunt/knowledge/__init__.py




File: ghunt/knowledge/keys.py

keys = {
    "pantheon": {"key": "AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g", "origin": "https://console.cloud.google.com"},
    "photos": {"key": "AIzaSyAa2odBewW-sPJu3jMORr0aNedh3YlkiQc", "origin": "https://photos.google.com"},
    "apis_explorer": {"key": "AIzaSyAa8yy0GdcGPHdtD083HiGGx_S0vMPScDM", "origin": "https://explorer.apis.google.com"},
    "calendar": {"key": "AIzaSyBNlYH01_9Hc5S1J9vuFmu2nUqBZJNAXxs", "origin": "https://calendar.google.com"},
    "youtubei": {"key": "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w", "origin": "https://youtube.com"},
    "drive": {"key": "AIzaSyD_InbmSFufIEps5UAt2NmB_3LvBH3Sz_8", "origin": "https://drive.google.com"},
    "geolocation": {"key": "AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg", "origin": "https://geo-devrel-javascript-samples.web.app"} # https://developers.google.com/maps/documentation/javascript/overview
}


File: ghunt/knowledge/people.py

# https://developers.google.com/people/api/rest/v1/people#usertype
user_types = {
    "USER_TYPE_UNKNOWN": "The user type is not known.", # Official
    "GOOGLE_USER": "The user is a Google user.", # Official
    "GPLUS_USER": "The user is a Currents user.", # Official
    "GOOGLE_APPS_USER": "The user is a Google Workspace user.", # Official
    "OWNER_USER_TYPE_UNKNOWN": "The user type is not known.", # Guess
    "GPLUS_DISABLED_BY_ADMIN": "This user's Currents account has been disabled by an admin.", # Guess
    "GOOGLE_APPS_USER": "The user is a Google Apps user.", # Guess
    "GOOGLE_FAMILY_USER": "The user is a Google Family user.", # Guess
    "GOOGLE_FAMILY_CHILD_USER": "The user is a Google Family child user.", # Guess
    "GOOGLE_APPS_ADMIN_DISABLED": "This admin of Google Apps has been disabled.", # Guess
    "GOOGLE_ONE_USER": "The user is a Google One user.", # Guess
    "GOOGLE_FAMILY_CONVERTED_CHILD_USER": "This Google Family user was converted to a child user." # Guess
}


File: ghunt/knowledge/drive.py

default_file_capabilities = [
    'can_block_owner',
    'can_copy',
    'can_download',
    'can_print',
    'can_read',
    'can_remove_my_drive_parent'
]

default_folder_capabilities = [
    'can_block_owner',
    'can_download',
    'can_list_children',
    'can_print',
    'can_read',
    'can_remove_my_drive_parent'
]

request_fields = [
    'copyRequiresWriterPermission',
    'sourceAppId',
    'authorizedAppIds',
    'linkShareMetadata',
    'teamDriveId',
    'primaryDomainName',
    'approvalMetadata',
    'md5Checksum',
    'resourceKey',
    'quotaBytesUsed',
    'hasChildFolders',
    'fullFileExtension',
    'isAppAuthorized',
    'iconLink',
    'trashingUser',
    'title',
    'recency',
    'detectors',
    'exportLinks',
    'modifiedDate',
    'copyable',
    'description',
    'mimeType',
    'passivelySubscribed',
    'videoMediaMetadata',
    'headRevisionId',
    'customerId',
    'fileExtension',
    'originalFilename',
    'parents',
    'imageMediaMetadata',
    'recencyReason',
    'folderColorRgb',
    'createdDate',
    'labels',
    'abuseNoticeReason',
    'webViewLink',
    'driveId',
    'ownedByMe',
    'flaggedForAbuse',
    'lastModifyingUser',
    'thumbnailLink',
    'capabilities',
    'sharedWithMeDate',
    'primarySyncParentId',
    'sharingUser',
    'version',
    'permissionsSummary',
    'actionItems',
    'labelInfo',
    'explicitlyTrashed',
    'shared',
    'subscribed',
    'ancestorHasAugmentedPermissions',
    'writersCanShare',
    'permissions',
    'alternateLink',
    'hasLegacyBlobComments',
    'id',
    'userPermission',
    'hasThumbnail',
    'lastViewedByMeDate',
    'fileSize',
    'kind',
    'thumbnailVersion',
    'spaces',
    'organizationDisplayName',
    'abuseIsAppealable',
    'trashedDate',
    'folderFeatures',
    'webContentLink',
    'contentRestrictions',
    'shortcutDetails',
    'folderColor',
    'hasAugmentedPermissions'
]

mime_types = {
    "application/vnd.google-apps.audio": "Audio 🎧",
    "application/vnd.google-apps.document":	"Google Docs 📝",
    "application/vnd.google-apps.drive-sdk": "3rd party shortcut ↪️",
    "application/vnd.google-apps.drawing": "Google Drawing ✏️",
    "application/vnd.google-apps.file":	"Google Drive file 📄",
    "application/vnd.google-apps.folder": "Google Drive folder 🗂️",
    "application/vnd.google-apps.form":	"Google Forms 👨‍🏫",
    "application/vnd.google-apps.fusiontable": "Google Fusion Tables 🌶️",
    "application/vnd.google-apps.jam": "Google Jamboard 🖍️",
    "application/vnd.google-apps.map": "Google My Maps 📍",
    "application/vnd.google-apps.photo": "Photo 📷",
    "application/vnd.google-apps.presentation":	"Google Slides ❇️",
    "application/vnd.google-apps.script": "Google Apps Scripts 📜",
    "application/vnd.google-apps.shortcut":	"Shortcut ↩️",
    "application/vnd.google-apps.site":	"Google Sites 🌐",
    "application/vnd.google-apps.spreadsheet": "Google Sheets 📟",
    "application/vnd.google-apps.unknown": "Unknown ❔",
    "application/vnd.google-apps.video": "Video 📼",
    "application/pdf": "PDF Document 📕",
    "application/msword": "Microsoft Word document 📝",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "OpenXML Word document 📝",
    "application/vnd.ms-powerpoint.presentation.macroEnabled.12": "Microsoft Powerpoint with macros ❇️",
    "application/vnd.ms-excel": "Microsoft Excel spreadsheet 📟",
    "image/jpeg": "JPEG Image 🖼️",
    "audio/mpeg": "MPEG Audio 🎧",
    "video/mpeg": "MPEG Video 📼",
    "application/zip": "ZIP Archive 🗃️",
    "text/plain": "Plain Text 📃",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "OpenXML Spreadsheet document ❇️",
    "application/vnd.android.package-archive": "Android Package 📱",
    "application/vnd.google-apps.kix": "Google Apps 🈸"
}


File: ghunt/lib/httpx.py

import httpx

class AsyncClient(httpx.AsyncClient):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def _merge_cookies(self, cookies: dict):
        """Don't save the cookies in the client."""
        return cookies


File: ghunt/lib/__init__.py




File: ghunt/modules/__init__.py




File: ghunt/modules/gaia.py

from ghunt import globals as gb
from ghunt.objects.base import GHuntCreds
from ghunt.apis.peoplepa import PeoplePaHttp
from ghunt.apis.vision import VisionHttp
from ghunt.helpers import gmaps, auth, ia
from ghunt.helpers.knowledge import get_user_type_definition
from ghunt.helpers.utils import get_httpx_client

import httpx

from typing import *
from pathlib import Path


async def hunt(as_client: httpx.AsyncClient, gaia_id: str, json_file: Path=None):
    if not as_client:
        as_client = get_httpx_client()

    ghunt_creds = await auth.load_and_auth(as_client)

    #gb.rc.print("\n[+] Target found !", style="spring_green3")

    people_pa = PeoplePaHttp(ghunt_creds)
    # vision_api = VisionHttp(ghunt_creds)
    is_found, target = await people_pa.people(as_client, gaia_id, params_template="max_details")
    if not is_found:
        exit("[-] The target wasn't found.")

    if json_file:
        json_results = {}

    containers = target.sourceIds

    if len(containers) > 1 or not "PROFILE" in containers:
        print("[!] You have this person in these containers :")
        for container in containers:
            print(f"- {container.title()}")

    if not "PROFILE" in containers:
        exit("[-] Given information does not match a public Google Account.")

    container = "PROFILE"
    
    gb.rc.print("🙋 Google Account data\n", style="plum2")

    # if container in target.names:
        # print(f"Name : {target.names[container].fullname}\n")

    if container in target.profilePhotos:
        if target.profilePhotos[container].isDefault:
            print("[-] Default profile picture")
        else:
            print("[+] Custom profile picture !")
            print(f"=> {target.profilePhotos[container].url}")
            
            # await ia.detect_face(vision_api, as_client, target.profilePhotos[container].url)
            print()

    if container in target.coverPhotos:
        if target.coverPhotos[container].isDefault:
            print("[-] Default cover picture\n")
        else:
            print("[+] Custom cover picture !")
            print(f"=> {target.coverPhotos[container].url}")

            # await ia.detect_face(vision_api, as_client, target.coverPhotos[container].url)
            print()

    print(f"Last profile edit : {target.sourceIds[container].lastUpdated.strftime('%Y/%m/%d %H:%M:%S (UTC)')}\n")

    print(f"Gaia ID : {target.personId}\n")

    if container in target.profileInfos:
        print("User types :")
        for user_type in target.profileInfos[container].userTypes:
            definition = get_user_type_definition(user_type)
            gb.rc.print(f"- {user_type} [italic]({definition})[/italic]")

    gb.rc.print(f"\n📞 Google Chat Extended Data\n", style="light_salmon3")

    #print(f"Presence : {target.extendedData.dynamiteData.presence}")
    print(f"Entity Type : {target.extendedData.dynamiteData.entityType}")
    #print(f"DND State : {target.extendedData.dynamiteData.dndState}")
    gb.rc.print(f"Customer ID : {x if (x := target.extendedData.dynamiteData.customerId) else '[italic]Not found.[/italic]'}")

    gb.rc.print(f"\n🌐 Google Plus Extended Data\n", style="cyan")

    print(f"Entreprise User : {target.extendedData.gplusData.isEntrepriseUser}")
    #print(f"Content Restriction : {target.extendedData.gplusData.contentRestriction}")
    
    if container in target.inAppReachability:
        print("\n[+] Activated Google services :")
        for app in target.inAppReachability[container].apps:
            print(f"- {app}")

    gb.rc.print("\n🗺️ Maps data", style="green4")

    err, stats, reviews, photos = await gmaps.get_reviews(as_client, target.personId)
    gmaps.output(err, stats, reviews, photos, target.personId)

    if json_file:
        if container == "PROFILE":
            json_results[f"{container}_CONTAINER"] = {
                "profile": target,
                "maps": {
                    "photos": photos,
                    "reviews": reviews,
                    "stats": stats
                }
            }
        else:
            json_results[f"{container}_CONTAINER"] = {
                "profile": target
            }
    
    if json_file:
        import json
        from ghunt.objects.encoders import GHuntEncoder;
        with open(json_file, "w", encoding="utf-8") as f:
            f.write(json.dumps(json_results, cls=GHuntEncoder, indent=4))
        gb.rc.print(f"\n[+] JSON output wrote to {json_file} !", style="italic")

    await as_client.aclose()


File: ghunt/modules/email.py

from ghunt import globals as gb
from ghunt.helpers.utils import get_httpx_client
from ghunt.objects.base import GHuntCreds
from ghunt.apis.peoplepa import PeoplePaHttp
from ghunt.apis.vision import VisionHttp
from ghunt.helpers import gmaps, playgames, auth, calendar as gcalendar, ia
from ghunt.helpers.knowledge import get_user_type_definition

import httpx

from typing import *
from pathlib import Path


async def hunt(as_client: httpx.AsyncClient, email_address: str, json_file: Path=None):
    if not as_client:
        as_client = get_httpx_client()
 
    ghunt_creds = await auth.load_and_auth(as_client)

    #gb.rc.print("[+] Target found !", style="sea_green3")

    people_pa = PeoplePaHttp(ghunt_creds)
    # vision_api = VisionHttp(ghunt_creds)
    is_found, target = await people_pa.people_lookup(as_client, email_address, params_template="max_details")
    if not is_found:
        exit("[-] The target wasn't found.")

    if json_file:
        json_results = {}

    containers = target.sourceIds

    if len(containers) > 1 or not "PROFILE" in containers:
        print("[!] You have this person in these containers :")
        for container in containers:
            print(f"- {container.title()}")

    if not "PROFILE" in containers:
        exit("[-] Given information does not match a public Google Account.")

    container = "PROFILE"
    
    gb.rc.print("🙋 Google Account data\n", style="plum2")

    # if container in target.names:
        # print(f"Name : {target.names[container].fullname}\n")

    if container in target.profilePhotos:
        if target.profilePhotos[container].isDefault:
            print("[-] Default profile picture")
        else:
            print("[+] Custom profile picture !")
            print(f"=> {target.profilePhotos[container].url}")
            
            # await ia.detect_face(vision_api, as_client, target.profilePhotos[container].url)
            print()

    if container in target.coverPhotos:
        if target.coverPhotos[container].isDefault:
            print("[-] Default cover picture\n")
        else:
            print("[+] Custom cover picture !")
            print(f"=> {target.coverPhotos[container].url}")

            # await ia.detect_face(vision_api, as_client, target.coverPhotos[container].url)
            print()

    print(f"Last profile edit : {target.sourceIds[container].lastUpdated.strftime('%Y/%m/%d %H:%M:%S (UTC)')}\n")
    
    if container in target.emails:
        print(f"Email : {target.emails[container].value}")
    else:
        print(f"Email : {email_address}\n")

    print(f"Gaia ID : {target.personId}")

    if container in target.profileInfos:
        print("\nUser types :")
        for user_type in target.profileInfos[container].userTypes:
            definition = get_user_type_definition(user_type)
            gb.rc.print(f"- {user_type} [italic]({definition})[/italic]")

    gb.rc.print(f"\n📞 Google Chat Extended Data\n", style="light_salmon3")

    #print(f"Presence : {target.extendedData.dynamiteData.presence}")
    print(f"Entity Type : {target.extendedData.dynamiteData.entityType}")
    #print(f"DND State : {target.extendedData.dynamiteData.dndState}")
    gb.rc.print(f"Customer ID : {x if (x := target.extendedData.dynamiteData.customerId) else '[italic]Not found.[/italic]'}")

    gb.rc.print(f"\n🌐 Google Plus Extended Data\n", style="cyan")

    print(f"Entreprise User : {target.extendedData.gplusData.isEntrepriseUser}")
    #print(f"Content Restriction : {target.extendedData.gplusData.contentRestriction}")
    
    if container in target.inAppReachability:
        print("\n[+] Activated Google services :")
        for app in target.inAppReachability[container].apps:
            print(f"- {app}")

    gb.rc.print("\n🎮 Play Games data", style="deep_pink2")

    player_results = await playgames.search_player(ghunt_creds, as_client, email_address)
    if player_results:
        player_candidate = player_results[0]
        print("\n[+] Found player profile !")
        print(f"\nUsername : {player_candidate.name}")
        print(f"Player ID : {player_candidate.id}")
        print(f"Avatar : {player_candidate.avatar_url}")
        _, player = await playgames.get_player(ghunt_creds, as_client, player_candidate.id)
        playgames.output(player)
    else:
        print("\n[-] No player profile found.")

    gb.rc.print("\n🗺️ Maps data", style="green4")

    err, stats, reviews, photos = await gmaps.get_reviews(as_client, target.personId)
    gmaps.output(err, stats, reviews, photos, target.personId)

    gb.rc.print("\n🗓️ Calendar data\n", style="slate_blue3")

    cal_found, calendar, calendar_events = await gcalendar.fetch_all(ghunt_creds, as_client, email_address)

    if cal_found:
        print("[+] Public Google Calendar found !\n")
        if calendar_events.items:
            if "PROFILE" in target.names:
                gcalendar.out(calendar, calendar_events, email_address, target.names[container].fullname)
            else:
                gcalendar.out(calendar, calendar_events, email_address)
        else:
            print("=> No recent events found.")
    else:
        print("[-] No public Google Calendar.")

    if json_file:
        if container == "PROFILE":
            json_results[f"{container}_CONTAINER"] = {
                "profile": target,
                "play_games": player if player_results else None,
                "maps": {
                    "photos": photos,
                    "reviews": reviews,
                    "stats": stats
                },
                "calendar": {
                    "details": calendar,
                    "events": calendar_events
                } if cal_found else None
            }
        else:
            json_results[f"{container}_CONTAINER"] = {
                "profile": target
            }

    if json_file:
        import json
        from ghunt.objects.encoders import GHuntEncoder;
        with open(json_file, "w", encoding="utf-8") as f:
            f.write(json.dumps(json_results, cls=GHuntEncoder, indent=4))
        gb.rc.print(f"\n[+] JSON output wrote to {json_file} !", style="italic")

    await as_client.aclose()


File: ghunt/modules/login.py

from typing import *

import httpx
from pathlib import Path

from ghunt import globals as gb
from ghunt.helpers.utils import *
from ghunt.helpers import auth
from ghunt.objects.base import GHuntCreds
from ghunt.errors import GHuntInvalidSession


async def check_and_login(as_client: httpx.AsyncClient, clean: bool=False) -> None:
    """Check the users credentials validity, and generate new ones."""

    ghunt_creds = GHuntCreds()

    if clean:
        creds_path = Path(ghunt_creds.creds_path)
        if creds_path.is_file():
            creds_path.unlink()
            print(f"[+] Credentials file at {creds_path} deleted !")
        else:
            print(f"Credentials file at {creds_path} doesn't exists, no need to delete.")
        exit()

    if not as_client:
        as_client = get_httpx_client()

    is_session_valid = True
    try:
        ghunt_creds = await auth.load_and_auth(as_client, help=False)
    except GHuntInvalidSession as e:
        print(f"[-] {e}\n")
        is_session_valid = False

    if not is_session_valid:
        oauth_token, master_token = auth.auth_dialog()
    else:
        # in case user wants to enter new creds (example: for new account)
        is_master_token_valid = await auth.check_master_token(as_client, ghunt_creds.android.master_token)
        if is_master_token_valid:
            print("[+] The master token seems valid !")
        else:
            print("[-] Seems like the master token is invalid.")

        cookies_valid = await auth.check_cookies(as_client, ghunt_creds.cookies)
        if cookies_valid:
            print("[+] The cookies seem valid !")
        else:
            print("[-] Seems like the cookies are invalid.")

        osids_valid = await auth.check_osids(as_client, ghunt_creds.cookies, ghunt_creds.osids)
        if osids_valid:
            print("[+] OSIDs seem valid !")
        else:
            print("[-] Seems like OSIDs are invalid.")
        new_gen_inp = input("\nDo you want to create a new session ? (Y/n) ").lower()
        if new_gen_inp == "y":
            oauth_token, master_token = auth.auth_dialog()
        else:
            exit()

    ghunt_creds.android.authorization_tokens = {} # Reset the authorization tokens

    if oauth_token:
        print(f"\n[+] Got OAuth2 token => {oauth_token}")
        master_token, services, owner_email, owner_name = await auth.android_master_auth(as_client, oauth_token)

        print("\n[Connected account]")
        print(f"Name : {owner_name}")
        print(f"Email : {owner_email}")
        gb.rc.print("\n🔑 [underline]A master token has been generated for your account and saved in the credentials file[/underline], please keep it safe as if it were your password, because it gives access to a lot of Google services, and with that, your personal information.", style="bold")
        print(f"Master token services access : {', '.join(services)}")

    # Feed the GHuntCreds object
    ghunt_creds.android.master_token = master_token

    ghunt_creds.cookies = {"a": "a"} # Dummy ata
    ghunt_creds.osids = {"a": "a"} # Dummy data

    print("Generating cookies and osids...")
    await auth.gen_cookies_and_osids(as_client, ghunt_creds)
    print("[+] Cookies and osids generated !")

    ghunt_creds.save_creds()

    await as_client.aclose()



File: ghunt/modules/drive.py

from ghunt.helpers.utils import *
from ghunt.objects.base import DriveExtractedUser, GHuntCreds
from ghunt.apis.drive import DriveHttp
from ghunt.apis.clientauthconfig import ClientAuthConfigHttp
from ghunt import globals as gb
from ghunt.helpers import auth
from ghunt.helpers.drive import get_comments_from_file, get_users_from_file
from ghunt.knowledge import drive as drive_knownledge

import httpx
import inflection
import humanize

import inspect
from typing import *
from datetime import timedelta


def show_user(user: DriveExtractedUser):
    if user.name:
        print(f"Full name : {user.name}")
    else:
        gb.rc.print("Full name : -", style="bright_black")
    print(f"Email : {user.email_address}")
    if user.gaia_id:
        print(f"Gaia ID : {user.gaia_id}")
    else:
        gb.rc.print("Gaia ID : -", style="bright_black")
    if user.is_last_modifying_user:
        print("[+] Last user to have modified the document !")

async def hunt(as_client: httpx.AsyncClient, file_id: str, json_file: bool=Path):
    if not as_client:
        as_client = get_httpx_client()

    ghunt_creds = await auth.load_and_auth(as_client)

    drive = DriveHttp(ghunt_creds)
    file_found, file = await drive.get_file(as_client, file_id)
    if not file_found:
        exit("[-] The file wasn't found.")

    is_folder = file.mime_type == "application/vnd.google-apps.folder"
    file_type = drive_knownledge.mime_types.get(file.mime_type)

    #gb.rc.print(f"[+] {'Folder' if is_folder else 'File'} found !", style="sea_green3")

    gb.rc.print("🗃️ Drive properties\n", style="deep_pink4")
        
    print(f"Title : {file.title}")
    print(f"{'Folder' if is_folder else 'File'} ID : {file.id}")
    if file.md5_checksum:
        print(f"MD5 Checksum : {file.md5_checksum}")
    if file.file_size:
        print(f"{'Folder' if is_folder else 'File'} size : {humanize.naturalsize(file.file_size)}")

    if file_type:
        gb.rc.print(f"\nType : {file_type} [italic]\[{file.mime_type}][/italic]")
    else:
        print(f"\nMime Type : {file.mime_type}")
    if is_folder:
        print(f"Folder link :\n=> {file.alternate_link}")
    else:
        print(f"Download link :\n=> {file.alternate_link}")

    print(f"\n[+] Created date : {file.created_date.strftime('%Y/%m/%d %H:%M:%S (UTC)')}")
    print(f"[+] Modified date : {file.modified_date.strftime('%Y/%m/%d %H:%M:%S (UTC)')}")

    for perm in file.permissions:
        if perm.id == "anyoneWithLink":
            giving_roles = [perm.role.upper()] + [x.upper() for x in perm.additional_roles if x != perm.role]
            print(f"\n[+] Sharing with link enabled ! Giving the role{'s' if len(giving_roles) > 1 else ''} {humanize_list(giving_roles)}.")

    #print("\n[Source application]")
    gb.rc.print("\n📱 Source application\n", style="deep_pink2")
    brand_found = False
    brand = None
    if file.source_app_id:
        print(f"App ID : {file.source_app_id}")
        cac = ClientAuthConfigHttp(ghunt_creds)
        brand_found, brand = await cac.get_brand(as_client, file.source_app_id)
        if brand_found:
            print(f"Name : {brand.display_name}")
            if brand.home_page_url:
                print(f"Home page : {brand.home_page_url}")
            else:
                gb.rc.print(f"Home page : [italic][bright_black]Not found.[/italic][/bright_black]")
        else:
            gb.rc.print("Not found.", style="italic")
    else:
        gb.rc.print("No source application.", style="italic")

    if file.image_media_metadata.height and file.image_media_metadata.width:
        #print("\n[Image metadata]")
        gb.rc.print("\n📸 Image metadata\n", style="light_coral")
        print(f"Height : {file.image_media_metadata.height}")
        print(f"Width : {file.image_media_metadata.width}")
        if isinstance((data := file.image_media_metadata.rotation), int):
            print(f"Rotation : {data}")

    if file.video_media_metadata.height and file.video_media_metadata.width:
        #print("\n[Video metadata]")
        gb.rc.print("\n📸 Video metadata\n", style="light_coral")
        print(f"Height : {file.video_media_metadata.height}")
        print(f"Width : {file.video_media_metadata.width}")
        if (data := file.video_media_metadata.duration_millis):
            duration = timedelta(milliseconds=int(file.video_media_metadata.duration_millis))
            print(f"Duration : {humanize.precisedelta(duration)}")

    #print("\n[Parents]")
    gb.rc.print("\n📂 Parents\n", style="gold3")
    if file.parents:
        print(f"[+] Parents folders :")
        for parent in file.parents:
            print(f"- 📁 {parent.id}{' [Root folder]' if parent.is_root else ''}")
    else:
        gb.rc.print("No parent folder found.", style="italic")

    if is_folder:
        #print("\n[Items]")
        gb.rc.print("\n🗃️ Items\n", style="gold3")
        found, _, drive_childs = await drive.get_childs(as_client, file_id)
        if found and drive_childs.items:
            count = f"{x if (x := len(drive_childs.items)) < 1000 else '>= 1000'}"
            print(f"[+] {count} items found inside this folder !")
        else:
            gb.rc.print("No items found.", style="italic")

    #print("\n[Users]")
    gb.rc.print("\n👪 Users\n", style="dark_orange")
    users = get_users_from_file(file)
    if (owners := [x for x in users if x.role == "owner"]):
        print(f"-> 👤 Owner{'s' if len(owners) > 1 else ''}")
        for user in owners:
            show_user(user)

    if (writers := [x for x in users if x.role == "writer"]):
        print(f"\n-> 👤 Writer{'s' if len(writers) > 1 else ''}")
        for user in writers:
            show_user(user)

    if (commenters := [x for x in users if x.role == "commenter"]):
        print(f"\n-> 👤 Commenter{'s' if len(commenters) > 1 else ''}")
        for user in commenters:
            show_user(user)

    if (readers := [x for x in users if x.role == "reader"]):
        print(f"\n-> 👤 Reader{'s' if len(readers) > 1 else ''}")
        for user in readers:
            show_user(user)

    if (nones := [x for x in users if x.role == "none"]):
        print(f"\n-> 👤 User{'s' if len(nones) > 1 else ''} with no right")
        for user in nones:
            show_user(user)

    #print("[Comments]")
    gb.rc.print("\n🗣️ Comments\n", style="plum2")
    comments_found, _, drive_comments = await drive.get_comments(as_client, file_id)
    if comments_found and drive_comments.items:
        authors = get_comments_from_file(drive_comments)
        if len(drive_comments.items) > 20:
            print(f"[+] Authors ({len(authors)} found, showing the top 20) :")
        else:
            print("[+] Authors :")
        for _, author in authors[:20]:
            print(f"- 🙋 {author['name']} ({author['count']} comment{'s' if author['count'] > 1 else ''})")
    else:
        gb.rc.print("No comments.", style="italic")

    #print("\n[Capabilities]")
    gb.rc.print("\n🧙 Capabilities\n", style="dodger_blue1")
    capabilities = sorted([k for k,v in inspect.getmembers(file.capabilities) if v and not k.startswith("_")])
    if is_folder:
        if capabilities == drive_knownledge.default_folder_capabilities:
            print("[-] You don't have special permissions against this folder.")
        else:
            print(f"[+] You have special permissions against this folder ! ✨")
            for cap in capabilities:
                print(f"- {inflection.humanize(cap)}")
    else:
        if capabilities == drive_knownledge.default_file_capabilities:
            print("[-] You don't have special permissions against this file.")
        else:
            print(f"[+] You have special permissions against this file ! ✨")
            for cap in capabilities:
                print(f"- {inflection.humanize(cap)}")

    if json_file:
        json_results = {
            "file": file if file_found else None,
            "source_app": brand if brand_found else None,
            "users": users,
            "comments": drive_comments if comments_found else None
        }

        import json
        from ghunt.objects.encoders import GHuntEncoder;
        with open(json_file, "w", encoding="utf-8") as f:
            f.write(json.dumps(json_results, cls=GHuntEncoder, indent=4))
        gb.rc.print(f"\n[+] JSON output wrote to {json_file} !", style="italic")

    await as_client.aclose()



File: ghunt/modules/geolocate.py

from ghunt import globals as gb
from ghunt.helpers.utils import get_httpx_client
from ghunt.apis.geolocation import GeolocationHttp
from ghunt.helpers import auth

import httpx
from geopy.geocoders import Nominatim

from typing import *
from pathlib import Path
import json


async def main(as_client: httpx.AsyncClient, bssid: str, input_file: Path, json_file: Path=None):
    # Verifying args
    body = None
    if input_file:
        if not input_file.exists():
            exit(f"[-] The input file \"{input_file}\" doesn't exist.")
        with open(input_file, "r", encoding="utf-8") as f:
            try:
                body = json.load(f)
            except json.JSONDecodeError:
                exit("[-] The input file is not a valid JSON file.")

    if not as_client:
        as_client = get_httpx_client()
 
    ghunt_creds = await auth.load_and_auth(as_client)

    geo_api = GeolocationHttp(ghunt_creds)
    found, resp = await geo_api.geolocate(as_client, bssid=bssid, body=body)
    if not found:
        exit("[-] The location wasn't found.")

    geolocator = Nominatim(user_agent="nominatim")
    location = geolocator.reverse(f"{resp.location.latitude}, {resp.location.longitude}", timeout=10)
    raw_address = location.raw['address']
    address = location.address

    gb.rc.print("📍 Location found !\n", style="plum2")
    gb.rc.print(f"🛣️ [italic]Accuracy : {resp.accuracy} meters[/italic]\n")
    gb.rc.print(f"Latitude : {resp.location.latitude}", style="bold")
    gb.rc.print(f"Longitude : {resp.location.longitude}\n", style="bold")
    gb.rc.print(f"🏠 Estimated address : {address}\n")
    gb.rc.print(f"🗺️ [italic][link=https://www.google.com/maps/search/?q={resp.location.latitude},{resp.location.longitude}]Open in Google Maps[/link][/italic]\n", style=f"cornflower_blue")

    if json_file:
        from ghunt.objects.encoders import GHuntEncoder;
        with open(json_file, "w", encoding="utf-8") as f:
            f.write(json.dumps({
                "accuracy": resp.accuracy,
                "latitude": resp.location.latitude,
                "longitude": resp.location.longitude,
                "address": raw_address,
                "pretty_address": address
            }, cls=GHuntEncoder, indent=4))
        gb.rc.print(f"[+] JSON output wrote to {json_file} !", style="italic")

    await as_client.aclose()


File: ghunt/helpers/auth.py

import asyncio
import json
import base64
from typing import *

import httpx
from bs4 import BeautifulSoup as bs

from ghunt import globals as gb
from ghunt.objects.base import GHuntCreds
from ghunt.errors import *
from ghunt.helpers.utils import *
from ghunt.helpers import listener
from ghunt.helpers.knowledge import get_domain_of_service, get_package_sig
from ghunt.knowledge.services import services_baseurls
from ghunt.helpers.auth import *


async def android_master_auth(as_client: httpx.AsyncClient, oauth_token: str) -> Tuple[str, List[str], str, str]:
    """
        Takes an oauth_token to perform an android authentication
        to get the master token and other informations.

        Returns the master token, connected services, account email and account full name.
    """
    data = {
        "Token": oauth_token,
        "service": "ac2dm",
        "get_accountid": 1,
        "ACCESS_TOKEN": 1,
        "add_account": 1,
        "callerSig": "38918a453d07199354f8b19af05ec6562ced5788"
    }

    req = await as_client.post("https://android.googleapis.com/auth", data=data)
    resp = parse_oauth_flow_response(req.text)
    for keyword in ["Token", "Email", "services", "firstName", "lastName"]:
        if keyword not in resp:
            raise GHuntAndroidMasterAuthError(f'Expected "{keyword}" in the response of the Android Master Authentication.\nThe oauth_token may be expired.')
    return resp["Token"], resp["services"].split(","), resp["Email"], f'{resp["firstName"]} {resp["lastName"]}'

async def android_oauth_app(as_client: httpx.AsyncClient, master_token: str,
                package_name: str, scopes: List[str]) -> Tuple[str, List[str], int]:
    """
        Uses the master token to ask for an authorization token,
        with specific scopes and app package name.

        Returns the authorization token, granted scopes and expiry UTC timestamp.
    """
    client_sig = get_package_sig(package_name)

    data = {
        "app": package_name,
        "service": f"oauth2:{' '.join(scopes)}",
        "client_sig": client_sig,
        "Token": master_token
    }

    req = await as_client.post("https://android.googleapis.com/auth", data=data)
    resp = parse_oauth_flow_response(req.text)
    for keyword in ["Expiry", "grantedScopes", "Auth"]:
        if keyword not in resp:
            raise GHuntAndroidAppOAuth2Error(f'Expected "{keyword}" in the response of the Android App OAuth2 Authentication.\nThe master token may be revoked.')
    return resp["Auth"], resp["grantedScopes"].split(" "), int(resp["Expiry"])

async def gen_osid(as_client: httpx.AsyncClient, cookies: Dict[str, str], generated_osids: dict[str, str], service: str) -> None:
    domain = get_domain_of_service(service)

    params = {
        "service": service,
        "osid": 1,
        "continue": f"https://{domain}/",
        "followup": f"https://{domain}/",
        "authuser": 0
    }

    req = await as_client.get(f"https://accounts.google.com/ServiceLogin", params=params, cookies=cookies, headers=gb.config.headers)

    body = bs(req.text, 'html.parser')
    
    params = {x.attrs["name"]:x.attrs["value"] for x in body.find_all("input", {"type":"hidden"})}

    headers = {**gb.config.headers, **{"Content-Type": "application/x-www-form-urlencoded"}}
    req = await as_client.post(f"https://{domain}/accounts/SetOSID", cookies=cookies, data=params, headers=headers)

    if not "OSID" in req.cookies:
        raise GHuntOSIDAuthError("[-] No OSID header detected, exiting...")

    generated_osids[service] = req.cookies["OSID"]

async def gen_osids(as_client: httpx.AsyncClient, cookies: Dict[str, str], osids: List[str]) -> Dict[str, str]:
    """
        Generate OSIDs of given services names,
        contained in the "osids" dict argument.
    """
    generated_osids = {}
    tasks = [gen_osid(as_client, cookies, generated_osids, service) for service in osids]
    await asyncio.gather(*tasks)

    return generated_osids

async def check_cookies(as_client: httpx.AsyncClient, cookies: Dict[str, str]) -> bool:
    """Checks the validity of given cookies."""
    continue_url = "https://www.google.com/robots.txt"
    params = {"continue": continue_url}
    req = await as_client.get("https://accounts.google.com/CheckCookie", params=params, cookies=cookies)
    return req.status_code == 302 and not req.headers.get("Location", "").startswith(("https://support.google.com", "https://accounts.google.com/CookieMismatch"))

async def check_osid(as_client: httpx.AsyncClient, cookies: Dict[str, str], service: str) -> bool:
    """Checks the validity of given OSID."""
    domain = get_domain_of_service(service)
    wanted = ["authuser", "continue", "osidt", "ifkv"]
    req = await as_client.get(f"https://accounts.google.com/ServiceLogin?service={service}&osid=1&continue=https://{domain}/&followup=https://{domain}/&authuser=0",
                    cookies=cookies, headers=gb.config.headers)

    body = bs(req.text, 'html.parser')
    params = [x.attrs["name"] for x in body.find_all("input", {"type":"hidden"})]
    if not all([param in wanted for param in params]):
        return False

    return True

async def check_osids(as_client: httpx.AsyncClient, cookies: Dict[str, str], osids: Dict[str, str]) -> bool:
    """Checks the validity of given OSIDs."""
    tasks = [check_osid(as_client, cookies, service) for service in osids]
    results = await asyncio.gather(*tasks)
    return all(results)

async def check_master_token(as_client: httpx.AsyncClient, master_token: str) -> str:
    """Checks the validity of the android master token."""
    try:
        await android_oauth_app(as_client, master_token, "com.google.android.play.games", ["https://www.googleapis.com/auth/games.firstparty"])
    except GHuntAndroidAppOAuth2Error:
        return False
    return True

async def gen_cookies_and_osids(as_client: httpx.AsyncClient, ghunt_creds: GHuntCreds, osids: list[str]=[*services_baseurls.keys()]):
    from ghunt.apis.accounts import Accounts
    accounts_api = Accounts(ghunt_creds)
    is_logged_in, uber_auth = await accounts_api.OAuthLogin(as_client)
    if not is_logged_in:
        raise GHuntLoginError("[-] Not logged in.")

    params = {
        "uberauth": uber_auth,
        "continue": "https://www.google.com",
        "source": "ChromiumAccountReconcilor",
        "externalCcResult": "doubleclick:null,youtube:null"
    }

    req = await as_client.get("https://accounts.google.com/MergeSession", params=params)
    cookies = dict(req.cookies)
    ghunt_creds.cookies = cookies
    osids = await gen_osids(as_client, cookies, osids)
    ghunt_creds.osids = osids

async def check_and_gen(as_client: httpx.AsyncClient, ghunt_creds: GHuntCreds):
    """Checks the validity of the cookies and generate new ones if needed."""
    if not await check_cookies(as_client, ghunt_creds.cookies):
        await gen_cookies_and_osids(as_client, ghunt_creds)
        if not await check_cookies(as_client, ghunt_creds.cookies):
            raise GHuntLoginError("[-] Can't generate cookies after multiple retries. Exiting...")

    ghunt_creds.save_creds(silent=True)
    gb.rc.print("[+] Authenticated !\n", style="sea_green3")

def auth_dialog() -> Tuple[Dict[str, str], str] :
    """
        Launch the dialog that asks the user
        how he want to generate its credentials.
    """
    choices = ("You can facilitate configuring GHunt by using the GHunt Companion extension on Firefox, Chrome, Edge and Opera here :\n"
                "=> https://github.com/mxrch/ghunt_companion\n\n"
                "[1] (Companion) Put GHunt on listening mode (currently not compatible with docker)\n"
                "[2] (Companion) Paste base64-encoded authentication\n"
                "[3] Enter the oauth_token (stats with \"oauth2_4/\")\n"
                "[4] Enter the master token (starts with \"aas_et/\")\n"
                "Choice => ")

    oauth_token = ""
    master_token = ""
    choice = input(choices)
    if choice in ["1", "2"]:
        if choice == "1":
            received_data = listener.run()
        elif choice == "2":
            received_data = input("Paste the encoded credentials here => ")
        data = json.loads(base64.b64decode(received_data))
        oauth_token = data["oauth_token"]

    elif choice == "3":
        oauth_token = input(f"OAuth token => ").strip('" ')

    elif choice == "4":
        master_token = input(f"Master token => ").strip('" ')

    else:
        exit("Please choose a valid choice. Exiting...")

    return oauth_token, master_token

async def load_and_auth(as_client: httpx.AsyncClient, help=True) -> GHuntCreds:
    """Returns an authenticated GHuntCreds object."""
    creds = GHuntCreds()
    try:
        creds.load_creds()
    except GHuntInvalidSession as e:
        if help:
            raise GHuntInvalidSession(f"Please generate a new session by doing => ghunt login") from e
        else:
            raise e

    await check_and_gen(as_client, creds)

    return creds


File: ghunt/helpers/ia.py

from ghunt import globals as gb
from ghunt.apis.vision import VisionHttp

import httpx

from base64 import b64encode
import asyncio


async def detect_face(vision_api: VisionHttp, as_client: httpx.AsyncClient, image_url: str) -> None:
    req = await as_client.get(image_url)
    encoded_image = b64encode(req.content).decode()
    
    are_faces_found = False
    faces_results = None

    for retry in range(5):
        rate_limited, are_faces_found, faces_results = await vision_api.detect_faces(as_client, image_content=encoded_image)
        if not rate_limited:
            break
        await asyncio.sleep(0.5)
    else:
        exit("\n[-] Vision API keeps rate-limiting.")

    if are_faces_found:
        if len(faces_results.face_annotations) > 1:
            gb.rc.print(f"🎭 {len(faces_results.face_annotations)} faces detected !", style="italic")
        else:
            gb.rc.print(f"🎭 [+] Face detected !", style="italic bold")
    else:
        gb.rc.print(f"🎭 No face detected.", style="italic bright_black")

    return faces_results


File: ghunt/helpers/playgames.py

from ghunt.objects.base import GHuntCreds
from ghunt.apis.playgames import PlayGames
from ghunt.apis.playgateway import PlayGatewayPaGrpc
from ghunt.parsers.playgames import Player, PlayerProfile
from ghunt.parsers.playgateway import PlayerSearchResult
from ghunt.objects.utils import TMPrinter

import httpx
from alive_progress import alive_bar

from typing import *


async def get_player(ghunt_creds: GHuntCreds, as_client: httpx.AsyncClient, player_id: str):
    playgames = PlayGames(ghunt_creds)

    tmprinter = TMPrinter()
    tmprinter.out("[~] Getting player profile...")
    is_found, player_profile = await playgames.get_profile(as_client, player_id)
    tmprinter.clear()
    if not is_found or not player_profile.profile_settings.profile_visible:
        return is_found, Player()
    
    playgateway_pa = PlayGatewayPaGrpc(ghunt_creds)
    player_stats = await playgateway_pa.get_player_stats(as_client, player_id)

    with alive_bar(player_stats.played_games_count, title="🚎 Fetching played games...", receipt=False) as bar:
        _, next_page_token, played_games = await playgames.get_played_games(as_client, player_id)
        bar(len(played_games.games))
        while next_page_token:
            _, next_page_token, new_played_games = await playgames.get_played_games(as_client, player_id, next_page_token)
            played_games.games += new_played_games.games
            bar(len(new_played_games.games))

    with alive_bar(player_stats.achievements_count, title="🚎 Fetching achievements...", receipt=False) as bar:
        _, next_page_token, achievements = await playgames.get_achievements(as_client, player_id)
        bar(len(achievements.achievements))
        while next_page_token:
            _, next_page_token, new_achievements = await playgames.get_achievements(as_client, player_id, next_page_token)
            achievements.achievements += new_achievements.achievements
            bar(len(new_achievements.achievements))

    player = Player(player_profile, played_games.games, achievements.achievements)
    return is_found, player

async def search_player(ghunt_creds: GHuntCreds, as_client: httpx.AsyncClient, query: str) -> List[PlayerSearchResult]:
    playgateway_pa = PlayGatewayPaGrpc(ghunt_creds)
    player_search_results = await playgateway_pa.search_player(as_client, query)
    return player_search_results.results

def output(player: Player):
    if not player.profile.profile_settings.profile_visible:
        print("\n[-] Profile is private.")
        return

    print("\n[+] Profile is public !")
    print(f"\n[+] Played to {len(player.played_games)} games")
    print(f"[+] Got {len(player.achievements)} achievements")

    if player.played_games:
        print(f"\n[+] Last played game : {player.profile.last_played_app.app_name} ({player.profile.last_played_app.timestamp_millis} UTC)")

        if player.achievements:
            app_ids_count = {}
            for achievement in player.achievements:
                if (app_id := achievement.app_id) not in app_ids_count:
                    app_ids_count[app_id] = 0
                app_ids_count[app_id] += 1
            app_ids_count = dict(sorted(app_ids_count.items(), key=lambda item: item[1], reverse=True))
            achiv_nb = list(app_ids_count.values())[0]
            target_game = None
            for game in player.played_games:
                if game.game_data.id == list(app_ids_count.keys())[0]:
                    target_game = game
                    break

            print(f"[+] Game with the most achievements : {target_game.game_data.name} ({achiv_nb})")


File: ghunt/helpers/__init__.py




File: ghunt/helpers/gmaps.py

from dateutil.relativedelta import relativedelta
from datetime import datetime
import json
from geopy import distance
from geopy.geocoders import Nominatim
from typing import *

import httpx
from alive_progress import alive_bar

from ghunt import globals as gb
from ghunt.objects.base import *
from ghunt.helpers.utils import *
from ghunt.objects.utils import *
from ghunt.helpers.knowledge import get_gmaps_type_translation


def get_datetime(datepublished: str):
    """
        Get an approximative date from the maps review date
        Examples : 'last 2 days', 'an hour ago', '3 years ago'
    """
    if datepublished.split()[0] in ["a", "an"]:
        nb = 1
    else:
        if datepublished.startswith("last"):
            nb = int(datepublished.split()[1])
        else:
            nb = int(datepublished.split()[0])

    if "minute" in datepublished:
        delta = relativedelta(minutes=nb)
    elif "hour" in datepublished:
        delta = relativedelta(hours=nb)
    elif "day" in datepublished:
        delta = relativedelta(days=nb)
    elif "week" in datepublished:
        delta = relativedelta(weeks=nb)
    elif "month" in datepublished:
        delta = relativedelta(months=nb)
    elif "year" in datepublished:
        delta = relativedelta(years=nb)
    else:
        delta = relativedelta()

    return (datetime.today() - delta).replace(microsecond=0, second=0)

async def get_reviews(as_client: httpx.AsyncClient, gaia_id: str) -> Tuple[str, Dict[str, int], List[MapsReview], List[MapsPhoto]]:
    """Extracts the target's statistics, reviews and photos."""
    next_page_token = ""
    agg_reviews = []
    agg_photos = []
    stats = {}

    req = await as_client.get(f"https://www.google.com/locationhistory/preview/mas?authuser=0&hl=en&gl=us&pb={gb.config.templates['gmaps_pb']['stats'].format(gaia_id)}")
    data = json.loads(req.text[5:])
    if not data[16][8]:
        return "empty", stats, [], []
    stats = {sec[6]:sec[7] for sec in data[16][8][0]}
    total_reviews = stats["Reviews"] + stats["Ratings"] + stats["Photos"]
    if not total_reviews:
        return "empty", stats, [], []

    with alive_bar(total_reviews, receipt=False) as bar:
        for category in ["reviews", "photos"]:
            first = True
            while True:
                if first:
                    req = await as_client.get(f"https://www.google.com/locationhistory/preview/mas?authuser=0&hl=en&gl=us&pb={gb.config.templates['gmaps_pb'][category]['first'].format(gaia_id)}")
                    first = False
                else:
                    req = await as_client.get(f"https://www.google.com/locationhistory/preview/mas?authuser=0&hl=en&gl=us&pb={gb.config.templates['gmaps_pb'][category]['page'].format(gaia_id, next_page_token)}")
                data = json.loads(req.text[5:])

                new_reviews = []
                new_photos = []
                next_page_token = ""

                # Reviews
                if category == "reviews":
                    if not data[24]:
                        return "private", stats, [], []
                    reviews_data = data[24][0]
                    if not reviews_data:
                        break
                    for review_data in reviews_data:
                        review = MapsReview()
                        review.id = review_data[6][0]
                        review.date = datetime.utcfromtimestamp(review_data[6][1][3] / 1000000)
                        if len(review_data[6][2]) > 15 and review_data[6][2][15]:
                            review.comment = review_data[6][2][15][0][0]
                        review.rating = review_data[6][2][0][0]

                        review.location.id = review_data[1][14][0]
                        review.location.name = review_data[1][2]
                        review.location.address = review_data[1][3]
                        review.location.tags = review_data[1][4] if review_data[1][4] else []
                        review.location.types = [x for x in review_data[1][8] if x]
                        if review_data[1][0]:
                            review.location.position.latitude = review_data[1][0][2]
                            review.location.position.longitude = review_data[1][0][3]
                        if len(review_data[1]) > 31 and review_data[1][31]:
                            review.location.cost_level = len(review_data[1][31])
                        new_reviews.append(review)
                        bar()

                    agg_reviews += new_reviews

                    if not new_reviews or len(data[24]) < 4 or not data[24][3]:
                        break
                    next_page_token = data[24][3].strip("=")

                # Photos
                elif category == "photos" :
                    if not data[22]:
                        return "private", stats, [], []
                    photos_data = data[22][1]
                    if not photos_data:
                        break
                    for photo_data in photos_data:
                        photos = MapsPhoto()
                        photos.id = photo_data[0][10]
                        photos.url = photo_data[0][6][0].split("=")[0]
                        date = photo_data[0][21][6][8]
                        photos.date = datetime(date[0], date[1], date[2], date[3]) # UTC
                        # photos.approximative_date = get_datetime(date[8][0]) # UTC

                        if len(photo_data) > 1:
                            photos.location.id = photo_data[1][14][0]
                            photos.location.name = photo_data[1][2]
                            photos.location.address = photo_data[1][3]
                            photos.location.tags = photo_data[1][4] if photo_data[1][4] else []
                            photos.location.types = [x for x in photo_data[1][8] if x] if photo_data[1][8] else []
                            if photo_data[1][0]:
                                photos.location.position.latitude = photo_data[1][0][2]
                                photos.location.position.longitude = photo_data[1][0][3]
                            if len(photo_data[1]) > 31 and photo_data[1][31]:
                                photos.location.cost_level = len(photo_data[1][31])
                        new_photos.append(photos)
                        bar()

                    agg_photos += new_photos

                    if not new_photos or len(data[22]) < 4 or not data[22][3]:
                        break
                    next_page_token = data[22][3].strip("=")

    return "", stats, agg_reviews, agg_photos

def avg_location(locs: Tuple[float, float]):
    """
        Calculates the average location
        from a list of (latitude, longitude) tuples.
    """
    latitude = []
    longitude = []
    for loc in locs:
        latitude.append(loc[0])
        longitude.append(loc[1])

    latitude = sum(latitude) / len(latitude)
    longitude = sum(longitude) / len(longitude)
    return latitude, longitude

def translate_confidence(percents: int):
    """Translates the percents number to a more human-friendly text"""
    if percents >= 100:
        return "Extremely high"
    elif percents >= 80:
        return "Very high"
    elif percents >= 60:
        return "Little high"
    elif percents >= 40:
        return "Okay"
    elif percents >= 20:
        return "Low"
    elif percents >= 10:
        return "Very low"
    else:
        return "Extremely low"

def sanitize_location(location: Dict[str, str]):
    """Returns the nearest place from a Nomatim location response."""
    not_country = False
    not_town = False
    town = "?"
    country = "?"
    if "city" in location:
        town = location["city"]
    elif "village" in location:
        town = location["village"]
    elif "town" in location:
        town = location["town"]
    elif "municipality" in location:
        town = location["municipality"]
    else:
        not_town = True
    if not "country" in location:
        not_country = True
        location["country"] = country
    if not_country and not_town:
        return False
    location["town"] = town
    return location

def calculate_probable_location(geolocator: Nominatim, reviews_and_photos: List[MapsReview|MapsPhoto], gmaps_radius: int):
    """Calculates the probable location from a list of reviews and the max radius."""
    tmprinter = TMPrinter()
    radius = gmaps_radius

    locations = {}
    tmprinter.out(f"Calculation of the distance of each review...")
    for nb, review in enumerate(reviews_and_photos):
        if not review.location.position.latitude or not review.location.position.longitude:
            continue
        if review.location.id not in locations:
            locations[review.location.id] = {"dates": [], "locations": [], "range": None, "score": 0}
        location = (review.location.position.latitude, review.location.position.longitude)
        for review2 in reviews_and_photos:
            location2 = (review2.location.position.latitude, review2.location.position.longitude)
            dis = distance.distance(location, location2).km

            if dis <= radius:
                locations[review.location.id]["dates"].append(review2.date)
                locations[review.location.id]["locations"].append(location2)

        maxdate = max(locations[review.location.id]["dates"])
        mindate = min(locations[review.location.id]["dates"])
        locations[review.location.id]["range"] = maxdate - mindate
        tmprinter.out(f"Calculation of the distance of each review ({nb}/{len(reviews_and_photos)})...")

    tmprinter.clear()

    locations = {k: v for k, v in
                 sorted(locations.items(), key=lambda k: len(k[1]["locations"]), reverse=True)}  # We sort it

    tmprinter.out("Identification of redundant areas...")
    to_del = []
    for id in locations:
        if id in to_del:
            continue
        for id2 in locations:
            if id2 in to_del or id == id2:
                continue
            if all([loc in locations[id]["locations"] for loc in locations[id2]["locations"]]):
                to_del.append(id2)
    for hash in to_del:
        del locations[hash]

    tmprinter.out("Calculating confidence...")

    maxrange = max([locations[hash]["range"] for hash in locations])
    maxlen = max([len(locations[hash]["locations"]) for hash in locations])
    minreq = 3
    mingroups = 3

    score_steps = 4
    for hash, loc in locations.items():
        if len(loc["locations"]) == maxlen:
            locations[hash]["score"] += score_steps * 4
        if loc["range"] == maxrange:
            locations[hash]["score"] += score_steps * 3
        if len(locations) >= mingroups:
            others = sum([len(locations[h]["locations"]) for h in locations if h != hash])
            if len(loc["locations"]) > others:
                locations[hash]["score"] += score_steps * 2
        if len(loc["locations"]) >= minreq:
            locations[hash]["score"] += score_steps

    panels = sorted(set([loc["score"] for loc in locations.values()]), reverse=True)

    maxscore = sum([p * score_steps for p in range(1, score_steps + 1)])
    for panel in panels:
        locs = [loc for loc in locations.values() if loc["score"] == panel]
        if len(locs[0]["locations"]) == 1:
            panel /= 2
        if len(reviews_and_photos) < 4:
            panel /= 2
        confidence = translate_confidence(panel / maxscore * 100)
        for nb, loc in enumerate(locs):
            avg = avg_location(loc["locations"])
            while True:
                try:
                    location = geolocator.reverse(f"{avg[0]}, {avg[1]}", timeout=10).raw["address"]
                    break
                except:
                    pass
            location = sanitize_location(location)
            locs[nb]["avg"] = location
            del locs[nb]["locations"]
            del locs[nb]["score"]
            del locs[nb]["range"]
            del locs[nb]["dates"]

        tmprinter.clear()

        return confidence, locs

def output(err: str, stats: Dict[str, int], reviews: List[MapsReview], photos: List[MapsPhoto], gaia_id: str):
    """Pretty print the Maps results, and do some guesses."""

    print(f"\nProfile page : https://www.google.com/maps/contrib/{gaia_id}/reviews")

    reviews_and_photos: List[MapsReview|MapsPhoto] = reviews + photos
    if err != "private" and (err == "empty" or not reviews_and_photos):
        print("\n[-] No review.")
        return

    print("\n[Statistics]")
    for section, number in stats.items():
        if number:
            print(f"{section} : {number}")

    if err == "private":
        print("\n[-] Reviews are private.")
        return

    print("\n[Reviews]")
    avg_ratings = round(sum([x.rating for x in reviews]) / len(reviews), 1)
    print(f"[+] Average rating : {ppnb(avg_ratings)}/5\n")

    costs_table = {
        1: "Inexpensive",
        2: "Moderately expensive",
        3: "Expensive",
        4: "Very expensive"
    }

    total_costs = 0
    costs_stats = {x:0 for x in range(1,5)}
    for review in reviews_and_photos:
        if review.location.cost_level:
            costs_stats[review.location.cost_level] += 1
            total_costs += 1
    costs_stats = dict(sorted(costs_stats.items(), key=lambda item: item[1], reverse=True)) # We sort the dict by cost popularity

    if total_costs:
        print("[Costs]")
        for cost, desc in costs_table.items():
            line = f"> {ppnb(round(costs_stats[cost]/total_costs*100, 1))}% {desc} ({costs_stats[cost]})"
            style = ""
            if not costs_stats[cost]:
                style = "bright_black"
            elif costs_stats[cost] == list(costs_stats.values())[0]:
                style = "spring_green1"
            gb.rc.print(line, style=style)
            
        avg_costs = round(sum([x*y for x,y in costs_stats.items()]) / total_costs)
        print(f"\n[+] Average costs : {costs_table[avg_costs]}")
    else:
        print("[-] No costs data.")

    types = {}
    for review in reviews_and_photos:
        for type in review.location.types:
            if type not in types:
                types[type] = 0
            types[type] += 1
    types = dict(sorted(types.items(), key=lambda item: item[1], reverse=True))

    types_and_tags = {}
    for review in reviews_and_photos:
        for type in review.location.types:
            if type not in types_and_tags:
                types_and_tags[type] = {}
            for tag in review.location.tags:
                if tag not in types_and_tags[type]:
                    types_and_tags[type][tag] = 0
                types_and_tags[type][tag] += 1
            types_and_tags[type] = dict(sorted(types_and_tags[type].items(), key=lambda item: item[1], reverse=True))
    types_and_tags = dict(sorted(types_and_tags.items()))

    if types_and_tags:
        print("\nTarget's locations preferences :")

        unknown_trads = []
        for type, type_count in types.items():
            tags_counts = types_and_tags[type]
            translation = get_gmaps_type_translation(type)
            if not translation:
                unknown_trads.append(type)
            gb.rc.print(f"\n🏨 [underline]{translation if translation else type.title()} [{type_count}]", style="bold")
            nb = 0
            for tag, tag_count in list(tags_counts.items()):
                if nb >= 7:
                    break
                elif tag.lower() == type:
                    continue
                print(f"- {tag} ({tag_count})")
                nb += 1

        if unknown_trads:
            print(f"\n⚠️ The following gmaps types haven't been found in GHunt\'s knowledge.")
            for type in unknown_trads:
                print(f"- {type}")
            print("Please open an issue on the GHunt Github or submit a PR to add it !")

    geolocator = Nominatim(user_agent="nominatim")

    confidence, locations = calculate_probable_location(geolocator, reviews_and_photos, gb.config.gmaps_radius)
    print(f"\n[+] Probable location (confidence => {confidence}) :")

    loc_names = []
    for loc in locations:
        loc_names.append(
            f"- {loc['avg']['town']}, {loc['avg']['country']}"
        )

    loc_names = set(loc_names)  # delete duplicates
    for loc in loc_names:
        print(loc)


File: ghunt/helpers/calendar.py

from xmlrpc.client import Boolean
from dateutil.relativedelta import relativedelta
from beautifultable import BeautifulTable
import httpx

from typing import *
from copy import deepcopy

from ghunt.parsers.calendar import Calendar, CalendarEvents
from ghunt.objects.base import GHuntCreds
from ghunt.objects.utils import TMPrinter
from ghunt.apis.calendar import CalendarHttp


async def fetch_all(ghunt_creds: GHuntCreds, as_client: httpx.AsyncClient, email_address: str) -> Tuple[Boolean, Calendar, CalendarEvents]:
    calendar_api = CalendarHttp(ghunt_creds)
    found, calendar = await calendar_api.get_calendar(as_client, email_address)
    if not found:
        return False, None, None
    tmprinter = TMPrinter()
    _, events = await calendar_api.get_events(as_client, email_address, params_template="max_from_beginning")
    next_page_token = deepcopy(events.next_page_token)
    while next_page_token:
        tmprinter.out(f"[~] Dumped {len(events.items)} events...")
        _, new_events = await calendar_api.get_events(as_client, email_address, params_template="max_from_beginning", page_token=next_page_token)
        events.items += new_events.items
        next_page_token = deepcopy(new_events.next_page_token)
    tmprinter.clear()
    return True, calendar, events

def out(calendar: Calendar, events: CalendarEvents, email_address: str, display_name="", limit=5):
    """
        Output fetched calendar events.
        if limit = 0, = all events are shown
    """

    ### Calendar

    print(f"Calendar ID : {calendar.id}")
    if calendar.summary != calendar.id:
        print(f"[+] Calendar Summary : {calendar.summary}")
    print(f"Calendar Timezone : {calendar.time_zone}\n")

    ### Events
    target_events = events.items[-limit:]
    if target_events:
        print(f"[+] {len(events.items)} event{'s' if len(events.items) > 1 else ''} dumped ! Showing the last {len(target_events)} one{'s' if len(target_events) > 1 else ''}...\n")

        table = BeautifulTable()
        table.set_style(BeautifulTable.STYLE_GRID)
        table.columns.header = ["Name", "Datetime (UTC)", "Duration"]

        for event in target_events:
            title = "/"
            if event.summary:
                title = event.summary
            duration = "?"
            if event.end.date_time and event.start.date_time:
                duration = relativedelta(event.end.date_time, event.start.date_time)
                if duration.days or duration.hours or duration.minutes:
                    duration = (f"{(str(duration.days) + ' day' + ('s' if duration.days > 1 else '')) if duration.days else ''} "
                        f"{(str(duration.hours) + ' hour' + ('s' if duration.hours > 1 else '')) if duration.hours else ''} "
                        f"{(str(duration.minutes) + ' minute' + ('s' if duration.minutes > 1 else '')) if duration.minutes else ''}").strip()         

            date = "?"
            if event.start.date_time:
                date = event.start.date_time.strftime("%Y/%m/%d %H:%M:%S")
            table.rows.append([title, date, duration])

        print(table)

        print(f"\n🗃️ Download link :\n=> https://calendar.google.com/calendar/ical/{email_address}/public/basic.ics")
    else:
        print("[-] No events dumped.")

    ### Names

    names = set()
    for event in events.items:
        if event.creator.email == email_address and (name := event.creator.display_name) and name != display_name:
            names.add(name)
    if names:
        print("\n[+] Found other names used by the target :")
        for name in names:
            print(f"- {name}")


File: ghunt/helpers/gmail.py

import httpx


async def is_email_registered(as_client: httpx.AsyncClient, email: str) -> bool:
    """
        Abuse the gxlu endpoint to check if any email address
        is registered on Google. (not only gmail accounts)
    """
    req = await as_client.get(f"https://mail.google.com/mail/gxlu", params={"email": email})
    return "Set-Cookie" in req.headers


File: ghunt/helpers/utils.py

from pathlib import Path
from PIL import Image
import hashlib
from typing import *
from time import time
from datetime import datetime, timezone
from dateutil.parser import isoparse
from copy import deepcopy
import jsonpickle
import json
from packaging.version import parse as parse_version

import httpx
import imagehash
from io import BytesIO

from ghunt import globals as gb
from ghunt import version as current_version
from ghunt.lib.httpx import AsyncClient


def get_httpx_client() -> httpx.AsyncClient:
    """
        Returns a customized to better support the needs of GHunt CLI users.
    """
    return AsyncClient(http2=True, timeout=15)
    # return AsyncClient(http2=True, timeout=15, proxies="http://127.0.0.1:8282", verify=False)

def oprint(obj: any) -> str:
    serialized = jsonpickle.encode(obj)
    pretty_output = json.dumps(json.loads(serialized), indent=2)
    print(pretty_output)

def within_docker() -> bool:
    return Path('/.dockerenv').is_file()

def gen_sapisidhash(sapisid: str, origin: str, timestamp: str = str(int(time()))) -> str:
    return f"{timestamp}_{hashlib.sha1(' '.join([timestamp, sapisid, origin]).encode()).hexdigest()}"

def inject_osid(cookies: Dict[str, str], osids: Dict[str, str], service: str) -> Dict[str, str]:
    cookies_with_osid = deepcopy(cookies)
    cookies_with_osid["OSID"] = osids[service]
    return cookies_with_osid
    
def is_headers_syntax_good(headers: Dict[str, str]) -> bool:
    try:
        httpx.Headers(headers)
        return True
    except:
        return False

async def get_url_image_flathash(as_client: httpx.AsyncClient, image_url: str) -> str:
    req = await as_client.get(image_url)
    img = Image.open(BytesIO(req.content))
    flathash = imagehash.average_hash(img)
    return str(flathash)

async def is_default_profile_pic(as_client: httpx.AsyncClient, image_url: str) -> Tuple[bool, str]:
    """
        Returns a boolean which indicates if the image_url
        is a default profile picture, and the flathash of
        the image.
    """
    flathash = await get_url_image_flathash(as_client, image_url)
    if imagehash.hex_to_flathash(flathash, 8) - imagehash.hex_to_flathash("000018183c3c0000", 8) < 10 :
        return True, str(flathash)
    return False, str(flathash)

def get_class_name(obj) -> str:
        return str(obj).strip("<>").split(" ")[0]

def get_datetime_utc(date_str):
    """Converts ISO to datetime object in UTC"""
    date = isoparse(date_str)
    margin = date.utcoffset()
    return date.replace(tzinfo=timezone.utc) - margin

def ppnb(nb: float|int) -> float:
    """
        Pretty print float number
        Ex: 3.9 -> 3.9
            4.0 -> 4
            4.1 -> 4.1
    """
    try:
        return int(nb) if nb % int(nb) == 0.0 else nb
    except ZeroDivisionError:
        if nb == 0.0:
            return 0
        else:
            return nb

def parse_oauth_flow_response(body: str):
    """
        Correctly format the response sent by android.googleapis.com
        during the Android OAuth2 Login Flow.
    """
    return {sp[0]:'='.join(sp[1:]) for x in body.split("\n") if (sp := x.split("="))}

def humanize_list(array: List[any]):
    """
        Transforms a list to a human sentence.
        Ex : ["reader", "writer", "owner"] -> "reader, writer and owner".
    """
    if len(array) <= 1:
        return ''.join(array)

    final = ""
    for nb, item in enumerate(array):
        if nb == 0:
            final += f"{item}"
        elif nb+1 < len(array):
            final += f", {item}"
        else:
            final += f" and {item}"
    return final

def unicode_patch(txt: str):
    bad_chars = {
        "é": "e",
        "è": "e",
        "ç": "c",
        "à": "a"
    }
    return txt.replace(''.join([*bad_chars.keys()]), ''.join([*bad_chars.values()]))

def show_version():
    new_version, new_metadata = check_new_version()
    print()
    gb.rc.print(f"> GHunt {current_version.metadata.get('version', '')} ({current_version.metadata.get('name', '')}) <".center(53), style="bold")
    print()
    if new_version:
        gb.rc.print(f"🥳 New version {new_metadata.get('version', '')} ({new_metadata.get('name', '')}) is available !", style="bold red")
        gb.rc.print(f"🤗 Run 'pipx upgrade ghunt' to update.", style="bold light_pink3")
    else:
        gb.rc.print("🎉 You are up to date !", style="light_pink3")
        

def check_new_version() -> tuple[bool, dict[str, str]]:
    """
        Checks if there is a new version of GHunt available.
    """
    req = httpx.get("https://raw.githubusercontent.com/mxrch/GHunt/master/ghunt/version.py")
    if req.status_code != 200:
        return False, {}
    
    raw = req.text.strip().removeprefix("metadata = ")
    data = json.loads(raw)
    new_version = data.get("version", "")
    new_name = data.get("name", "")

    if parse_version(new_version) > parse_version(current_version.metadata.get("version", "")):
        return True, {"version": new_version, "name": new_name}
    return False, {}


File: ghunt/helpers/drive.py

from typing import *

from ghunt.parsers.drive import DriveComment, DriveCommentList, DriveCommentReply, DriveFile
from ghunt.objects.base import DriveExtractedUser
from ghunt.helpers.utils import oprint # TEMP


def get_users_from_file(file: DriveFile) -> List[DriveExtractedUser]:
    """
        Extracts the users from the permissions of a Drive file,
        and the last modifying user.
    """
    
    users: Dict[str, DriveExtractedUser] = {}
    for perms in [file.permissions, file.permissions_summary.select_permissions]:
        for perm in perms:
            if not perm.email_address:
                continue
            #oprint(perm)
            user = DriveExtractedUser()
            user.email_address = perm.email_address
            user.gaia_id = perm.user_id
            user.name = perm.name
            user.role = perm.role
            users[perm.email_address] = user

    # Last modifying user
    target_user = file.last_modifying_user
    if target_user.id:
        email = target_user.email_address
        if not email:
            email = target_user.email_address_from_account
        if not email:
            return users

        if email in users:
            users[email].is_last_modifying_user = True

    return list(users.values())

def get_comments_from_file(comments: DriveCommentList) -> List[Tuple[str, Dict[str, any]]]:
    """
        Extracts the comments and replies of a Drive file.
    """

    def update_stats(authors: List[Dict[str, Dict[str, any]]], comment: DriveComment|DriveCommentReply):
        name = comment.author.display_name
        pic_url = comment.author.picture.url
        key = f"{name}${pic_url}" # Two users can have the same name, not the same picture URL (I hope so)
                                  # So we do this to make users "unique"
        if key not in authors:
            authors[key] = {
                "name": name,
                "pic_url": pic_url,
                "count": 0
            }
        authors[key]["count"] += 1

    authors: Dict[str, Dict[str, any]] = {}
    for comment in comments.items:
        update_stats(authors, comment)
        for reply in comment.replies:
            update_stats(authors, reply)

    return sorted(authors.items(), key=lambda k_v: k_v[1]['count'], reverse=True)


File: ghunt/helpers/knowledge.py

from ghunt.knowledge.services import services_baseurls
from ghunt.knowledge.keys import keys
from ghunt.knowledge.maps import types_translations
from ghunt.knowledge.people import user_types
from ghunt.knowledge.sig import sigs
from ghunt.errors import GHuntKnowledgeError

from typing import *


def get_domain_of_service(service: str) -> str:
    if service not in services_baseurls:
        raise GHuntKnowledgeError(f'The service "{service}" has not been found in GHunt\'s services knowledge.')
    return services_baseurls.get(service)

def get_origin_of_key(key_name: str) -> str:
    if key_name not in keys:
        raise GHuntKnowledgeError(f'The key "{key_name}" has not been found in GHunt\'s API keys knowledge.')
    return keys.get(key_name, {}).get("origin")

def get_api_key(key_name: str) -> str:
    if key_name not in keys:
        raise GHuntKnowledgeError(f'The key "{key_name}" has not been found in GHunt\'s API keys knowledge.')
    return keys.get(key_name, {}).get("key")

def get_gmaps_type_translation(type_name: str) -> str:
    if type_name not in types_translations:
        raise GHuntKnowledgeError(f'The gmaps type "{type_name}" has not been found in GHunt\'s knowledge.\nPlease open an issue on the GHunt Github or submit a PR to add it !')
    return types_translations.get(type_name)

def get_user_type_definition(type_name: str) -> str:
    if type_name not in user_types:
        raise GHuntKnowledgeError(f'The user type "{type_name}" has not been found in GHunt\'s knowledge.\nPlease open an issue on the GHunt Github or submit a PR to add it !')
    return user_types.get(type_name)

def get_package_sig(package_name: str) -> str:
    if package_name not in sigs:
        raise GHuntKnowledgeError(f'The package name "{package_name}" has not been found in GHunt\'s SIGs knowledge.')
    return sigs.get(package_name)


File: ghunt/helpers/banner.py

from ghunt import globals as gb

def show_banner():

    banner = """
    [red] .d8888b.  [/][blue]888    888[/][red]                   888    
    [/][red]d88P  Y88b [/][blue]888    888[/][red]                   888    
    [/][yellow]888    [/][red]888 [/][blue]888    888[/][red]                   888    
    [/][yellow]888        [/][blue]8888888888[/][green] 888  888[/][yellow] 88888b. [/][red] 888888 
    [/][yellow]888  [/][blue]88888 [/][blue]888    888[/][green] 888  888[/][yellow] 888 "88b[/][red] 888    
    [/][yellow]888    [/][blue]888 [/][blue]888    888[/][green] 888  888[/][yellow] 888  888[/][red] 888    
    [/][green]Y88b  d88P [/][blue]888    888[/][green] Y88b 888[/][yellow] 888  888[/][red] Y88b.  
    [/][green] "Y8888P88 [/][blue]888    888[/][green]  "Y88888[/][yellow] 888  888[/][red]  "Y888[/red] v2

             [bold]By: mxrch (🐦 [deep_sky_blue1][link=https://x.com/mxrchreborn]@mxrchreborn[/link][/deep_sky_blue1])
       [indian_red1]Support my work on GitHub Sponsors ! 💖[/indian_red1][/bold]
    """
                                                
    gb.rc.print(banner)


File: ghunt/helpers/listener.py

from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import *

from ghunt.objects.base import SmartObj


class DataBridge(SmartObj):
    def __init__(self):
        self.data = None

class Server(BaseHTTPRequestHandler):    
    def _set_response(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.send_header('Access-Control-Allow-Origin','*')
        self.end_headers()

    def do_GET(self):
        if self.path == "/ghunt_ping":
            self._set_response()
            self.wfile.write(b"ghunt_pong")

    def do_POST(self):
        if self.path == "/ghunt_feed":
            content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
            post_data = self.rfile.read(content_length) # <--- Gets the data itself
            self.data_bridge.data = post_data.decode('utf-8')

            self._set_response()
            self.wfile.write(b"ghunt_received_ok")

    def log_message(self, format, *args):
        return

def run(server_class=HTTPServer, handler_class=Server, port=60067):
    server_address = ('127.0.0.1', port)
    handler_class.data_bridge = DataBridge()
    server = server_class(server_address, handler_class)
    try:
        print(f"GHunt is listening on port {port}...")

        while True:
            server.handle_request()
            if handler_class.data_bridge.data:
                break

    except KeyboardInterrupt:
        exit("[-] Exiting...")
    else:
        if handler_class.data_bridge.data:
            print("[+] Received cookies !")
            return handler_class.data_bridge.data


![](assets/long_banner.png)

<br>

#### 🌐 GHunt Online version : https://osint.industries

<br>

![Python minimum version](https://img.shields.io/badge/Python-3.10%2B-brightgreen)

# 😊 Description

GHunt (v2) is an offensive Google framework, designed to evolve efficiently.\
It's currently focused on OSINT, but any use related with Google is possible.

Features :
- CLI usage and modules
- Python library usage
- Fully async
- JSON export
- Browser extension to ease login

# ✔️ Requirements
- Python >= 3.10

# ⚙️ Installation

```bash
$ pip3 install pipx
$ pipx ensurepath
$ pipx install ghunt
```
It will automatically use venvs to avoid dependency conflicts with other projects.

# 💃 Usage

## Login

First, launch the listener by doing `ghunt login` and choose between 1 of the 2 first methods :
```bash
$ ghunt login

[1] (Companion) Put GHunt on listening mode (currently not compatible with docker)
[2] (Companion) Paste base64-encoded cookies
[3] Enter manually all cookies

Choice =>
```

Then, use GHunt Companion to complete the login.

The extension is available on the following stores :\
\
[![Firefox](https://files.catbox.moe/5g2ld5.png)](https://addons.mozilla.org/en-US/firefox/addon/ghunt-companion/)&nbsp;&nbsp;&nbsp;[![Chrome](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/ghunt-companion/dpdcofblfbmmnikcbmmiakkclocadjab)

## Modules

Then, profit :
```bash
Usage: ghunt [-h] {login,email,gaia,drive,geolocate} ...

Positional Arguments:
  {login,email,gaia,drive,geolocate}
    login               Authenticate GHunt to Google.
    email               Get information on an email address.
    gaia                Get information on a Gaia ID.
    drive               Get information on a Drive file or folder.
    geolocate           Geolocate a BSSID.

Options:
  -h, --help            show this help message and exit
```

📄 You can also use --json with email, gaia, drive and geolocate modules to export in JSON ! Example :

```bash
$ ghunt email <email_address> --json user_data.json
```

**Have fun 🥰💞**

# 🧑‍💻 Developers

📕 I started writing some docs [here](https://github.com/mxrch/GHunt/wiki) and examples [here](https://github.com/mxrch/GHunt/tree/master/examples), feel free to contribute !

To use GHunt as a lib, you can't use pipx because it uses a venv.\
So you should install GHunt with pip :
```bash
$ pip3 install ghunt
```

And now, you should be able to `import ghunt` in your projects !\
You can right now play with the [examples](https://github.com/mxrch/GHunt/tree/master/examples).

# 📮 Details

## Obvious disclaimer

This tool is for educational purposes only, I am not responsible for its use.

## Less obvious disclaimer

This project is under [AGPL Licence](https://choosealicense.com/licenses/agpl-3.0/), and you have to respect it.\
**Use it only in personal, criminal investigations, pentesting, or open-source projects.**

## Thanks

- [novitae](https://github.com/novitae) for being my Python colleague
- All the people on [Malfrats Industries](https://discord.gg/sg2YcrC6x9) and elsewhere for the beta test !
- The HideAndSec team 💗 (blog : https://hideandsec.sh)
- [Med Amine Jouini](https://dribbble.com/jouiniamine) for his beautiful rework of the Google logo, which I was inspired by *a lot*.

## Sponsors

Thanks to these awesome people for supporting me !

<!-- sponsors --><a href="https://github.com/BlWasp"><img src="https://github.com/BlWasp.png" width="50px" alt="BlWasp" /></a>&nbsp;&nbsp;<a href="https://github.com/C3n7ral051nt4g3ncy"><img src="https://github.com/C3n7ral051nt4g3ncy.png" width="50px" alt="C3n7ral051nt4g3ncy" /></a>&nbsp;&nbsp;<a href="https://github.com/im-hanzou"><img src="https://github.com/im-hanzou.png" width="50px" alt="im-hanzou" /></a>&nbsp;&nbsp;<a href="https://github.com/gingeleski"><img src="https://github.com/gingeleski.png" width="50px" alt="gingeleski" /></a>&nbsp;&nbsp;<a href="https://github.com/ItsMalware"><img src="https://github.com/ItsMalware.png" width="50px" alt="ItsMalware" /></a>&nbsp;&nbsp;<!-- sponsors -->

\
You like my work ?\
[Sponsor me](https://github.com/sponsors/mxrch) on GitHub ! 🤗
