Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

<br>

> **This is `ghunt-fixed`, a fork of [mxrch/GHunt](https://github.com/mxrch/GHunt), modified on 2026-07-31.**
> The upstream PyPI release (2.3.4) crashes with `KeyError: 'container'` on some accounts because
> Google's People API responses now sometimes omit `metadata.container`. This fork patches
> `ghunt/parsers/people.py` to handle that missing key gracefully instead of crashing
> (see [upstream PR #593](https://github.com/mxrch/GHunt/pull/593), not yet merged/released as of this fork).
> Still licensed under AGPLv3, same as upstream — see [LICENSE.md](LICENSE.md).

<br>

#### 🌐 GHunt Online version : https://osint.industries
#### 🐍 Now Python 3.13 compatible !

Expand Down
48 changes: 33 additions & 15 deletions ghunt/parsers/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ async def _scrape(self, as_client: httpx.AsyncClient, photo_data: Dict[str, any]
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])
image_url = photo_data.get("imageUrl", "")
if image_url:
self.url = '='.join(image_url.split("=")[:-1])
if (isDefault := photo_data.get("isDefault")):
self.isDefault = isDefault
else:
Expand Down Expand Up @@ -107,8 +109,8 @@ def __init__(self):

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())
if app.get("metadata", {}).get("container") == container_name:
self.apps.append(app.get("appType", "").title())

class PersonContainers(dict):
pass
Expand All @@ -129,45 +131,61 @@ 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"]:
if not (container := email_data.get("metadata", {}).get("container")):
continue
person_email = PersonEmail()
person_email._scrape(email_data)
self.emails[email_data["metadata"]["container"]] = person_email
self.emails[container] = person_email

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

if person_data.get("readOnlyProfileInfo"):
for profile_data in person_data["readOnlyProfileInfo"]:
if not (container := profile_data.get("metadata", {}).get("container")):
continue
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
self.profileInfos[container] = person_profile

# Each photo carries its own container in its metadata, independent of
# readOnlyProfileInfo's containers, so it's keyed separately rather than
# nested in the loop above (that previously mis-attributed every photo
# to whichever profile container happened to be current in the outer loop).
if person_data.get("photo"):
for photo_data in person_data["photo"]:
if not (container := photo_data.get("metadata", {}).get("container")):
continue
person_photo = PersonPhoto()
await person_photo._scrape(as_client, photo_data, "profile_photo")
self.profilePhotos[container] = person_photo

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

if person_data.get("coverPhoto"):
for cover_photo_data in person_data["coverPhoto"]:
if not (container := cover_photo_data.get("metadata", {}).get("container")):
continue
person_cover_photo = PersonPhoto()
await person_cover_photo._scrape(as_client, cover_photo_data, "cover_photo")
container = cover_photo_data.get("metadata", {}).get("container", "unknown")
self.coverPhotos[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"])
if (container := app_data.get("metadata", {}).get("container")):
containers_names.add(container)

for container_name in containers_names:
person_app_reachability = PersonInAppReachability()
Expand Down