Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add web module (JSON API and SPA) #474

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Features :
- Fully async
- JSON export
- Browser extension to ease login
- HTTP API

# ✔️ Requirements
- Python >= 3.10
Expand Down Expand Up @@ -57,14 +58,15 @@ The extension is available on the following stores :\

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

positional arguments:
{login,email,gaia,drive}
login (--clean) Authenticate GHunt to Google.
email (--json) Get information on an email address.
gaia (--json) Get information on a Gaia ID.
drive (--json) Get information on a Drive file or folder.
web (--api) Launch web app.

options:
-h, --help show this help message and exit
Expand Down
19 changes: 14 additions & 5 deletions ghunt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,31 @@ def parse_and_run():
parser_drive.add_argument("file_id", help="Example: 1N__vVu4c9fCt4EHxfthUNzVOs_tp8l6tHcMBnpOZv_M")
parser_drive.add_argument('--json', type=str, help="File to write the JSON output to.")

### Web module
parser_drive = subparsers.add_parser('web', help="Launch web app.")
parser_drive.add_argument('--host', type=str, help="Host. Defaults to 0.0.0.0.", default='0.0.0.0')
parser_drive.add_argument('--port', type=int, help="Port. Defaults to 8080.", default=8080)
parser_drive.add_argument('--api', action='store_true', help="API only. No front-end.")

### Parsing
args = parser.parse_args(args=None if sys.argv[1:] else ['--help'])
process_args(args)

def process_args(args: argparse.Namespace):
import trio
import anyio
match args.module:
case "login":
from ghunt.modules import login
trio.run(login.check_and_login, None, args.clean)
anyio.run(login.check_and_login, None, args.clean)
case "email":
from ghunt.modules import email
trio.run(email.hunt, None, args.email_address, args.json)
anyio.run(email.hunt, None, args.email_address, args.json)
case "gaia":
from ghunt.modules import gaia
trio.run(gaia.hunt, None, args.gaia_id, args.json)
anyio.run(gaia.hunt, None, args.gaia_id, args.json)
case "drive":
from ghunt.modules import drive
trio.run(drive.hunt, None, args.file_id, args.json)
anyio.run(drive.hunt, None, args.file_id, args.json)
case "web":
from ghunt.modules import web
anyio.run(web.hunt, None, args.host, args.port, args.api)
6 changes: 3 additions & 3 deletions ghunt/helpers/ia.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from ghunt.apis.vision import VisionHttp

import httpx
import trio
import anyio

from base64 import b64encode

