mirror of
https://github.com/mxrch/GHunt.git
synced 2026-10-17 04:41:03 +08:00
First commit ! 🎈
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
lib/__pycache__/
|
||||
chromedriver.exe
|
||||
data.txt
|
||||
login.py
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,97 @@
|
||||
import httpx
|
||||
from seleniumwire import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from config import cfg
|
||||
from lib.utils import *
|
||||
import json
|
||||
from os.path import isfile
|
||||
|
||||
cookies = ""
|
||||
auth = ""
|
||||
keys = ""
|
||||
already_exists = False
|
||||
if isfile('data.txt'):
|
||||
try:
|
||||
with open('data.txt', 'r') as f:
|
||||
out = json.loads(f.read())
|
||||
auth = out["auth"]
|
||||
hangouts_token = out["keys"]
|
||||
cookies = out["cookies"]
|
||||
print("[+] Detected stored cookies, checking it")
|
||||
already_exists = True
|
||||
except Exception:
|
||||
print("[-] Stored cookies are corrupted.")
|
||||
choice = input("Do you want to generate new cookies ? (Y/n) ").lower()
|
||||
if choice != "y":
|
||||
exit()
|
||||
|
||||
if not already_exists:
|
||||
cookies = {"__Secure-3PSID": "", "APISID": "", "SAPISID": "", "HSID": "", "CONSENT": "YES+FR.fr+V10+BX"}
|
||||
for name in cookies.keys():
|
||||
if not cookies[name]:
|
||||
cookies[name] = input(f"{name} => ").strip()
|
||||
|
||||
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0'}
|
||||
req = httpx.get("https://docs.google.com/document/u/0/?usp=direct_url", cookies=cookies, headers=headers, allow_redirects=False)
|
||||
exitflag = False
|
||||
if req.status_code != 200:
|
||||
exitflag = True
|
||||
if not exitflag:
|
||||
req2 = httpx.get("https://hangouts.google.com", cookies=cookies, headers=headers, allow_redirects=False)
|
||||
if "myaccount.google.com" not in req2.text:
|
||||
exitflag = True
|
||||
if exitflag:
|
||||
exit("\n[-] Seems like the cookies are invalid, try regenerating them.")
|
||||
print("\n[+] The cookies seems valid ! Generating the Google Docs and Hangouts token...\n")
|
||||
|
||||
if already_exists:
|
||||
choice = input("Do you want to generate new Google Docs and Hangouts tokens ? (Y/n) ").lower()
|
||||
if choice != "y":
|
||||
exit()
|
||||
|
||||
# Google Docs
|
||||
source = req.text
|
||||
trigger = '\"token\":\"'
|
||||
if trigger not in source:
|
||||
exit("[-] I can't find the Google Docs token in the source code...\n")
|
||||
else:
|
||||
gdoc_token = source.split(trigger)[1][:100].split('"')[0]
|
||||
print("Google Docs Token => {}".format(gdoc_token))
|
||||
|
||||
# Hangouts
|
||||
tmprinter = TMPrinter()
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument('--log-level=3')
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
|
||||
chrome_options.add_argument("--headless")
|
||||
options = {
|
||||
'connection_timeout': None # Never timeout, otherwise it floods errors
|
||||
}
|
||||
|
||||
tmprinter.out("Starting browser...")
|
||||
|
||||
driverpath = get_driverpath()
|
||||
driver = webdriver.Chrome(executable_path=driverpath, seleniumwire_options=options, options=chrome_options)
|
||||
|
||||
tmprinter.out("Setting cookies...")
|
||||
driver.get("https://hangouts.google.com/robots.txt")
|
||||
for k,v in cookies.items():
|
||||
driver.add_cookie({'name': k, 'value': v})
|
||||
|
||||
tmprinter.out("Fetching Hangouts homepage...")
|
||||
driver.get("https://hangouts.google.com")
|
||||
tmprinter.out("Waiting for the /v2/people/me/blockedPeople request, it can takes a few minutes...")
|
||||
req = driver.wait_for_request('/v2/people/me/blockedPeople', timeout=120)
|
||||
tmprinter.out("Request found !")
|
||||
#driver.get("about:blank")
|
||||
driver.close()
|
||||
|
||||
auth = req.headers["Authorization"]
|
||||
tmprinter.out("")
|
||||
print("Authorization Token => {}".format(auth))
|
||||
hangouts_token = req.url.split("key=")[1]
|
||||
print("Hangouts Token => {}".format(hangouts_token))
|
||||
|
||||
output = {"auth": auth, "keys": {"gdoc": gdoc_token, "hangouts": hangouts_token}, "cookies": cookies}
|
||||
with open('data.txt', 'w') as f:
|
||||
f.write(json.dumps(output))
|
||||
@@ -0,0 +1,12 @@
|
||||
cfg = dict(
|
||||
regexs = {
|
||||
"albums": r'href=\"\.\/albumarchive\/\d*?\/album\/(.*?)\" jsaction.*?>(?:<.*?>){5}(.*?)<\/div><.*?>(\d*?) ',
|
||||
"photos": r'\],\"(https:\/\/lh\d\.googleusercontent\.com\/.*?)\",\[\"\d{21}\"(?:.*?,){16}\"(.*?)\"',
|
||||
"review_loc_by_id": r'{}\",.*?\[\[null,null,(.*?),(.*?)\]'
|
||||
},
|
||||
|
||||
headless = False, # if True, it doesn't show the browser while scraping GMaps reviews
|
||||
ytb_hunt_always = False, # if True, search the Youtube channel everytime
|
||||
gmaps_radius = 30, # in km. The radius distance to create groups of gmaps reviews.
|
||||
gdocs_public_doc = "1jaEEHZL32t1RUN5WuZEnFpqiEPf_APYKrRBG9LhLdvE" # The public Google Doc to use it as an endpoint, to use Google's Search.
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
import httpx
|
||||
import sys
|
||||
import json
|
||||
from geopy.geocoders import Nominatim
|
||||
from PIL import Image
|
||||
from pprint import pprint
|
||||
from lib.utils import *
|
||||
import lib.youtube as ytb
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from lib.photos import gpics
|
||||
import lib.gmaps as gmaps
|
||||
from config import cfg
|
||||
from os.path import isfile
|
||||
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
exit("Please put an email address.")
|
||||
|
||||
query = sys.argv[1]
|
||||
|
||||
if not isfile('data.txt'):
|
||||
exit("Please generate cookies and tokens first.")
|
||||
|
||||
cookies = ""
|
||||
auth = ""
|
||||
key = ""
|
||||
with open('data.txt', 'r') as f:
|
||||
out = json.loads(f.read())
|
||||
auth = out["auth"]
|
||||
hangouts_token = out["keys"]["hangouts"]
|
||||
cookies = out["cookies"]
|
||||
|
||||
client = httpx.Client(cookies=cookies)
|
||||
|
||||
host = "https://people-pa.clients6.google.com"
|
||||
|
||||
url = "/v2/people/lookup?key={}".format(hangouts_token)
|
||||
body = """id={}&type=EMAIL&matchType=EXACT&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&requestMask.includeContainer=AFFINITY&requestMask.includeContainer=PROFILE&requestMask.includeContainer=DOMAIN_PROFILE&requestMask.includeContainer=ACCOUNT&requestMask.includeContainer=EXTERNAL_ACCOUNT&requestMask.includeContainer=CIRCLE&requestMask.includeContainer=DOMAIN_CONTACT&requestMask.includeContainer=DEVICE_CONTACT&requestMask.includeContainer=GOOGLE_GROUP&requestMask.includeContainer=CONTACT"""
|
||||
|
||||
headers = {
|
||||
"X-HTTP-Method-Override": "GET",
|
||||
"Authorization": auth,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Origin": "https://hangouts.google.com"
|
||||
}
|
||||
|
||||
req = client.post(host + url, data=body.format(query), headers=headers, cookies=cookies)
|
||||
data = json.loads(req.text)
|
||||
if not "matches" in data:
|
||||
exit("Invalid Gmail address.")
|
||||
|
||||
#print(data)
|
||||
geolocator = Nominatim(user_agent="nominatim")
|
||||
print(f"[+] {len(data['matches'])} account found !")
|
||||
|
||||
for user in data["matches"]:
|
||||
print("\n------------------------------\n")
|
||||
|
||||
|
||||
gaiaID = user["personId"][0]
|
||||
email = user["lookupId"]
|
||||
infos = data["people"][gaiaID]
|
||||
|
||||
timestamp = int(infos["metadata"]["lastUpdateTimeMicros"][:-3])
|
||||
last_edit = datetime.utcfromtimestamp(timestamp).strftime("%Y/%m/%d %H:%M:%S (UTC)")
|
||||
|
||||
req = client.get("https://www.google.com/maps/contrib/{}".format(gaiaID))
|
||||
gmaps_source = req.text
|
||||
|
||||
name = gmaps_source.split("Contributions by")[1].split('"')[0].strip()
|
||||
|
||||
print("Name: {}\n\nLast profile edit : {}\n\nEmail : {}\nGoogle ID : {}\n".format(name, last_edit, email, gaiaID))
|
||||
|
||||
profil_pic = infos["photo"][0]["url"]
|
||||
isBot = infos["extendedData"]["hangoutsExtendedData"]["isBot"]
|
||||
if isBot:
|
||||
print("Hangouts Bot : Yes !\n")
|
||||
else:
|
||||
print("Hangouts Bot : No\n")
|
||||
|
||||
ytb_hunt = False
|
||||
try:
|
||||
services = [x["appType"].lower() if x["appType"].lower() != "babel" else "hangouts" for x in infos["inAppReachability"]]
|
||||
if "youtube" in services:
|
||||
ytb_hunt = True
|
||||
print("Activated Google services :")
|
||||
print('\n'.join(["- "+x.capitalize() for x in services]))
|
||||
except KeyError:
|
||||
ytb_hunt = True
|
||||
print("Unable to fetch connected Google services.")
|
||||
|
||||
if ytb_hunt or cfg["ytb_hunt_always"]:
|
||||
req = client.get(profil_pic)
|
||||
img = Image.open(BytesIO(req.content))
|
||||
hash = image_hash(img)
|
||||
data = ytb.get_channels(client, name, cfg)
|
||||
if not data:
|
||||
print("\nYoutube channel not found.")
|
||||
else:
|
||||
confidence, channels = ytb.get_confidence(data, name, hash)
|
||||
if confidence:
|
||||
print(f"\nYoutube channel (confidence => {confidence}%) :")
|
||||
for channel in channels:
|
||||
print(f"- [{channel['name']}] {channel['profil_url']}")
|
||||
possible_usernames = ytb.extract_usernames(channels)
|
||||
if possible_usernames:
|
||||
print("\nPossible usernames found :")
|
||||
for username in possible_usernames:
|
||||
print(f"- {username}")
|
||||
else:
|
||||
print("\nYoutube channel not found.")
|
||||
|
||||
gpics(gaiaID, client, cfg)
|
||||
reviews = gmaps.scrape(gaiaID, client, cookies, cfg)
|
||||
if reviews:
|
||||
confidence, locations = gmaps.get_confidence(reviews, cfg)
|
||||
print(f"\nProbable location (confidence => {confidence}) :")
|
||||
for loc in locations:
|
||||
print(f"- {loc['avg']['town']}, {loc['avg']['country']}")
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
from pprint import pprint
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from seleniumwire import webdriver
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime
|
||||
import httpx
|
||||
from geopy import distance
|
||||
from geopy.geocoders import Nominatim
|
||||
import hashlib
|
||||
from lib.utils import *
|
||||
|
||||
|
||||
def scrape(gaiaID, client, cookies, cfg):
|
||||
|
||||
def get_datetime(datepublished):
|
||||
if datepublished.split()[0] == "a":
|
||||
nb = 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)
|
||||
|
||||
tmprinter = TMPrinter()
|
||||
|
||||
base_url = f"https://www.google.com/maps/contrib/{gaiaID}/reviews?hl=en"
|
||||
print(f"\nGoogle Maps : {base_url.replace('?hl=en', '')}")
|
||||
|
||||
tmprinter.out("Initial request...")
|
||||
|
||||
req = client.get(base_url)
|
||||
source = req.text
|
||||
|
||||
data = source.split(';window.APP_INITIALIZATION_STATE=')[1].split(';window.APP_FLAGS')[0].replace("\\", "")
|
||||
|
||||
if "/maps/reviews/data" not in data:
|
||||
tmprinter.out("")
|
||||
print("=> No reviews")
|
||||
return False
|
||||
|
||||
chrome_options = Options()
|
||||
chrome_options.add_argument('--log-level=3')
|
||||
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
|
||||
if cfg["headless"]:
|
||||
chrome_options.add_argument("--headless")
|
||||
options = {
|
||||
'connection_timeout': None # Never timeout, otherwise it floods errors
|
||||
}
|
||||
|
||||
tmprinter.out("Starting browser...")
|
||||
|
||||
driverpath = get_driverpath()
|
||||
driver = webdriver.Chrome(executable_path=driverpath, seleniumwire_options=options, options=chrome_options)
|
||||
wait = WebDriverWait(driver, 15)
|
||||
|
||||
tmprinter.out("Setting cookies...")
|
||||
driver.get("https://www.google.com/robots.txt")
|
||||
for k,v in cookies.items():
|
||||
driver.add_cookie({'name': k, 'value': v})
|
||||
|
||||
tmprinter.out("Fetching reviews page...")
|
||||
driver.get(base_url)
|
||||
|
||||
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'div.section-scrollbox')))
|
||||
scrollbox = driver.find_element_by_css_selector('div.section-scrollbox')
|
||||
|
||||
tab_info = driver.find_elements_by_css_selector('div.section-tab-info')
|
||||
if tab_info:
|
||||
scroll_max = sum([int(x) for x in tab_info[0].text.split() if x.isdigit()])
|
||||
else:
|
||||
return False
|
||||
|
||||
print(f"=> {scroll_max} reviews found ! ")
|
||||
|
||||
timeout = scroll_max*1.25
|
||||
timeout_start = time.time()
|
||||
reviews_elements = driver.find_elements_by_xpath('//div[@data-review-id][@aria-label]')
|
||||
tmprinter.out(f"Fetching reviews... ({len(reviews_elements)}/{scroll_max})")
|
||||
while len(reviews_elements) < scroll_max:
|
||||
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", scrollbox)
|
||||
reviews_elements = driver.find_elements_by_xpath('//div[@data-review-id][@aria-label]')
|
||||
tmprinter.out(f"Fetching reviews... ({len(reviews_elements)}/{scroll_max})")
|
||||
if time.time() > timeout_start + timeout:
|
||||
tmprinter.out(f"Timeout while fetching reviews !")
|
||||
break
|
||||
|
||||
tmprinter.out("Fetching internal requests history...")
|
||||
requests = [r.url for r in driver.requests if "locationhistory" in r.url]
|
||||
tmprinter.out(f"Fetching internal requests... (0/{len(requests)})")
|
||||
for nb,load in enumerate(requests):
|
||||
req = client.get(load)
|
||||
data += req.text.replace('\n', '')
|
||||
tmprinter.out(f"Fetching internal requests... ({nb+1}/{len(requests)})")
|
||||
|
||||
tmprinter.out(f"Fetching reviews location... (0/{len(reviews_elements)})")
|
||||
reviews = []
|
||||
for nb,review in enumerate(reviews_elements):
|
||||
id = review.get_attribute("data-review-id")
|
||||
location = re.compile(cfg["regexs"]["review_loc_by_id"].format(id)).findall(data)[0]
|
||||
date = get_datetime(review.find_element_by_css_selector('span.section-review-publish-date').text)
|
||||
reviews.append({"location": location, "date": date})
|
||||
tmprinter.out(f"Fetching reviews location... ({nb+1}/{len(reviews_elements)})")
|
||||
|
||||
return reviews
|
||||
|
||||
def avg_location(locs):
|
||||
latitude = []
|
||||
longitude = []
|
||||
for loc in locs:
|
||||
latitude.append(float(loc[0]))
|
||||
longitude.append(float(loc[1]))
|
||||
|
||||
latitude = sum(latitude)/len(latitude)
|
||||
longitude = sum(longitude)/len(longitude)
|
||||
return latitude, longitude
|
||||
|
||||
def translate_confidence(percents):
|
||||
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 get_confidence(data, cfg):
|
||||
|
||||
geolocator = Nominatim(user_agent="nominatim")
|
||||
tmprinter = TMPrinter()
|
||||
radius = cfg["gmaps_radius"]
|
||||
|
||||
locations = {}
|
||||
tmprinter.out(f"Calculation of the distance of each review...")
|
||||
for nb,review in enumerate(data):
|
||||
hash = hashlib.md5(str(review).encode()).hexdigest()
|
||||
if hash not in locations:
|
||||
locations[hash] = {"dates": [], "locations": [], "range": None, "score": 0}
|
||||
location = review["location"]
|
||||
for review2 in data:
|
||||
location2 = review2["location"]
|
||||
dis = distance.distance(location, location2).km
|
||||
|
||||
if dis <= radius:
|
||||
locations[hash]["dates"].append(review2["date"])
|
||||
locations[hash]["locations"].append(review2["location"])
|
||||
|
||||
maxdate = max(locations[hash]["dates"])
|
||||
mindate = min(locations[hash]["dates"])
|
||||
locations[hash]["range"] = maxdate - mindate
|
||||
tmprinter.out(f"Calculation of the distance of each review ({nb}/{len(data)})...")
|
||||
|
||||
tmprinter.out("")
|
||||
|
||||
|
||||
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 hash in locations:
|
||||
if hash in to_del:
|
||||
continue
|
||||
for hash2 in locations:
|
||||
if hash2 in to_del or hash == hash2:
|
||||
continue
|
||||
if all([loc in locations[hash]["locations"] for loc in locations[hash2]["locations"]]):
|
||||
to_del.append(hash2)
|
||||
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
|
||||
|
||||
#for hash,loc in locations.items():
|
||||
# print(f"{hash} => {len(loc['locations'])} ({int(loc['score'])/40*100})")
|
||||
|
||||
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(data) < 4:
|
||||
panel/=2
|
||||
confidence = translate_confidence(panel/maxscore*100)
|
||||
for nb,loc in enumerate(locs):
|
||||
avg = avg_location(loc["locations"])
|
||||
location = geolocator.reverse(f"{avg[0]}, {avg[1]}").raw["address"]
|
||||
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.out("")
|
||||
return confidence, locs
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
from PIL import Image, ExifTags
|
||||
from PIL.ExifTags import TAGS, GPSTAGS
|
||||
from pprint import pprint
|
||||
from datetime import datetime
|
||||
from geopy.geocoders import Nominatim
|
||||
|
||||
|
||||
class ExifEater():
|
||||
|
||||
def __init__(self):
|
||||
self.devices = {}
|
||||
self.softwares = {}
|
||||
self.locations = {}
|
||||
self.geolocator = Nominatim(user_agent="nominatim")
|
||||
|
||||
def get_GPS(self, img):
|
||||
location = ""
|
||||
geoaxis = {}
|
||||
geotags = {}
|
||||
try:
|
||||
exif = img._getexif()
|
||||
|
||||
for (idx, tag) in TAGS.items():
|
||||
if tag == 'GPSInfo':
|
||||
if idx in exif:
|
||||
for (key, val) in GPSTAGS.items():
|
||||
if key in exif[idx]:
|
||||
geotags[val] = exif[idx][key]
|
||||
|
||||
for axis in ["Latitude", "Longitude"]:
|
||||
dms = geotags[f'GPS{axis}']
|
||||
ref = geotags[f'GPS{axis}Ref']
|
||||
|
||||
degrees = dms[0][0] / dms[0][1]
|
||||
minutes = dms[1][0] / dms[1][1] / 60.0
|
||||
seconds = dms[2][0] / dms[2][1] / 3600.0
|
||||
|
||||
if ref in ['S', 'W']:
|
||||
degrees = -degrees
|
||||
minutes = -minutes
|
||||
seconds = -seconds
|
||||
|
||||
geoaxis[axis] = round(degrees + minutes + seconds, 5)
|
||||
location = self.geolocator.reverse("{}, {}".format(geoaxis["Latitude"], geoaxis["Longitude"])).raw["address"]
|
||||
except Exception:
|
||||
return ""
|
||||
else:
|
||||
if location:
|
||||
return f'{location["village"]}, {location["country"]}'
|
||||
else:
|
||||
return ""
|
||||
|
||||
def feed(self, img):
|
||||
if img.getexif():
|
||||
location = self.get_GPS(img)
|
||||
exif = { ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in ExifTags.TAGS }
|
||||
interesting_fields = ["Make", "Model", "DateTime", "Software"]
|
||||
metadata = {k:v for k,v in exif.items() if k in interesting_fields}
|
||||
try:
|
||||
date = datetime.strptime(metadata["DateTime"], '%Y:%m:%d %H:%M:%S')
|
||||
is_date_valid = "Valid"
|
||||
except:
|
||||
date = None
|
||||
is_date_valid = "Invalid"
|
||||
|
||||
if location:
|
||||
if location not in self.locations:
|
||||
self.locations[location] = {"Valid": [], "Invalid": []}
|
||||
self.locations[location][is_date_valid].append(date)
|
||||
if "Make" in metadata and "Model" in metadata:
|
||||
if metadata["Model"] not in self.devices:
|
||||
self.devices[metadata["Model"]] = {"Make": metadata["Make"], "History": {"Valid": [], "Invalid": []}, "Firmwares": {}}
|
||||
self.devices[metadata["Model"]]["History"]["Valid"].append(date)
|
||||
if "Software" in metadata:
|
||||
if metadata["Software"] not in self.devices[metadata["Model"]]["Firmwares"]:
|
||||
self.devices[metadata["Model"]]["Firmwares"][metadata["Software"]] = {"Valid": [], "Invalid": []}
|
||||
self.devices[metadata["Model"]]["Firmwares"][metadata["Software"]][is_date_valid].append(date)
|
||||
elif "Software" in metadata:
|
||||
if metadata["Software"] not in self.softwares:
|
||||
self.softwares[metadata["Software"]] = {"Valid": [], "Invalid": []}
|
||||
self.softwares[metadata["Software"]][is_date_valid].append(date)
|
||||
|
||||
def give_back(self):
|
||||
return self.locations, self.devices
|
||||
|
||||
def output(self):
|
||||
bkn = '\n' # to use in f-strings
|
||||
|
||||
def picx(n):
|
||||
return "s" if n > 1 else ""
|
||||
|
||||
def print_dates(dates_list):
|
||||
dates = {}
|
||||
dates["max"] = max(dates_list).strftime("%Y/%m/%d")
|
||||
dates["min"] = min(dates_list).strftime("%Y/%m/%d")
|
||||
if dates["max"] == dates["min"]:
|
||||
return dates["max"]
|
||||
else:
|
||||
return f'{dates["min"]} -> {dates["max"]}'
|
||||
|
||||
#pprint((self.devices, self.softwares, self.locations))
|
||||
|
||||
devices = self.devices
|
||||
if devices:
|
||||
print(f"[+] {len(devices)} device{picx(len(devices))} found !")
|
||||
for model, data in devices.items():
|
||||
make = data["Make"]
|
||||
if model.lower().startswith(make.lower()):
|
||||
model = model[len(make):].strip()
|
||||
n = len(data["History"]["Valid"] + data["History"]["Invalid"])
|
||||
for validity,dateslist in data["History"].items():
|
||||
if dateslist and ((validity == "Valid") or (validity == "Invalid" and not data["History"]["Valid"])):
|
||||
if validity == "Valid":
|
||||
dates = print_dates(data["History"]["Valid"])
|
||||
elif validity == "Valid" and data["History"]["Invalid"]:
|
||||
dates = print_dates(data["History"]["Valid"])
|
||||
dates += " (+ ?)"
|
||||
elif validity == "Invalid" and not data["History"]["Valid"]:
|
||||
dates = "?"
|
||||
print(f"{bkn if data['Firmwares'] else ''}- {make.capitalize()} {model} ({n} pic{picx(n)}) [{dates}]")
|
||||
if data["Firmwares"]:
|
||||
n = len(data['Firmwares'])
|
||||
print(f"-> {n} Firmware{picx(n)} found !")
|
||||
for firmware,firmdata in data["Firmwares"].items():
|
||||
for validity2,dateslist2 in firmdata.items():
|
||||
if dateslist2 and ((validity2 == "Valid") or (validity2 == "Invalid" and not firmdata["Valid"])):
|
||||
if validity2 == "Valid":
|
||||
dates2 = print_dates(firmdata["Valid"])
|
||||
elif validity2 == "Valid" and firmdata["Invalid"]:
|
||||
dates2 = print_dates(firmdata["Valid"])
|
||||
dates2 += " (+ ?)"
|
||||
elif validity2 == "Invalid" and not firmdata["Valid"]:
|
||||
dates2 = "?"
|
||||
print(f"--> {firmware} [{dates2}]")
|
||||
|
||||
locations = self.locations
|
||||
if locations:
|
||||
print(f"\n[+] {len(locations)} location{picx(len(locations))} found !")
|
||||
for location, data in locations.items():
|
||||
n = len(data["Valid"] + data["Invalid"])
|
||||
for validity, dateslist in data.items():
|
||||
if dateslist and ((validity == "Valid") or (validity == "Invalid" and not data["Valid"])):
|
||||
if validity == "Valid":
|
||||
dates = print_dates(data["Valid"])
|
||||
elif validity == "Valid" and data["Invalid"]:
|
||||
dates = print_dates(data["Valid"])
|
||||
dates += " (+ ?)"
|
||||
elif validity == "Invalid" and not data["Valid"]:
|
||||
dates = "?"
|
||||
print(f"- {location} ({n} pic{picx(n)}) [{dates}]")
|
||||
|
||||
softwares = self.softwares
|
||||
if softwares:
|
||||
print(f"\n[+] {len(softwares)} software{picx(len(softwares))} found !")
|
||||
for software, data in softwares.items():
|
||||
n = len(data["Valid"] + data["Invalid"])
|
||||
for validity, dateslist in data.items():
|
||||
if dateslist and ((validity == "Valid") or (validity == "Invalid" and not data["Valid"])):
|
||||
if validity == "Valid":
|
||||
dates = print_dates(data["Valid"])
|
||||
elif validity == "Valid" and data["Invalid"]:
|
||||
dates = print_dates(data["Valid"])
|
||||
dates += " (+ ?)"
|
||||
elif validity == "Invalid" and not data["Valid"]:
|
||||
dates = "?"
|
||||
print(f"- {software} ({n} pic{picx(n)}) [{dates}]")
|
||||
|
||||
if not devices and not locations and not softwares:
|
||||
print("- Nothing found")
|
||||
@@ -0,0 +1,44 @@
|
||||
import httpx
|
||||
import re
|
||||
from io import BytesIO
|
||||
from lib.metadata import ExifEater
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def gpics(gaiaID, client, cfg):
|
||||
baseurl = "https://get.google.com/albumarchive/"
|
||||
|
||||
print(f"\nGoogle Photos : {baseurl + gaiaID}")
|
||||
req = client.get(baseurl + gaiaID)
|
||||
results = re.compile(cfg["regexs"]["albums"]).findall(req.text)
|
||||
|
||||
list_albums_length = len(results)
|
||||
|
||||
if results:
|
||||
exifeater = ExifEater()
|
||||
pics = []
|
||||
for album in results:
|
||||
album_name = album[1]
|
||||
album_link = baseurl + gaiaID + "/album/" + album[0]
|
||||
album_length = int(album[2])
|
||||
|
||||
if album_length >= 1:
|
||||
req = client.get(album_link)
|
||||
source = req.text.replace('\n', '')
|
||||
open('tmp.html', 'w').write(source)
|
||||
results_pics = re.compile(cfg["regexs"]["photos"]).findall(source)
|
||||
for pic in results_pics:
|
||||
pic_name = pic[1]
|
||||
pic_link = pic[0]
|
||||
pics.append(pic_link)
|
||||
|
||||
print(f"=> {list_albums_length} albums{', '+str(len(pics))+' photos' if list_albums_length else ''}")
|
||||
for pic in pics:
|
||||
req = client.get(pic)
|
||||
img = Image.open(BytesIO(req.content))
|
||||
exifeater.feed(img)
|
||||
|
||||
print("\nSearching metadata...")
|
||||
exifeater.output()
|
||||
else:
|
||||
print("=> No album")
|
||||
@@ -0,0 +1,33 @@
|
||||
import httpx
|
||||
import json
|
||||
from pprint import pprint
|
||||
|
||||
def search(query, cfg):
|
||||
cookies = ""
|
||||
token = ""
|
||||
with open('data.txt', 'r') as f:
|
||||
out = json.loads(f.read())
|
||||
token = out["keys"]["gdoc"]
|
||||
cookies = out["cookies"]
|
||||
|
||||
doc = cfg["gdocs_public_doc"]
|
||||
size = 1000
|
||||
|
||||
data = {"request": '["documentsuggest.search.search_request","{}",[{}],null,1]'.format(query, size)}
|
||||
|
||||
req = httpx.post('https://docs.google.com/document/d/{}/explore/search?token={}'.format(doc, token), cookies=cookies, data=data)
|
||||
if req.status_code != 200:
|
||||
print("Error : request gives {}".format(req.status_code))
|
||||
exit()
|
||||
|
||||
output = json.loads(req.text.replace(")]}'", ""))
|
||||
#pprint(output)
|
||||
|
||||
results = []
|
||||
for result in output[0][1]:
|
||||
link = result[0][0]
|
||||
title = result[0][1]
|
||||
desc = result[0][2]
|
||||
results.append({"title": title, "desc": desc, "link": link})
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,42 @@
|
||||
import imagehash
|
||||
from platform import system
|
||||
from os.path import isfile
|
||||
|
||||
|
||||
def image_hash(img):
|
||||
hash = str(imagehash.average_hash(img))
|
||||
return hash
|
||||
|
||||
class TMPrinter():
|
||||
def __init__(self):
|
||||
self.max_len = 0
|
||||
|
||||
def out(self, text):
|
||||
if len(text) > self.max_len:
|
||||
self.max_len = len(text)
|
||||
else:
|
||||
text += (" " * (self.max_len - len(text)))
|
||||
print(text, end='\r')
|
||||
|
||||
def sanitize_location(location):
|
||||
if "city" in location:
|
||||
town = location["city"]
|
||||
elif "village" in location:
|
||||
town = location["village"]
|
||||
elif "town" in location:
|
||||
town = location["town"]
|
||||
else:
|
||||
town = location["municipality"]
|
||||
location["town"] = town
|
||||
return location
|
||||
|
||||
def get_driverpath():
|
||||
if system() == "Windows":
|
||||
driverpath = "chromedriver.exe"
|
||||
else:
|
||||
driverpath = "chromedriver"
|
||||
|
||||
if isfile(driverpath):
|
||||
return driverpath
|
||||
else:
|
||||
exit("The chromedriver is missing.\nPlease put it in the GHunt directory.")
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import httpx
|
||||
import json
|
||||
import urllib.parse
|
||||
from PIL import Image
|
||||
from lib.utils import *
|
||||
from lib.search import search as gdoc_search
|
||||
from pprint import pprint
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def youtube_channel_search(client, query):
|
||||
link = "https://www.youtube.com/results?search_query={}&sp=EgIQAg%253D%253D"
|
||||
req = client.get(link.format(urllib.parse.quote(query)))
|
||||
source = req.text
|
||||
data = json.loads(source.split('window["ytInitialData"] = ')[1].split('window["ytInitialPlayerResponse"]')[0].split(';\n')[0])
|
||||
channels = data["contents"]["twoColumnSearchResultsRenderer"]["primaryContents"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"]
|
||||
results = {"channels": [], "length": len(channels)}
|
||||
try:
|
||||
for channel in channels:
|
||||
if len(results["channels"]) >= 10:
|
||||
break
|
||||
title = channel["channelRenderer"]["title"]["simpleText"]
|
||||
if not query.lower() in title.lower():
|
||||
continue
|
||||
avatar_link = channel["channelRenderer"]["thumbnail"]["thumbnails"][0]["url"].split('=')[0]
|
||||
if avatar_link[:2] == "//":
|
||||
avatar_link = "https:"+avatar_link
|
||||
profil_url = "https://youtube.com" + channel["channelRenderer"]["navigationEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||
req = client.get(avatar_link)
|
||||
img = Image.open(BytesIO(req.content))
|
||||
hash = image_hash(img)
|
||||
results["channels"].append({"profil_url": profil_url, "name": title, "hash": hash})
|
||||
return results
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def youtube_channel_search_gdocs(client, query, cfg):
|
||||
search_query = f"site:youtube.com/channel \\\"{query}\\\""
|
||||
search_results = gdoc_search(search_query, cfg)
|
||||
channels = []
|
||||
for result in search_results:
|
||||
sanitized = "https://youtube.com/"+('/'.join(result["link"].split('/')[3:5]))
|
||||
if sanitized not in channels:
|
||||
channels.append(sanitized)
|
||||
|
||||
if not channels:
|
||||
return False
|
||||
results = {"channels": [], "length": len(channels)}
|
||||
channels = channels[:5]
|
||||
for profil_url in channels:
|
||||
req = client.get(profil_url)
|
||||
source = req.text
|
||||
|
||||
data = json.loads(source.split('window["ytInitialData"] = ')[1].split('window["ytInitialPlayerResponse"]')[0].split(';\n')[0])
|
||||
avatar_link = data["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].split('=')[0]
|
||||
req = client.get(avatar_link)
|
||||
img = Image.open(BytesIO(req.content))
|
||||
hash = image_hash(img)
|
||||
title = data["metadata"]["channelMetadataRenderer"]["title"]
|
||||
results["channels"].append({"profil_url": profil_url, "name": title, "hash": hash})
|
||||
return results
|
||||
|
||||
def get_channels(client, query, cfg):
|
||||
from_youtube = youtube_channel_search(client, query)
|
||||
from_gdocs = youtube_channel_search_gdocs(client, query, cfg)
|
||||
to_process = []
|
||||
if from_youtube:
|
||||
from_youtube["origin"] = "youtube"
|
||||
to_process.append(from_youtube)
|
||||
if from_gdocs:
|
||||
from_gdocs["origin"] = "gdocs"
|
||||
to_process.append(from_gdocs)
|
||||
if not to_process:
|
||||
return False
|
||||
return to_process
|
||||
|
||||
def get_confidence(data, query, hash):
|
||||
score_steps = 4
|
||||
|
||||
for source_nb, source in enumerate(data):
|
||||
for channel_nb, channel in enumerate(source["channels"]):
|
||||
score = 0
|
||||
|
||||
if hash == channel["hash"]:
|
||||
score += score_steps*4
|
||||
if query == channel["name"]:
|
||||
score += score_steps*3
|
||||
if query in channel["name"]:
|
||||
score += score_steps*2
|
||||
if ((source["origin"] == "youtube" and source["length"] <= 5) or
|
||||
(source["origin"] == "google" and source["length"] <= 4)):
|
||||
score += score_steps
|
||||
data[source_nb]["channels"][channel_nb]["score"] = score
|
||||
|
||||
channels = []
|
||||
for source in data:
|
||||
for channel in source["channels"]:
|
||||
found_better = False
|
||||
for source2 in data:
|
||||
for channel2 in source["channels"]:
|
||||
if channel["profil_url"] == channel2["profil_url"]:
|
||||
if channel2["score"] > channel["score"]:
|
||||
found_better = True
|
||||
break
|
||||
if found_better:
|
||||
break
|
||||
if found_better:
|
||||
continue
|
||||
else:
|
||||
channels.append(channel)
|
||||
channels = sorted([json.loads(chan) for chan in set([json.dumps(channel) for channel in channels])], key=lambda k: k['score'], reverse=True)
|
||||
panels = sorted(set([c["score"] for c in channels]), reverse=True)
|
||||
if panels and panels[0] <= 0:
|
||||
return 0, []
|
||||
|
||||
maxscore = sum([p*score_steps for p in range(1,score_steps+1)])
|
||||
for panel in panels:
|
||||
chans = [c for c in channels if c["score"] == panel]
|
||||
if len(chans) > 1:
|
||||
panel-=5
|
||||
return (panel/maxscore*100), chans
|
||||
|
||||
def extract_usernames(channels):
|
||||
return [chan['profil_url'].split("/user/")[1] for chan in channels if "/user/" in chan['profil_url']]
|
||||
@@ -0,0 +1,5 @@
|
||||
geopy
|
||||
httpx
|
||||
selenium-wire
|
||||
selenium
|
||||
imagehash
|
||||
Reference in New Issue
Block a user