Expand All @@ -18,7 +18,7 @@ async def detect_face(vision_api: VisionHttp, as_client: httpx.AsyncClient, imag
rate_limited, are_faces_found, faces_results = await vision_api.detect_faces(as_client, image_content=encoded_image)
if not rate_limited:
break
await trio.sleep(0.5)
await anyio.sleep(0.5)
else:
exit("\n[-] Vision API keeps rate-limiting.")

Expand All @@ -30,4 +30,4 @@ async def detect_face(vision_api: VisionHttp, as_client: httpx.AsyncClient, imag
else:
gb.rc.print(f"🎭 No face detected.", style="italic bright_black")

return faces_results
return faces_results
205 changes: 205 additions & 0 deletions ghunt/modules/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from ghunt.helpers.utils import get_httpx_client
from ghunt.objects.base import GHuntCreds
from ghunt.objects.encoders import GHuntEncoder
from ghunt.apis.peoplepa import PeoplePaHttp
from ghunt.apis.drive import DriveHttp
from ghunt.helpers import gmaps, playgames, auth, calendar
from ghunt.helpers.drive import get_users_from_file

import json
import httpx
import humanize
import uvicorn
from uhttp import App, Request, Response


app = App()


def _jsonify(obj):
return Response(
status=200,
headers={'content-type': 'application/json'},
body=json.dumps(obj, cls=GHuntEncoder, indent=4).encode()
)


@app.get(r'/email/(?P<email>[\[email protected]]+)')
async def _email(request: Request):
body = {
'account': {
'id': '',
'name': '',
'email': '',
'picture': '',
'cover': '',
'last_updated': ''
},
'maps': {
'stats': {},
'reviews': [],
'photos': [],
},
'calendar': {
'details': {},
'events': []
},
'player': {
'id': '',
'name': '',
'gamertag': '',
'title': '',
'avatar': '',
'banner': '',
'experience': '',
}
}

people_pa = PeoplePaHttp(request.state['creds'])
found, person = await people_pa.people_lookup(
request.state['client'],
request.params['email'],
params_template='max_details'
)
if not found:
return _jsonify(body)
body['account'].update(
id=person.personId,
name=person.names['PROFILE'].fullname,
email=person.emails['PROFILE'].value,
picture=person.profilePhotos['PROFILE'].url,
cover=person.coverPhotos['PROFILE'].url,
last_updated=person.sourceIds['PROFILE'].lastUpdated.strftime(
'%Y/%m/%d %H:%M:%S (UTC)'
)
)

err, stats, reviews, photos = await gmaps.get_reviews(
request.state['client'],
person.personId
)
if not err:
body['maps'].update(stats=stats, reviews=reviews, photos=photos)

found, details, events = await calendar.fetch_all(
request.state['creds'],
request.state['client'],
request.params['email']
)
if found:
body['calendar'].update(details=details, events=events)

player_results = await playgames.search_player(
request.state['creds'],
request.state['client'],
request.params['email']
)
if player_results:
_, player = await playgames.player(
request.state['creds'],
request.state['client'],
player_results[0].id
)
body['games'].update(
id=player.profile.id,
name=player.profile.display_name,
gamertag=player.profile.gamertag,
title=player.profile.title,
avatar=player.profile.avatar_url,
banner=player.profile.banner_url_landscape,
experience=player.profile.experience_info.current_xp,
)

return _jsonify(body)


@app.get(r'/gaia/(?P<gaia>\d+)')
async def _gaia(request: Request):
body = {
'id': '',
'name': '',
'email': '',
'picture': '',
'cover': '',
'last_updated': ''
}

people_pa = PeoplePaHttp(request.state['creds'])
found, person = await people_pa.people(
request.state['client'],
request.params['gaia'],
params_template='max_details'
)
if found:
body.update(
id=person.personId,
name=person.names['PROFILE'].fullname,
email=person.emails['PROFILE'].value,
picture=person.profilePhotos['PROFILE'].url,
cover=person.coverPhotos['PROFILE'].url,
lastUpdated=person.sourceIds['PROFILE'].lastUpdated.strftime(
'%Y/%m/%d %H:%M:%S (UTC)'
)
)

return _jsonify(body)


@app.get(r'/drive/(?P<drive>\w+)')
async def _drive(request: Request):
body = {
'id': '',
'title': '',
'size': '',
'icon': '',
'thumbnail': '',
'description': '',
'created': '',
'modified': '',
'users': [],
}

drive = DriveHttp(request.state['creds'])
found, file = await drive.get_file(
request.state['client'], request.params['drive']
)
if found:
body.update(
id=file.id,
title=file.title,
size=humanize.naturalsize(file.file_size),
icon=file.icon_link,
thumbnail=file.thumbnail_link,
description=file.description,
created=file.created_date.strftime('%Y/%m/%d %H:%M:%S (UTC)'),
modified=file.modified_date.strftime('%Y/%m/%d %H:%M:%S (UTC)'),
users=get_users_from_file(file)
)

return _jsonify(body)


@app.after
def _cors(request: Request, response: Response):
if request.headers.get('origin'):
response.headers['access-control-allow-origin'] = '*'


async def hunt(as_client: httpx.AsyncClient, host: str, port: int, api: bool):
@app.startup
def setup_ghunt(state):
state['client'] = as_client or get_httpx_client()
state['creds'] = GHuntCreds()
state['creds'].load_creds()
if not state['creds'].are_creds_loaded():
raise RuntimeError('Missing credentials')
if not auth.check_cookies(state['creds'].cookies):
raise RuntimeError('Invalid cookies')

if not api:
from ghunt import static
app.mount(static.app)

config = uvicorn.Config(app, host=host, port=port)
server = uvicorn.Server(config)
await server.serve()
8 changes: 4 additions & 4 deletions ghunt/objects/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ghunt.helpers.auth import *

import httpx
import trio
import anyio

from datetime import datetime, timezone
from typing import *
Expand All @@ -25,7 +25,7 @@ def __init__(self):
self.creds: GHuntCreds = None
self.headers: Dict[str, str] = {}
self.cookies: Dict[str, str] = {}
self.gen_token_lock: trio.Lock = None
self.gen_token_lock: anyio.Lock = None

self.authentication_mode: str = ""
self.require_key: str = ""
Expand All @@ -39,7 +39,7 @@ def _load_api(self, creds: GHuntCreds, headers: Dict[str, str]):
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 = trio.Lock()
self.gen_token_lock = anyio.Lock()

cookies = {}
if self.authentication_mode in ["sapisidhash", "cookies_only"]:
Expand Down Expand Up @@ -149,4 +149,4 @@ def recursive_merge(obj1, obj2, module_name: str) -> any:
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)
self = recursive_merge(self, obj, module_name)
5 changes: 5 additions & 0 deletions ghunt/static/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
from uhttp_static import static


app = static(os.path.dirname(__file__))
21 changes: 21 additions & 0 deletions ghunt/static/assets/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
body {
max-width: 700px;
margin: 2rem auto;
padding: 0 1rem;
}

h1 {
text-align: center;
margin-bottom: 1rem;
color: var(--bs-emphasis-color);
font-weight: bold;
font-size: 3em;
}

a {
text-decoration: none;
}

* {
box-shadow: none !important;
}
Binary file added ghunt/static/assets/img/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions ghunt/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GHunt</title>
<link rel="icon" href="assets/img/favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<h1 class="user-select-none">GHunt</h1>
<form>
<div class="input-group border rounded">
<select class="btn ps-3 pe-1 border-0 rounded-0 bg-transparent text-muted text-start">
<option value="email">Email</option>
<option value="gaia">Gaia</option>
<option value="drive">Drive</option>
</select>
<input type="text" class="form-control border-0 rounded-0 bg-transparent" required spellcheck="false" autocomplete="false" autofocus>
<button type="submit" class="btn ps-1 pe-3 border-0 rounded-0 bg-transparent text-muted"><i class="bi bi-search"></i></button>
</div>
</form>
<script>
const queryForm = document.querySelector("form");
const querySelect = document.querySelector("form select");
const queryInput = document.querySelector("form input");

queryForm.addEventListener("submit", (event) => {
event.preventDefault();
location.href = `/${querySelect.value}/${queryInput.value}`;
});
</script>
</body>
</html>
Loading