diff --git a/backup.py b/backup.py index bf8a463..a6ddfcd 100644 --- a/backup.py +++ b/backup.py @@ -4,10 +4,15 @@ # License v3.0. A copy of this license is available at # https://www.gnu.org/licenses/agpl-3.0.en.html -import base64, requests, time, json, os +import base64 +import time +import json +import os from datetime import datetime import console +from main import request_client, colours, config +from client_info import build_headers class backup(): def __init__(self, token, c: console.prnt, version) -> None: @@ -17,14 +22,16 @@ def __init__(self, token, c: console.prnt, version) -> None: self.fatal_error = False self.c = c - token_check = requests.get("https://discord.com/api/v9/users/@me", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + token_check = request_client.get("https://discord.com/api/v9/users/@me", + headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True) + ) if token_check.status_code != 200: self.fatal_error = "Invalid Token" else: self.user_me = token_check.json() self.user_info() self.relationships() - fav_gifs_msg = self.get_favourite_gifs() + self.get_favourite_gifs() self.guilds() print() self.group_chats() @@ -51,67 +58,24 @@ def __init__(self, token, c: console.prnt, version) -> None: print() self.c.success(f"Backup Complete!") - self.c.info(f"User Info + Avatar: {self.c.clnt.maincol}Done") - self.c.info(f"Guild Folders: {self.c.clnt.maincol}Done") - self.c.info(f"Favourited GIFs: {self.c.clnt.maincol}Done") - self.c.info(f"Guilds: {self.c.clnt.maincol}{guild1}/{guild2}") - self.c.info(f"Group Chats: {self.c.clnt.maincol}{self.gc_success}/{self.gc_success + self.gc_fail}") + self.c.info(f"User Info + Avatar: {colours['main_colour']}Done") + self.c.info(f"Guild Folders: {colours['main_colour']}Done") + self.c.info(f"Favourited GIFs: {colours['main_colour']}Done") + self.c.info(f"Guilds: {colours['main_colour']}{guild1}/{guild2}") + self.c.info(f"Group Chats: {colours['main_colour']}{self.gc_success}/{self.gc_success + self.gc_fail}") - if len(self.g_failed) != 0: self.c.warn(f"Failed Guilds:") - for guild in self.g_failed: self.c.warn(f"{guild['name']}", indent=2) + if len(self.g_failed) != 0: + self.c.warn(f"Failed Guilds:") + for guild in self.g_failed: + self.c.warn(f"{guild['name']}", indent=2) self.c.info(f"Relationships:") - self.c.info(f"Friends: {self.c.clnt.maincol}{len(self.friends)}", indent=2) - self.c.info(f"Blocked: {self.c.clnt.maincol}{len(self.blocked)}", indent=2) - self.c.info(f"Incoming: {self.c.clnt.maincol}{len(self.incoming)}", indent=2) - self.c.info(f"Outgoing: {self.c.clnt.maincol}{len(self.outgoing)}", indent=2) - self.c.info(f"DM Historys: {self.c.clnt.maincol}{len(self.dm_historys)}", indent=2) - self.c.info(f"Time Elapsed: {self.c.clnt.maincol}{self._show_time(int(self.after - self.before))}") - - - - def _headers(self, method, superprop=False, debugoptions=False, discordlocale=False, authorization=False, origin=False, referer="https://discord.com/channels/@me", context=False): - headers = { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9", - "Cookie": "locale=en-GB", - "Referer": referer, - "Sec-Ch-Ua": '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"', - "Sec-Ch-Ua-Mobile": "?0", - "Sec-Ch-Ua-Platform": '"Windows"', - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36" - } - - if referer is False: - del headers["Referer"] - - if method != "get": - headers["Content-Type"] = "application/json" - headers["Origin"] = "https://discord.com" - - if authorization is True: - headers["Authorization"] = self.token - if origin != False: - headers["Origin"] = origin - if debugoptions is True: - headers["X-Debug-Options"] = "bugReporterEnabled" - if discordlocale is True: - headers["X-Discord-Locale"] = "en-US" - if superprop is True: - headers["X-Super-Properties"] = "eyJvcyI6IldpbmRvd3MiLCJicm93c2VyIjoiQ2hyb21lIiwiZGV2aWNlIjoiIiwic3lzdGVtX2xvY2FsZSI6ImVuLVVTIiwiYnJvd3Nlcl91c2VyX2FnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwMC4wLjQ4OTYuMTI3IFNhZmFyaS81MzcuMzYiLCJicm93c2VyX3ZlcnNpb24iOiIxMDAuMC40ODk2LjEyNyIsIm9zX3ZlcnNpb24iOiIxMCIsInJlZmVycmVyIjoiIiwicmVmZXJyaW5nX2RvbWFpbiI6IiIsInJlZmVycmVyX2N1cnJlbnQiOiIiLCJyZWZlcnJpbmdfZG9tYWluX2N1cnJlbnQiOiIiLCJyZWxlYXNlX2NoYW5uZWwiOiJzdGFibGUiLCJjbGllbnRfYnVpbGRfbnVtYmVyIjoxMjY0NjIsImNsaWVudF9ldmVudF9zb3VyY2UiOm51bGx9" - if context != False: - headers["X-Context-Properties"] = context - - keyssorted = sorted(headers.keys(), key=lambda x:x.lower()) - newheaders={} - for key in keyssorted: - newheaders[key] = headers[key] - - return headers + self.c.info(f"Friends: {colours['main_colour']}{len(self.backup_data['friends'])}", indent=2) + self.c.info(f"Blocked: {colours['main_colour']}{len(self.backup_data['blocked'])}", indent=2) + self.c.info(f"Incoming: {colours['main_colour']}{len(self.backup_data['incoming'])}", indent=2) + self.c.info(f"Outgoing: {colours['main_colour']}{len(self.backup_data['outgoing'])}", indent=2) + self.c.info(f"DM Historys: {colours['main_colour']}{len(self.dm_historys)}", indent=2) + self.c.info(f"Time Elapsed: {colours['main_colour']}{self._show_time(int(self.after - self.before))}") def _reverse_snowflake(self, snfk): try: @@ -141,30 +105,30 @@ def user_info(self): self.backup_data["discriminator"] = str(self.user_me['discriminator']) self.backup_data["id"] = str(self.user_me["id"]) self.backup_data["bio"] = self.user_me["bio"] - self.c.success(f"Backed up: {self.c.clnt.maincol}User Info") + self.c.success(f"Backed up: {colours['main_colour']}User Info") - r = requests.get(f"https://cdn.discordapp.com/avatars/{self.backup_data['id']}/{self.user_me['avatar']}") + r = request_client.get(f"https://cdn.discordapp.com/avatars/{self.backup_data['id']}/{self.user_me['avatar']}") base64_bytes = base64.b64encode(r.content) base64_message = base64_bytes.decode('ascii') self.backup_data["avatar-bytes"] = base64_message - self.c.success(f"Backed up: {self.c.clnt.maincol}Avatar") + self.c.success(f"Backed up: {colours['main_colour']}Avatar") - r = requests.get(f"https://cdn.discordapp.com/banners/{self.backup_data['id']}/{self.user_me['banner']}") + r = request_client.get(f"https://cdn.discordapp.com/banners/{self.backup_data['id']}/{self.user_me['banner']}") base64_bytes = base64.b64encode(r.content) base64_message = base64_bytes.decode('ascii') self.backup_data["banner-bytes"] = base64_message - self.c.success(f"Backed up: {self.c.clnt.maincol}Banner") + self.c.success(f"Backed up: {colours['main_colour']}Banner") - r = requests.get(f"https://discord.com/api/v9/users/@me/settings", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/users/@me/settings", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) settings = r.json() self.backup_data["guild_folders"] = settings["guild_folders"] - self.c.success(f"Backed up: {self.c.clnt.maincol}Guild Folders") + self.c.success(f"Backed up: {colours['main_colour']}Guild Folders") def group_chats(self): while True: - r = requests.get(f"https://discord.com/api/v9/users/@me/channels", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/users/@me/channels", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if "You are being rate limited." in r.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{r.json()['retry_after']} seconds{self.c.clnt.white}.") + self.c.warn(f"Rate Limited: {colours['main_colour']}{r.json()['retry_after']} seconds{colours['white']}.") time.sleep((int(r.json()["retry_after"]) + 0.3)) else: break @@ -182,30 +146,28 @@ def group_chats(self): self.backup_data['group-chats'] = [] for channel in channels: - self.c.info(f"[{self.c.clnt.maincol}{channels.index(channel) + 1}/{len(channels)}{self.c.clnt.white}] Creating invite for {self.c.clnt.maincol}{channel['name']}{self.c.clnt.white} ({self.c.clnt.maincol}{len(channel['recipients'])} users{self.c.clnt.white}):") + self.c.info(f"[{colours['main_colour']}{channels.index(channel) + 1}/{len(channels)}{colours['white']}] Creating invite for {colours['main_colour']}{channel['name']}{colours['white']} ({colours['main_colour']}{len(channel['recipients'])} users{colours['white']}):") time_since_last_msg = time.time() - self._reverse_snowflake(channel['last_message_id']) - if int(self.c.clnt.cfg.group_chat_msg) != 0: - if time_since_last_msg > int(self.c.clnt.cfg.group_chat_msg): - self.c.warn(f"Group Chat too old ({self.c.clnt.maincol}{self._show_time(time_since_last_msg)} since last message{self.c.clnt.white})", indent=2) + if int(config.group_chat_msg) != 0: + if time_since_last_msg > int(config.group_chat_msg): + self.c.warn(f"Group Chat too old ({colours['main_colour']}{self._show_time(time_since_last_msg)} since last message{colours['white']})", indent=2) continue while True: - r = requests.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", - - json={ - "max_age": 604800 - }, - - headers=self._headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=True) + r = request_client.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", + json={ + "max_age": 604800 + }, + headers=build_headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token) ) if "You are being rate limited." in r.text or r.status_code == 429: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{(r.json()['retry_after'])} seconds{self.c.clnt.white}.", indent=2) + self.c.warn(f"Rate Limited: {colours['main_colour']}{(r.json()['retry_after'])} seconds{colours['white']}.", indent=2) time.sleep((r.json()["retry_after"]) + 0.3) else: if r.status_code == 200: code = r.json()['code'] - self.c.success(f"Created Invite | {self.c.clnt.maincol}{code}", indent=2) + self.c.success(f"Created Invite | {colours['main_colour']}{code}", indent=2) self.backup_data['group-chats'].append({"name": channel['name'], "id": channel['id'], "invite-code": r.json()['code']}) self.gc_success += 1 break @@ -219,41 +181,37 @@ def group_chats(self): def relationships(self): while True: - r = requests.get(f"https://discord.com/api/v9/users/@me/relationships", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/users/@me/relationships", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if "You are being rate limited." in r.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{r.json()['retry_after']} seconds{self.c.clnt.white}.") + self.c.warn(f"Rate Limited: {colours['main_colour']}{r.json()['retry_after']} seconds{colours['white']}.") time.sleep((int(r.json()["retry_after"]) + 0.3)) else: break friend_data = r.json() - self.friends = [] - self.blocked = [] - self.outgoing = [] - self.incoming = [] + relationship_map = { + 1: [], + 2: [], + 3: [], + 4: [] + } for user in friend_data: - if user["type"] == 1: - self.friends.append(user["id"]) - elif user["type"] == 2: - self.blocked.append(user["id"]) - elif user["type"] == 3: - self.incoming.append(user["id"]) - elif user["type"] == 4: - self.outgoing.append(user["id"]) - - self.backup_data["friends"] = self.friends - self.backup_data["blocked"] = self.blocked - self.backup_data["incoming"] = self.incoming - self.backup_data["outgoing"] = self.outgoing - - self.c.success(f"Backed up: {self.c.clnt.maincol}Relationships") + if user["type"] in relationship_map: + relationship_map[user["type"]].append(user["id"]) + + self.backup_data["friends"] = relationship_map[1] + self.backup_data["blocked"] = relationship_map[2] + self.backup_data["incoming"] = relationship_map[3] + self.backup_data["outgoing"] = relationship_map[4] + + self.c.success(f"Backed up: {colours['main_colour']}Relationships") def _get_invite(self, guild): while True: - r = requests.get(f"https://discord.com/api/v9/guilds/{guild['id']}/channels", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/guilds/{guild['id']}/channels", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if "You are being rate limited." in r.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{(r.json()['retry_after'])} seconds{self.c.clnt.white}.", indent=2) + self.c.warn(f"Rate Limited: {colours['main_colour']}{(r.json()['retry_after'])} seconds{colours['white']}.", indent=2) time.sleep((r.json()["retry_after"]) + 0.3) else: break @@ -271,14 +229,14 @@ def _get_invite(self, guild): channels.remove(channel) for channel in channels: - if done != False or error >= retries: + if done is not False or error >= retries: break for word in words: - if done != False or error >= retries: + if done is not False or error >= retries: break if word in channel['name']: while True: - r = requests.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", + r = request_client.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", json={ "max_age": 0, @@ -286,19 +244,19 @@ def _get_invite(self, guild): "temporary": False }, - headers=self._headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=True) + headers=build_headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True) ) if "You are being rate limited." in r.text or r.status_code == 429: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{(r.json()['retry_after'])} seconds{self.c.clnt.white}.", indent=2) + self.c.warn(f"Rate Limited: {colours['main_colour']}{(r.json()['retry_after'])} seconds{colours['white']}.", indent=2) time.sleep((r.json()["retry_after"]) + 0.3) else: if r.status_code == 200: done = code = r.json()['code'] - self.c.success(f"Created Invite in {self.c.clnt.maincol}#{channel['name']}{self.c.clnt.white} | {self.c.clnt.maincol}{code}", indent=2) + self.c.success(f"Created Invite in {colours['main_colour']}#{channel['name']}{colours['white']} | {colours['main_colour']}{code}", indent=2) break else: error += 1 - self.c.fail(f"Can't Create Invite in {self.c.clnt.maincol}#{channel['name']}{self.c.clnt.white} ({self.c.clnt.maincol}{error}/3{self.c.clnt.white})", indent=2) + self.c.fail(f"Can't Create Invite in {colours['main_colour']}#{channel['name']}{colours['white']} ({colours['main_colour']}{error}/3{colours['white']})", indent=2) break if done is False: @@ -306,7 +264,7 @@ def _get_invite(self, guild): if done != False or error >= retries: break while True: - r = requests.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", + r = request_client.post(f"https://discord.com/api/v9/channels/{channel['id']}/invites", json={ "max_age": 0, @@ -314,24 +272,24 @@ def _get_invite(self, guild): "temporary": False }, - headers=self._headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=True) + headers=build_headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token) ) if "You are being rate limited." in r.text or r.status_code == 429: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{(r.json()['retry_after'])} seconds{self.c.clnt.white}.", indent=2) + self.c.warn(f"Rate Limited: {colours['main_colour']}{(r.json()['retry_after'])} seconds{colours['white']}.", indent=2) time.sleep((r.json()["retry_after"]) + 0.3) else: if r.status_code == 200: done = code = r.json()['code'] - self.c.success(f"Created Invite in {self.c.clnt.maincol}#{channel['name']}{self.c.clnt.white} | {self.c.clnt.maincol}{code}", indent=2) + self.c.success(f"Created Invite in {colours['main_colour']}#{channel['name']}{colours['white']} | {colours['main_colour']}{code}", indent=2) break else: error += 1 - self.c.fail(f"Can't Create Invite in {self.c.clnt.maincol}#{channel['name']} {self.c.clnt.white}({self.c.clnt.maincol}{error}/3{self.c.clnt.white})", indent=2) + self.c.fail(f"Can't Create Invite in {colours['main_colour']}#{channel['name']} {colours['white']}({colours['main_colour']}{error}/3{colours['white']})", indent=2) break return code, done def guilds(self): - r = requests.get(f"https://discord.com/api/v9/users/@me/guilds", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/users/@me/guilds", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) guilds = r.json() self.guild_list = [] @@ -340,12 +298,12 @@ def guilds(self): for guild in guilds: - self.c.info(f"[{self.c.clnt.maincol}{guilds.index(guild) + 1}/{len(guilds)}{self.c.clnt.white}] Creating invite for {self.c.clnt.maincol}{guild['name']}{self.c.clnt.white}:") + self.c.info(f"[{colours['main_colour']}{guilds.index(guild) + 1}/{len(guilds)}{colours['white']}] Creating invite for {colours['main_colour']}{guild['name']}{colours['white']}:") if "VANITY_URL" in guild["features"]: while True: - r = requests.get(f"https://discord.com/api/v9/guilds/{guild['id']}", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get(f"https://discord.com/api/v9/guilds/{guild['id']}", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if "You are being rate limited." in r.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{(r.json()['retry_after'])} seconds{self.c.clnt.white}.", indent=2) + self.c.warn(f"Rate Limited: {colours['main_colour']}{(r.json()['retry_after'])} seconds{colours['white']}.", indent=2) time.sleep((r.json()["retry_after"]) + 0.3) else: break @@ -357,7 +315,7 @@ def guilds(self): if code == None: code, done = self._get_invite(guild) else: - self.c.success(f"Using {self.c.clnt.maincol}Vanity Url{self.c.clnt.white} as invite. | {self.c.clnt.maincol}{code}", indent=2) + self.c.success(f"Using {colours['main_colour']}Vanity Url{colours['white']} as invite. | {colours['main_colour']}{code}", indent=2) done = True else: @@ -404,14 +362,14 @@ def extract_id(js): self.backup_data['dm-history'] = dms self.dm_historys = dms - self.c.success(f"Backed up: {self.c.clnt.maincol}Users DMed") + self.c.success(f"Backed up: {colours['main_colour']}Users DMed") def get_favourite_gifs(self): - r = requests.get('https://discord.com/api/v9/users/@me/settings-proto/2', headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + r = request_client.get('https://discord.com/api/v9/users/@me/settings-proto/2', headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if r.status_code == 200: self.backup_data['settings'] = r.json()['settings'] - self.c.success(f"Backed up: {self.c.clnt.maincol}Favourited GIFs") + self.c.success(f"Backed up: {colours['main_colour']}Favourited GIFs") return 'Done' else: - self.c.fail(f"Couldn't back up: {self.c.clnt.maincol}Favourited GIFs ({r.status_code})") + self.c.fail(f"Couldn't back up: {colours['main_colour']}Favourited GIFs ({r.status_code})") return 'Failed' diff --git a/cfgload.py b/cfgload.py index 70b9e3c..4b237d1 100644 --- a/cfgload.py +++ b/cfgload.py @@ -10,7 +10,7 @@ class Config(): "Ex: `config.threads`" def __init__(self) -> None: - args = yaml.safe_load(open("config.yml")) + data = yaml.safe_load(open("config.yml")) - for arg, val in args.items(): - exec(f"self.{arg} = val") \ No newline at end of file + self.colour = data['colour'] + self.group_chat_msg = data['group_chat_msg'] \ No newline at end of file diff --git a/client_info.py b/client_info.py new file mode 100644 index 0000000..009dd91 --- /dev/null +++ b/client_info.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2021 github.com/ItsChasa +# +# This source code has been released under the GNU Affero General Public +# License v3.0. A copy of this license is available at +# https://www.gnu.org/licenses/agpl-3.0.en.html + +import base64 +import re +import json + +# browser data +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36" +sec_ch_ua = '"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"' +browser_version = "113.0.0.0" +request_client_identifier = "chrome_113" # tls_client uses this to determine what "Client Hello" to use + +# discord data +from main import request_client + +def get_client_build_number(): + client_request = request_client.get(f'https://discord.com/app', headers={'User-Agent': 'Mozilla/5.0'}).text + jsFileRegex = re.compile(r'([a-zA-z0-9]+)\.js', re.I) + for asset in jsFileRegex.findall(client_request)[::-1]: + if asset != "invisible": + break + + assetFileRequest = request_client.get(f'https://discord.com/assets/{asset}.js', headers={'User-Agent': 'Mozilla/5.0'}).text + try: + build_info_regex = re.compile(r'Build Number: "\)\.concat\("([0-9]{4,8})"') + build_info_strings = build_info_regex.findall(assetFileRequest) + build_num = build_info_strings[0] + except (RuntimeError, TypeError, NameError): + raise Exception(f"couldn't fetch discord build num from {asset}.js") + + return int(build_num) + +discord_build = get_client_build_number() +super_properties = { + "os": "Windows", + "browser": "Chrome", + "device": "", + "system_locale": "en-US", + "browser_user_agent": user_agent, + "browser_version": browser_version, + "os_version": "10", + "referrer": "", + "referring_domain": "", + "referrer_current": "", + "referring_domain_current": "", + "release_channel": "stable", + "client_build_number": discord_build, + "client_event_source": None +} +b64_super_properties = base64.b64encode(json.dumps(super_properties, separators=(',', ':')).encode()).decode() + + +def build_headers( + method, + superprop=False, + debugoptions=False, + discordlocale=False, + authorization=False, + origin=False, + referer="https://discord.com/channels/@me", + context=False, + timezone=False +): + headers = { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Cookie": "locale=en-GB", + "Referer": referer, + "Sec-Ch-Ua": sec_ch_ua, + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "User-Agent": user_agent + } + + if referer is False: + del headers["Referer"] + + if method != "get": + headers["Content-Type"] = "application/json" + if origin is not False: + headers["Origin"] = origin + + if authorization is not False: + headers["Authorization"] = authorization + if debugoptions is True: + headers["X-Debug-Options"] = "bugReporterEnabled" + if discordlocale is True: + headers["X-Discord-Locale"] = "en-US" + if superprop is True: + headers["X-Super-Properties"] = b64_super_properties + if context is True: + headers["X-Context-Properties"] = context + if timezone is True: + headers["X-Discord-Timezone"] = "Europe/London" + + keyssorted = sorted(headers.keys(), key=lambda x:x.lower()) + newheaders={} + for key in keyssorted: + newheaders[key] = headers[key] + + return headers \ No newline at end of file diff --git a/config.yml b/config.yml index 117d7da..a82bc57 100644 --- a/config.yml +++ b/config.yml @@ -15,7 +15,7 @@ colour: purple # group chat backup: -# if last message is older than _ seconds, dont create invite: +# if last message is older than _ seconds, dont create invite # format: seconds # set to 0 to disable it # default: 1209600 (2 weeks) diff --git a/console.py b/console.py index 3e5ab0e..099bcba 100644 --- a/console.py +++ b/console.py @@ -5,33 +5,33 @@ # License v3.0. A copy of this license is available at # https://www.gnu.org/licenses/agpl-3.0.en.html -import startup +from main import colours class prnt(): - def __init__(self, clnt : startup.Setup) -> None: - self.clnt = clnt + def __init__(self) -> None: + pass def success(self, content, end="\n", indent=0): ind = "" for _ in range(indent): ind += " " - print(f"{ind}{self.clnt.green}>{self.clnt.white} {content}", end=end) + print(f"{ind}{colours['green']}>{colours['white']} {content}", end=end) def info(self, content, end="\n", indent=0): ind = "" for _ in range(indent): ind += " " - print(f"{ind}{self.clnt.white}> {content}", end=end) + print(f"{ind}{colours['white']}> {content}", end=end) def fail(self, content, end="\n", indent=0): ind = "" for _ in range(indent): ind += " " - print(f"{ind}{self.clnt.red}>{self.clnt.white} {content}", end=end) + print(f"{ind}{colours['light_red']}>{colours['white']} {content}", end=end) def inp(self, content, end="\n", indent=0): ind = "" for _ in range(indent): ind += " " - print(f"{ind}{self.clnt.blue}>{self.clnt.white} {content}", end=end) + print(f"{ind}{colours['light_blue']}>{colours['white']} {content}", end=end) def warn(self, content, end="\n", indent=0): ind = "" for _ in range(indent): ind += " " - print(f"{ind}{self.clnt.yellow}>{self.clnt.white} {content}", end=end) \ No newline at end of file + print(f"{ind}{colours['yellow']}>{colours['white']} {content}", end=end) \ No newline at end of file diff --git a/fetch_tokens.py b/fetch_tokens.py index 9dd4e0d..c085b79 100644 --- a/fetch_tokens.py +++ b/fetch_tokens.py @@ -5,10 +5,17 @@ # License v3.0. A copy of this license is available at # https://www.gnu.org/licenses/agpl-3.0.en.html -import os, re, requests, base64, json, brotli +import os +import re +import base64 +import json +import brotli from Crypto.Cipher import AES from win32crypt import CryptUnprotectData +from client_info import build_headers +from main import request_client + def fetch(): """[[token, user#tag], ...] """ @@ -37,33 +44,12 @@ def get_decryption_key(path) -> str: return decryption_key - def getheaders(token): - headers = { - "accept": "*/*", - "accept-encoding": "gzip, deflate, br", - "accept-language": "en-US,en;q=0.9", - "authorization": token, - "cookie": "locale=en-GB", - "referer": "https://discord.com/channels/@me", - "sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"Windows"', - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-origin", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", - "x-debug-options": "bugReporterEnabled", - "x-discord-locale": "en-GB", - "x-super-properties": "eyJvcyI6IldpbmRvd3MiLCJicm93c2VyIjoiQ2hyb21lIiwiZGV2aWNlIjoiIiwic3lzdGVtX2xvY2FsZSI6ImVuLVVTIiwiYnJvd3Nlcl91c2VyX2FnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwMC4wLjQ4OTYuMTI3IFNhZmFyaS81MzcuMzYiLCJicm93c2VyX3ZlcnNpb24iOiIxMDAuMC40ODk2LjEyNyIsIm9zX3ZlcnNpb24iOiIxMCIsInJlZmVycmVyIjoiIiwicmVmZXJyaW5nX2RvbWFpbiI6IiIsInJlZmVycmVyX2N1cnJlbnQiOiIiLCJyZWZlcnJpbmdfZG9tYWluX2N1cnJlbnQiOiIiLCJyZWxlYXNlX2NoYW5uZWwiOiJzdGFibGUiLCJjbGllbnRfYnVpbGRfbnVtYmVyIjoxMjY0NjIsImNsaWVudF9ldmVudF9zb3VyY2UiOm51bGx9" - } - return headers - def reqJSON(req) -> dict: try: return req.json() except: return json.loads(brotli.decompress(req.content).decode("utf-8")) def check_token(tkn, name, ids:list, to_return_tokens:list): - r = requests.get("https://discord.com/api/v9/users/@me", headers=getheaders(tkn)) + r = request_client.get("https://discord.com/api/v9/users/@me", headers=build_headers("get", superprop=True, debugoptions=True, discordlocale=True, authorization=tkn, timezone=True)) if r.status_code == 200: tknid = base64.b64decode((tkn.split('.')[0] + '===').encode('ascii')).decode('ascii') if (tknid+name) not in ids: diff --git a/main.py b/main.py index e113f80..af8d4f2 100644 --- a/main.py +++ b/main.py @@ -5,159 +5,146 @@ # License v3.0. A copy of this license is available at # https://www.gnu.org/licenses/agpl-3.0.en.html -import os -os.system("cls") -from startup import Setup -clnt = Setup("Backup", "v1.2.0") - -# local imports -import console, backup, restore -c = console.prnt(clnt) +app_version = "v1.2.1" -# 3rd party imports -import time, sys, base64, os, json +import time +import sys +import base64 +import os +import json +import ctypes from pathlib import Path from easygui import fileopenbox +import tls_client +from colorama import Fore +from client_info import request_client_identifier +request_client = tls_client.Session(client_identifier=request_client_identifier) +# change cwd (for auto-backup) try: cwd = sys.argv[2] -except: account_id = None -else: - os.chdir(cwd) - account_id = sys.argv[1] - -while True: - choice = 0 - if account_id == None: - print(f""" -{clnt.white}> Main Menu -{clnt.white}1: {clnt.maincol}Backup -{clnt.white}2: {clnt.maincol}Restore -{clnt.white}3: {clnt.maincol}Add to Startup -{clnt.white}4: {clnt.maincol}Help - -{clnt.blue}>{clnt.white} Choice ({clnt.maincol}int{clnt.white}) """, end="") - choice = input() - try: choice = int(choice) - except: choice = "wtf are u retarded, do you know what numbers are" +except: pass +else: os.chdir(cwd) + +import cfgload +try: config = cfgload.Config() +except: + print("Config File was not found (config.yml), make sure it exists.") + time.sleep(3) + os._exit(1) + +colours = { + "light_red": Fore.LIGHTRED_EX, + "dark_red": Fore.RED, + "yellow": Fore.YELLOW, + "dark_blue": Fore.BLUE, + "light_blue": Fore.LIGHTBLUE_EX, + "dark_cyan": Fore.CYAN, + "light_cyan": Fore.LIGHTCYAN_EX, + "green": Fore.LIGHTGREEN_EX, + "purple": Fore.MAGENTA, + "pink": Fore.LIGHTMAGENTA_EX, + "gray": Fore.LIGHTBLACK_EX, + "black": Fore.BLACK, + "white": Fore.WHITE, +} +try: colours['main_colour'] = colours[config.colour] +except: colours['main_colour'] = Fore.MAGENTA + +if __name__ == '__main__': + import console + import backup + import restore + c = console.prnt() + + os.system('cls') + try: + github_data = request_client.get("https://api.github.com/repos/ItsChasa/Discord-Backup/releases/latest").json() + app_latest_ver = github_data['tag_name'] + app_latest_ver_link = github_data['html_url'] + except: + app_latest_ver = app_version + app_latest_ver_link = "null" + + print(f"""{colours['main_colour']} + ___ _ +( _ \ ( ) +| (_) ) _ _ ___| |/ ) _ _ _ _ +| _ ( / _ )/ ___) ( ( ) ( ) _ \ +| (_) ) (_| | (___| |\ \| (_) | (_) ) +(____/ \__ _)\____)_) (_)\___/| __/ + | | + (_) {colours['white']} +> Made with {colours['main_colour']}<3{colours['white']} by {colours['main_colour']}chasa{colours['white']} +> Github: {colours['main_colour']}https://github.com/itschasa/Discord-Backup{colours['white']} +> Don't forget to {colours['yellow']}star{colours['white']} it! +> Version: {colours['main_colour']}{app_version}""") + + try: ctypes.windll.kernel32.SetConsoleTitleW(f"github.com/itschasa/Discord-Backup") + except: pass + + if app_latest_ver != app_version: + print() + c.warn(f"You are running an outdated version! Download the new version on GitHub: {app_latest_ver}") + c.warn(app_latest_ver_link) + + try: cwd = sys.argv[2] + except: account_id = None else: - choice = 1 - print() - if choice == 1: - token_info = False - if account_id != None: - c.info(f"Launching Auto-Backup on ID: {clnt.maincol}{account_id}") - account_id_b64 = base64.b64encode(str(account_id).encode()).decode().replace('=', '') - import fetch_tokens - c.info(f"Scanning for tokens...") - tokens = fetch_tokens.fetch() - for tkn in tokens: - if str(tkn[0]).startswith(account_id_b64): - token_info = tkn - break - if token_info is False: - c.fail("Could not find valid token with the provided ID.") - c.fail("Exiting in 5 seconds...") - time.sleep(5) - os._exit(1) - else: - c.success(f"Found token with matching ID: {clnt.maincol}{token_info[1]}") - c.info("Starting Backup...") + os.chdir(cwd) + account_id = sys.argv[1] + + while True: + choice = 0 + if account_id == None: + print(f""" +{colours['white']}> Main Menu +{colours['white']}1: {colours['main_colour']}Backup +{colours['white']}2: {colours['main_colour']}Restore +{colours['white']}3: {colours['main_colour']}Add to Startup +{colours['white']}4: {colours['main_colour']}Help + +{colours['light_blue']}>{colours['white']} Choice ({colours['main_colour']}int{colours['white']}) """, end="") + choice = input() + try: choice = int(choice) + except: choice = "" else: - c.inp(f"Scan for tokens? ({clnt.maincol}y/n{clnt.white}) ", end=clnt.white) - if input().lower() == "y": + choice = 1 + print() + if choice == 1: + token_info = False + if account_id != None: + c.info(f"Launching Auto-Backup on ID: {colours['main_colour']}{account_id}") + account_id_b64 = base64.b64encode(str(account_id).encode()).decode().replace('=', '') import fetch_tokens c.info(f"Scanning for tokens...") tokens = fetch_tokens.fetch() - if len(tokens) != 0: - print() - while True: - c.info(f"Select which token you want to be auto-backed up:") - for tkn in tokens: - print(f"{clnt.white}{tokens.index(tkn)}: {clnt.maincol}{tkn[1]}{clnt.white} from {clnt.maincol}{tkn[3]}") - print() - c.inp(f"Choice {clnt.maincol}(int) """, end=clnt.white) - try: tknchoice = int(input()) - except ValueError: c.fail(f"Invalid Choice") - else: - try: - token_selected = tokens[tknchoice] - except: - c.fail(f"Invalid Choice") - else: - break - c.success(f"Selected {clnt.maincol}{token_selected[1]}") - token_info = token_selected - else: - c.fail(f"No tokens were found on your system.") - else: - c.inp("Token: ", end="") - token_info = [input(), ""] - - - if token_info != False: - bkup = backup.backup(token_info[0], c, clnt.version) - if bkup.fatal_error != False: - c.fail(bkup.fatal_error) - if len(sys.argv) > 2: - print() - c.warn("Closing in 5 seconds...") - time.sleep(5) - os._exit(0) - - - elif choice == 2: - c.info(f"1: {clnt.maincol}Restore Everything") - c.info(f"2: {clnt.maincol}Restore Server Folders") - c.info(f"See help for more info.") - print() - c.inp(f"Choice ({clnt.maincol}int{clnt.white}) ", end="") - try: - choice = int(input()) - if choice not in [1,2]: raise Exception - except: - c.fail(f"Invalid Choice") - else: - restore_server_folders = False - start_restore = False - if choice == 1: - try: - c.inp(f"Select backup file in new window.") - backupfile = fileopenbox(title="Load .bkup File", default="*.bkup") - backup_data = json.loads(open(backupfile, "r", encoding="utf-8").read()) - except Exception as e: - c.fail("Encountered Error whilst loading backup file.") - c.fail(f"{e}") + for tkn in tokens: + if str(tkn[0]).startswith(account_id_b64): + token_info = tkn + break + if token_info is False: + c.fail("Could not find valid token with the provided ID.") + c.fail("Exiting in 5 seconds...") + time.sleep(5) + os._exit(1) else: - c.success(f"Loaded backup data.") - start_restore = True + c.success(f"Found token with matching ID: {colours['main_colour']}{token_info[1]}") + c.info("Starting Backup...") else: - restore_server_folders = True - try: - c.inp(f"Select backup file in new window.") - backupfile = fileopenbox(title="Load .bkup File", default="*.bkup") - backup_data = json.loads(open(backupfile, "r", encoding="utf-8").read()) - except Exception as e: - c.fail("Encountered Error whilst loading backup file.") - c.fail(f"{e}") - else: - c.success(f"Loaded server folders.") - start_restore = True - - if start_restore is True: - c.inp(f"Scan for tokens? ({clnt.maincol}y/n{clnt.white}) ({clnt.maincol}account to restore on to{clnt.white}) ", end=clnt.white) + c.inp(f"Scan for tokens? ({colours['main_colour']}y/n{colours['white']}) ", end=colours['white']) if input().lower() == "y": import fetch_tokens c.info(f"Scanning for tokens...") tokens = fetch_tokens.fetch() if len(tokens) != 0: + print() while True: - print() c.info(f"Select which token you want to be auto-backed up:") for tkn in tokens: - print(f"{clnt.white}{tokens.index(tkn)}: {clnt.maincol}{tkn[1]}{clnt.white} from {clnt.maincol}{tkn[3]}") + print(f"{colours['white']}{tokens.index(tkn)}: {colours['main_colour']}{tkn[1]}{colours['white']} from {colours['main_colour']}{tkn[3]}") print() - c.inp(f"Choice {clnt.maincol}(int) """, end=clnt.white) + c.inp(f"Choice {colours['main_colour']}(int) """, end=colours['white']) try: tknchoice = int(input()) except ValueError: c.fail(f"Invalid Choice") else: @@ -167,84 +154,167 @@ c.fail(f"Invalid Choice") else: break - c.success(f"Selected {clnt.maincol}{token_selected[1]}") + c.success(f"Selected {colours['main_colour']}{token_selected[1]}") token_info = token_selected else: c.fail(f"No tokens were found on your system.") else: c.inp("Token: ", end="") token_info = [input(), ""] - - c.info("A Bot Token is required to fetch Usernames from IDs (used for friends/blocked). You can create one in the Discord Developer Portal.") - c.inp("Bot Token: ", end="") - bot_token = input() - - rstr = restore.restore(token_info[0], c, restore_server_folders, backup_data, bot_token, clnt.version) - if rstr.fatal_error != False: - c.fail(rstr.fatal_error) + + + if token_info != False: + bkup = backup.backup(token_info[0], c, app_version) + if bkup.fatal_error != False: + c.fail(bkup.fatal_error) + if len(sys.argv) > 2: + print() + c.warn("Closing in 5 seconds...") + time.sleep(5) + os._exit(0) - - elif choice == 3: - c.inp(f"By adding to startup, you agree that this program is allowed to search for tokens on your PC.") - c.inp(f"Your Account Token does not leave this machine and is not sent to our servers. ({clnt.maincol}y/n{clnt.white}) ", end=f"{clnt.white}") - if input().lower() == "y": - import fetch_tokens - c.info(f"Scanning for tokens...") - tokens = fetch_tokens.fetch() - if len(tokens) != 0: - while True: - print() - c.info(f"Select which token you want to be auto-backed up:") - for tkn in tokens: - print(f"{clnt.white}{tokens.index(tkn)}: {clnt.maincol}{tkn[1]}{clnt.white} from {clnt.maincol}{tkn[3]}") - print() - c.inp(f"Choice {clnt.maincol}(int) """, end=clnt.white) - try: tknchoice = int(input()) - except ValueError: c.fail(f"Invalid Choice") + elif choice == 2: + c.info(f"1: {colours['main_colour']}Restore Everything") + c.info(f"2: {colours['main_colour']}Restore Server Folders") + c.info(f"See help for more info.") + print() + c.inp(f"Choice ({colours['main_colour']}int{colours['white']}) ", end="") + try: + choice = int(input()) + if choice not in [1,2]: raise Exception + except: + c.fail(f"Invalid Choice") + else: + restore_server_folders = False + start_restore = False + if choice == 1: + try: + c.inp(f"Select backup file in new window.") + backupfile = fileopenbox(title="Load .bkup File", default="*.bkup") + backup_data = json.loads(open(backupfile, "r", encoding="utf-8").read()) + except Exception as e: + c.fail("Encountered Error whilst loading backup file.") + c.fail(f"{e}") else: - try: - token_selected = tokens[tknchoice] - except: - c.fail(f"Invalid Choice") + c.success(f"Loaded backup data.") + start_restore = True + else: + restore_server_folders = True + try: + c.inp(f"Select backup file in new window.") + backupfile = fileopenbox(title="Load .bkup File", default="*.bkup") + backup_data = json.loads(open(backupfile, "r", encoding="utf-8").read()) + except Exception as e: + c.fail("Encountered Error whilst loading backup file.") + c.fail(f"{e}") + else: + c.success(f"Loaded server folders.") + start_restore = True + + if start_restore is True: + c.inp(f"Scan for tokens? ({colours['main_colour']}y/n{colours['white']}) ({colours['main_colour']}account to restore on to{colours['white']}) ", end=colours['white']) + if input().lower() == "y": + import fetch_tokens + c.info(f"Scanning for tokens...") + tokens = fetch_tokens.fetch() + if len(tokens) != 0: + while True: + print() + c.info(f"Select which token you want to be auto-backed up:") + for tkn in tokens: + print(f"{colours['white']}{tokens.index(tkn)}: {colours['main_colour']}{tkn[1]}{colours['white']} from {colours['main_colour']}{tkn[3]}") + print() + c.inp(f"Choice {colours['main_colour']}(int) """, end=colours['white']) + try: tknchoice = int(input()) + except ValueError: c.fail(f"Invalid Choice") + else: + try: + token_selected = tokens[tknchoice] + except: + c.fail(f"Invalid Choice") + else: + break + c.success(f"Selected {colours['main_colour']}{token_selected[1]}") + token_info = token_selected else: - break - - c.success(f"Selected {clnt.maincol}{token_selected[1]}") - - try: - if getattr(sys, 'frozen', False): # if pyinstaller - tmp_path = "" - dirs_list = sys.executable.split("\\") - for dir in dirs_list[0:-1]: - tmp_path += f"{dir}\\" - application_path = tmp_path[0:-1] - application_name = dirs_list[-1] + c.fail(f"No tokens were found on your system.") else: - application_path = os.path.dirname(os.path.abspath(__file__)) - application_name = Path(__file__).name + c.inp("Token: ", end="") + token_info = [input(), ""] + + c.info("A Bot Token is required to fetch Usernames from IDs (used for friends/blocked). You can create one in the Discord Developer Portal.") + c.inp("Bot Token: ", end="") + bot_token = input() + + rstr = restore.restore(token_info[0], c, restore_server_folders, backup_data, bot_token, app_version) + if rstr.fatal_error != False: + c.fail(rstr.fatal_error) - except: - c.fail(f"Failed to find Executable.") - else: - roaming = os.getenv('APPDATA') - f = open(f"{roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\backupStartup.vbs", "w") - f.write(f'Set oShell = CreateObject ("WScript.Shell")') - f.write("\n") - f.write(f'oShell.run ("""{application_path}\{application_name}"" {token_selected[2]} ""{application_path}""")') - f.close() + + + elif choice == 3: + c.inp(f"By adding to startup, you agree that this program is allowed to search for tokens on your PC.") + c.inp(f"Your Account Token is only sent to discord's servers. ({colours['main_colour']}y/n{colours['white']}) ", end=f"{colours['white']}") + if input().lower() == "y": + import fetch_tokens + c.info(f"Scanning for tokens...") + tokens = fetch_tokens.fetch() + if len(tokens) != 0: + while True: + print() + c.info(f"Select which token you want to be auto-backed up:") + for tkn in tokens: + print(f"{colours['white']}{tokens.index(tkn)}: {colours['main_colour']}{tkn[1]}{colours['white']} from {colours['main_colour']}{tkn[3]}") + print() + c.inp(f"Choice {colours['main_colour']}(int) """, end=colours['white']) + try: tknchoice = int(input()) + except ValueError: c.fail(f"Invalid Choice") + else: + try: + token_selected = tokens[tknchoice] + except: + c.fail(f"Invalid Choice") + else: + break + + c.success(f"Selected {colours['main_colour']}{token_selected[1]}") - c.success(f"Added to startup! ({clnt.maincol}{roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\backupStartup.vbs{clnt.white})") - else: - c.fail(f"No tokens were found on your system.") - - elif choice == 4: - c.info('Go to: https://github.com/itschasa/Discord-Backup#how-to-use') - c.info('For additional help, join the Discord Server: https://discord.gg/MUP5TSEPc4') - c.info("If it's invalid, go to https://chasa.wtf and click the Discord icon.") - - else: - c.fail(f"Invalid Choice") - - time.sleep(1) + try: + if getattr(sys, 'frozen', False): # if pyinstaller + tmp_path = "" + dirs_list = sys.executable.split("\\") + for dir in dirs_list[0:-1]: + tmp_path += dir + "\\" + application_path = tmp_path[0:-1] + application_name = dirs_list[-1] + prefix_for_command = "" + else: + application_path = os.path.dirname(os.path.abspath(__file__)) + application_name = Path(__file__).name + prefix_for_command = "cmd.exe /C python " + + except: + c.fail(f"Failed to find Executable.") + else: + roaming = os.getenv('APPDATA') + f = open(f"{roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\backupStartup.vbs", "w") + f.write(f'Set oShell = CreateObject ("WScript.Shell")') + f.write("\n") + f.write(f'oShell.run "{prefix_for_command}""{application_path}\{application_name}"" {token_selected[2]} ""{application_path}"""') + f.close() + + c.success(f"Added to startup! ({colours['main_colour']}{roaming}\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\backupStartup.vbs{colours['white']})") + else: + c.fail(f"No tokens were found on your system.") + + elif choice == 4: + c.info('Go to: https://github.com/itschasa/Discord-Backup#how-to-use') + c.info('For additional help, join the Discord Server: https://discord.gg/MUP5TSEPc4') + c.info("If it's invalid, go to https://chasa.wtf and click the Discord icon.") + + else: + c.fail(f"Invalid Choice") + + time.sleep(0.5) diff --git a/requirements.txt b/requirements.txt index 4dde590..12ccf10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ easygui colorama Brotli easyyaml +tls-client>=0.2.1 \ No newline at end of file diff --git a/restore.py b/restore.py index 32b4271..ee2ef69 100644 --- a/restore.py +++ b/restore.py @@ -4,41 +4,49 @@ # License v3.0. A copy of this license is available at # https://www.gnu.org/licenses/agpl-3.0.en.html -import requests, base64, time, random +import base64 +import time +import requests import console +from main import request_client, colours +from client_info import build_headers -read_me_msg = """Due to the current situation with captchas, I've decided that it would be easier to put all the invites into a server for you to join.\n\nPublic Captcha Solvers right now are quite "on and off", meaning they sometimes work, and sometimes they don't.\n\nAfter you have joined all the servers, you can restore your original folders automatically, by using option 2 on the restore module.\nHowever, you can do this manually if you prefer.\n\n\n*Brought to you with <3 by https://github.com/itschasa/discord-backup :)*""" +read_me_msg = """After you have joined all the servers, use option 2 on restore to get your server folders back!\n\nCoded with <3 by https://github.com/itschasa/discord-backup :)""" class restore(): def __init__(self, token, c: console.prnt, restore_server_folders, restore_data, bot_token, version) -> None: + self.token = token + self.c = c + self.fatal_error = False + self.before = time.time() + self.bot_token = bot_token self.restore_data = restore_data + if self.restore_data['version'] != version: self.c.warn(f"This Backup wasn't done on the same version of this software. (b: {self.restore_data}, c:{version})") self.c.warn(f"This could lead to unexpected errors or side effects.") - self.c.warn(f"It's recommended you change your software version to the same backup before continuing.") - self.c.inp(f"Are you sure you want to continue? ({self.c.clnt.maincol}y/n{self.c.clnt.white})", end=f"{self.c.clnt.white}") + self.c.warn(f"It's recommended you change your software version to the same version as the backup was done on before continuing.") + self.c.inp(f"Are you sure you want to continue? ({colours['main_colour']}y/n{colours['white']})", end=f"{colours['white']}") warning_continue = input() if warning_continue != "y": self.fatal_error = "Chose to stop restore" return - self.token = token - self.c = c - self.fatal_error = False - self.before = time.time() - - self.bot_token = bot_token - - token_check = requests.get("https://discord.com/api/v9/users/@me", headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True)) + token_check = request_client.get("https://discord.com/api/v9/users/@me", headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True)) if token_check.status_code != 200: self.fatal_error = "Invalid User Token" else: self.user_me = token_check.json() - bot_token_check = requests.get("https://discord.com/api/v9/users/@me", headers=self._headers("get", authorization=True, token_override=f'Bot {self.bot_token}')) + bot_token_check = request_client.get("https://discord.com/api/v9/users/@me", + headers={ + 'Authorization' : f'Bot {self.bot_token}', + 'User-Agent': 'discord.py' + } + ) if bot_token_check.status_code != 200: - self.fatal_error = "Invalid User Token" + self.fatal_error = "Invalid Bot Token" else: print() @@ -51,12 +59,12 @@ def __init__(self, token, c: console.prnt, restore_server_folders, restore_data, print() self.c.success(f"Restore Complete!") - self.c.info(f"User Info + Avatar: {self.c.clnt.maincol}Done") - self.c.info(f"Favourited GIFs: {self.c.clnt.maincol}{fav_gifs_msg}") - self.c.info(f"Guild Folders: {self.c.clnt.maincol}{'Done' if restore_server_folders else 'Disabled'}") - self.c.info(f"Guilds: {self.c.clnt.maincol}Done") - self.c.info(f"Relationships: {self.c.clnt.maincol}Done") - self.c.info(f"Time Elapsed: {self.c.clnt.maincol}{self._show_time(int(self.after - self.before))}") + self.c.info(f"User Info + Avatar: {colours['main_colour']}Done") + self.c.info(f"Favourited GIFs: {colours['main_colour']}{fav_gifs_msg}") + self.c.info(f"Guild Folders: {colours['main_colour']}{'Done' if restore_server_folders else 'Disabled'}") + self.c.info(f"Guilds: {colours['main_colour']}Done") + self.c.info(f"Relationships: {colours['main_colour']}Done") + self.c.info(f"Time Elapsed: {colours['main_colour']}{self._show_time(int(self.after - self.before))}") def _show_time(self, time): time = int(time) @@ -71,66 +79,20 @@ def _show_time(self, time): elif day == 0 and hour != 0: return "%d hours %d minutes" % (hour, minutes) elif day == 0 and hour == 0 and minutes != 0: return "%d minutes %d seconds" % (minutes, seconds) else: return "%d seconds" % (seconds) - - def _headers(self, method, superprop=False, debugoptions=False, discordlocale=False, authorization=False, origin=False, referer="https://discord.com/channels/@me", context=False, token_override=False): - headers = { - "Accept": "*/*", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "en-US,en;q=0.9", - "Cookie": "locale=en-GB", - "Referer": referer, - "Sec-Ch-Ua": '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"', - "Sec-Ch-Ua-Mobile": "?0", - "Sec-Ch-Ua-Platform": '"Windows"', - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36" - } - - if referer is False: - del headers["Referer"] - - if method != "get": - headers["Content-Type"] = "application/json" - headers["Origin"] = "https://discord.com" - - if authorization is True: - if token_override is False: - headers["Authorization"] = self.token - else: - headers["Authorization"] = token_override - if origin != False: - headers["Origin"] = origin - if debugoptions is True: - headers["X-Debug-Options"] = "bugReporterEnabled" - if discordlocale is True: - headers["X-Discord-Locale"] = "en-US" - if superprop is True: - headers["X-Super-Properties"] = "eyJvcyI6IldpbmRvd3MiLCJicm93c2VyIjoiQ2hyb21lIiwiZGV2aWNlIjoiIiwic3lzdGVtX2xvY2FsZSI6ImVuLVVTIiwiYnJvd3Nlcl91c2VyX2FnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwMC4wLjQ4OTYuMTI3IFNhZmFyaS81MzcuMzYiLCJicm93c2VyX3ZlcnNpb24iOiIxMDAuMC40ODk2LjEyNyIsIm9zX3ZlcnNpb24iOiIxMCIsInJlZmVycmVyIjoiIiwicmVmZXJyaW5nX2RvbWFpbiI6IiIsInJlZmVycmVyX2N1cnJlbnQiOiIiLCJyZWZlcnJpbmdfZG9tYWluX2N1cnJlbnQiOiIiLCJyZWxlYXNlX2NoYW5uZWwiOiJzdGFibGUiLCJjbGllbnRfYnVpbGRfbnVtYmVyIjoxMjY0NjIsImNsaWVudF9ldmVudF9zb3VyY2UiOm51bGx9" - if context != False: - headers["X-Context-Properties"] = context - - keyssorted = sorted(headers.keys(), key=lambda x:x.lower()) - newheaders={} - for key in keyssorted: - newheaders[key] = headers[key] - - return headers def _snowflake(self): return str(int(bin(int((time.time() * 1000) - 1420070400000)).replace("0b", "") + "0000000000000000000000", 2)) def _message(self, guild, channel, content) -> bool: - headers = self._headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=True, - referer=f"https://discord.com/channels/{guild}/{channel}") + headers = build_headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, + referer=f"https://discord.com/channels/{guild}/{channel}", timezone=True) payload = { "content": content, "nonce": self._snowflake(), "tts": False } while True: - response = requests.post(f"https://discord.com/api/v9/channels/{channel}/messages", + response = request_client.post(f"https://discord.com/api/v9/channels/{channel}/messages", headers=headers, json=payload ) @@ -180,36 +142,50 @@ def servers(self): for folder in self.restore_data['guild_folders']: channels.append( { - "id": str(self.restore_data['guild_folders'].index(folder) + 4), + "id": str(self.restore_data['guild_folders'].index(folder) + 5), "parent_id": None, "name": f"folder-{self.restore_data['guild_folders'].index(folder)}", "type": 0 } ) - imgdata = base64.b64encode(requests.get("https://i.imgur.com/b6B3Fbw.jpg").content).decode() + try: + imgdata = base64.b64encode(requests.get("https://i.imgur.com/b6B3Fbw.jpg").content).decode() + if len(imgdata) < 500: + raise Exception + else: + imgdata = "data:image/jpeg;base64," + imgdata + except: + imgdata = None + payload = { - "channels": channels, - "guild_template_code": "FbwUwRp4j8Es", - "icon": f"data:image/jpeg;base64,{imgdata}", "name": "Backup Server", + "icon": imgdata, + "channels": channels, "system_channel_id": "0", + "guild_template_code": "2TffvPucqHkN", } - req = requests.post("https://discord.com/api/v9/guilds", - headers=self._headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=True), + req = request_client.post("https://discord.com/api/v9/guilds", + headers=build_headers("post", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True, origin="https://discord.com"), json=payload ) - - guild_id = req.json()['id'] + + try: guild_id = req.json()['id'] + except: + self.c.fail(f"Failed to create guild: {req.status_code}") + self.c.fail(req.text) + self.fatal_error = "Failed to create guild" + return + missing_guilds_chn_id = None group_chat_id = None missing_guilds = [""] self.c.success("Created Guild") - guild_channels = requests.get(f"https://discord.com/api/v9/guilds/{guild_id}/channels", - headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True) + guild_channels = request_client.get(f"https://discord.com/api/v9/guilds/{guild_id}/channels", + headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True) ).json() for chn in guild_channels: @@ -233,7 +209,7 @@ def servers(self): if str(inv['id']) == str(gld_id): invite_data = inv if invite_data is False: - self.c.warn(f"Couldn't find invite for guild: {self.c.clnt.maincol}{gld_id}") + self.c.warn(f"Couldn't find invite for guild: {colours['main_colour']}{gld_id}") missing_guilds_dat = f"ID: `{gld_id}` | Server was in Server Folders, but not in invites.\n\n" if len(missing_guilds[-1]) + len(missing_guilds_dat) > 2000: missing_guilds.append(missing_guilds_dat) @@ -246,9 +222,9 @@ def servers(self): cnt.append(dat) else: cnt[-1] += dat - self.c.success(f"Found data for guild: {self.c.clnt.maincol}{invite_data['name']}") + self.c.success(f"Found data for guild: {colours['main_colour']}{invite_data['name']}") else: - self.c.warn(f"Invite wasn't created for: {self.c.clnt.maincol}{invite_data['name']}") + self.c.warn(f"Invite wasn't created for: {colours['main_colour']}{invite_data['name']}") missing_guilds_dat = f"Name: `{invite_data['name']}` | ID: `{gld_id}` | Invite wasn't made on backup (missing perms). \n\n" if len(missing_guilds[-1]) + len(missing_guilds_dat) > 2000: missing_guilds.append(missing_guilds_dat) @@ -259,18 +235,18 @@ def servers(self): self.c.warn("Sleeping 1 second") time.sleep(1) if self._message(guild_id, chn['id'], msg): - self.c.success(f"Sent message in Channel: {self.c.clnt.maincol}#{chn['name']}") + self.c.success(f"Sent message in Channel: {colours['main_colour']}#{chn['name']}") else: - self.c.fail(f"Failed to send message in Channel: {self.c.clnt.maincol}#{chn['name']}") + self.c.fail(f"Failed to send message in Channel: {colours['main_colour']}#{chn['name']}") for msg in missing_guilds: self.c.warn("Sleeping 1 second") time.sleep(1) if self._message(guild_id, missing_guilds_chn_id, msg): - self.c.success(f"Sent message in Channel: {self.c.clnt.maincol}#missing-guilds") + self.c.success(f"Sent message in Channel: {colours['main_colour']}#missing-guilds") else: - self.c.fail(f"Failed to send message in Channel: {self.c.clnt.maincol}#missing-guilds") + self.c.fail(f"Failed to send message in Channel: {colours['main_colour']}#missing-guilds") group_chats = ["**Invites for Group Chats only last 7 days, so if your backup is old, then these might all be invalid.**\nAnother Note: Your old account must still be in these group chats. Being termed is ok. However, if your old account was kicked/left the group chats, then these invites will be invalid.\n\n"] @@ -285,9 +261,9 @@ def servers(self): self.c.warn("Sleeping 1 second") time.sleep(1) if self._message(guild_id, group_chat_id, msg): - self.c.success(f"Sent message in Channel: {self.c.clnt.maincol}#group-chats") + self.c.success(f"Sent message in Channel: {colours['main_colour']}#group-chats") else: - self.c.fail(f"Failed to send message in Channel: {self.c.clnt.maincol}#group-chats") + self.c.fail(f"Failed to send message in Channel: {colours['main_colour']}#group-chats") dm_messages = ["**DM History**\nFormat: `user#tag | user ping | last_dm`\n(sorted most recent at top)\n\n"] for dm in self.restore_data['dm-history']: @@ -302,9 +278,9 @@ def servers(self): self.c.warn("Sleeping 1 second") time.sleep(1) if self._message(guild_id, dm_history_id, msg): - self.c.success(f"Sent message in Channel: {self.c.clnt.maincol}#dm-history") + self.c.success(f"Sent message in Channel: {colours['main_colour']}#dm-history") else: - self.c.fail(f"Failed to send message in Channel: {self.c.clnt.maincol}#dm-history") + self.c.fail(f"Failed to send message in Channel: {colours['main_colour']}#dm-history") friend_msgs = ["**Friends/Relationships**\n\n"] @@ -368,9 +344,9 @@ def servers(self): self.c.warn("Sleeping 1 second") time.sleep(1) if self._message(guild_id, friend_chnl_id, msg): - self.c.success(f"Sent message in Channel: {self.c.clnt.maincol}#friends") + self.c.success(f"Sent message in Channel: {colours['main_colour']}#friends") else: - self.c.fail(f"Failed to send message in Channel: {self.c.clnt.maincol}#friends") + self.c.fail(f"Failed to send message in Channel: {colours['main_colour']}#friends") def user_data(self): @@ -378,45 +354,51 @@ def user_data(self): f = open(f"pfp-{time_now}.gif", "wb") f.write(base64.b64decode(self.restore_data['avatar-bytes'])) f.close() - self.c.success(f"Saved avatar: {self.c.clnt.maincol}pfp-{time_now}.png") + self.c.success(f"Saved avatar: {colours['main_colour']}pfp-{time_now}.png") f = open(f"bio-{time_now}.txt", "w", encoding="utf-8") f.write(self.restore_data['bio']) f.close() - self.c.success(f"Saved bio: {self.c.clnt.maincol}bio-{time_now}.txt") + self.c.success(f"Saved bio: {colours['main_colour']}bio-{time_now}.txt") if self.restore_data['banner-bytes'] != "": f = open(f"banner-{time_now}.gif", "wb") f.write(base64.b64decode(self.restore_data['banner-bytes'])) f.close() - self.c.success(f"Saved banner: {self.c.clnt.maincol}banner-{time_now}.gif") + self.c.success(f"Saved banner: {colours['main_colour']}banner-{time_now}.gif") def _get_user_info(self, userid): while True: - req = requests.get(f'https://discord.com/api/v9/users/{userid}', - headers={ # yeah.. discord bots require you not to use regular user agents, otherwise cf blocks you, weird shit lol - 'authorization' : f'Bot {self.bot_token}' + req = request_client.get(f'https://discord.com/api/v9/users/{userid}', + # seems like Cloudflare blocks requests that: + # 1) have a bot token + # 2) contain a browser user-agent + # probably to make sure bot devs identify what library they are using (for analytical purposes?) + + headers={ + 'Authorization' : f'Bot {self.bot_token}', + 'User-Agent': 'discord.py' } ) if "You are being rate limited." in req.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{req.json()['retry_after']} seconds{self.c.clnt.white}.") + self.c.warn(f"Rate Limited: {colours['main_colour']}{req.json()['retry_after']} seconds{colours['white']}.") time.sleep(req.json()["retry_after"]) else: if req.status_code != 200: - self.c.fail(f"Couldn't fetch username: {self.c.clnt.maincol}{userid}{self.c.clnt.white} ({req.json()['message']})") + self.c.fail(f"Couldn't fetch username: {colours['main_colour']}{userid}{colours['white']} ({req.json()['message']})") return 'Error' else: - self.c.success(f"Fetched username: {self.c.clnt.maincol}{userid}{self.c.clnt.white}.") + self.c.success(f"Fetched username: {colours['main_colour']}{userid}{colours['white']}.") return f"{req.json()['username']}#{req.json()['discriminator']}" def folders(self): while True: - user_guilds_req = requests.get(f"https://discord.com/api/v9/users/@me/guilds", - headers=self._headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=True) + user_guilds_req = request_client.get(f"https://discord.com/api/v9/users/@me/guilds", + headers=build_headers("get", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True) ) if "You are being rate limited." in user_guilds_req.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{user_guilds_req.json()['retry_after']} seconds{self.c.clnt.white}.") + self.c.warn(f"Rate Limited: {colours['main_colour']}{user_guilds_req.json()['retry_after']} seconds{colours['white']}.") time.sleep(user_guilds_req.json()["retry_after"]) else: break @@ -434,12 +416,12 @@ def folders(self): data_to_send.append(tmp) while True: - r = requests.patch(f"https://discord.com/api/v9/users/@me/settings", - headers = self._headers("patch", debugoptions=True, discordlocale=True, superprop=True, authorization=True), + r = request_client.patch(f"https://discord.com/api/v9/users/@me/settings", + headers = build_headers("patch", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True), json = {"guild_folders": data_to_send} ) if "You are being rate limited." in r.text: - self.c.warn(f"Rate Limited: {self.c.clnt.maincol}{r.json()['retry_after']} seconds{self.c.clnt.white}.") + self.c.warn(f"Rate Limited: {colours['main_colour']}{r.json()['retry_after']} seconds{colours['white']}.") time.sleep(r.json()["retry_after"]) else: break @@ -447,15 +429,15 @@ def folders(self): if r.status_code == 200: self.c.success(f"Restored Guild Folders") else: - self.c.fail(f"Couldn't Restore Guild Folders: {self.c.clnt.maincol}{r.status_code}") + self.c.fail(f"Couldn't Restore Guild Folders: {colours['main_colour']}{r.status_code}") def favourited_gifs(self): if self.restore_data.get('settings') == None: self.c.warn(f"Couldn't Find Favourite GIFs on Backup") return "Wasn't on Backup" else: - r = requests.patch(f"https://discord.com/api/v9/users/@me/settings-proto/2", - headers = self._headers("patch", debugoptions=True, discordlocale=True, superprop=True, authorization=True), + r = request_client.patch(f"https://discord.com/api/v9/users/@me/settings-proto/2", + headers = build_headers("patch", debugoptions=True, discordlocale=True, superprop=True, authorization=self.token, timezone=True), json = {"settings": self.restore_data['settings']} ) if r.status_code == 200: diff --git a/startup.py b/startup.py deleted file mode 100644 index 5f0f79b..0000000 --- a/startup.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2021 github.com/ItsChasa -# -# This source code has been released under the GNU Affero General Public -# License v3.0. A copy of this license is available at -# https://www.gnu.org/licenses/agpl-3.0.en.html - -import requests, sys, time, os, ctypes -from colorama import Fore -white = Fore.WHITE -gray = Fore.LIGHTBLACK_EX -red = Fore.RED - -from cfgload import Config - -colours = { - "light_red": Fore.LIGHTRED_EX, - "dark_red": Fore.RED, - "yellow": Fore.YELLOW, - "dark_blue": Fore.BLUE, - "light_blue": Fore.LIGHTBLUE_EX, - "dark_cyan": Fore.CYAN, - "light_cyan": Fore.LIGHTCYAN_EX, - "green": Fore.LIGHTGREEN_EX, - "purple": Fore.MAGENTA, - "pink": Fore.LIGHTMAGENTA_EX, - "gray": Fore.LIGHTBLACK_EX, - "black": Fore.BLACK -} - -class Setup(): - def welcome(self): - print(f"""{self.maincol} ___ _ -( _ \ ( ) -| (_) ) _ _ ___| |/ ) _ _ _ _ -| _ ( / _ )/ ___) ( ( ) ( ) _ \ -| (_) ) (_| | (___| |\ \| (_) | (_) ) -(____/ \__ _)\____)_) (_)\___/| __/ - | | - (_) {self.white} -> Made with {self.maincol}<3{self.white} by {self.maincol}chasa{self.white} -> Github: {self.maincol}https://github.com/ItsChasa/Discord-Backup{self.white} -> Don't forget to {self.yellow}star{self.white} it! -> Version: {self.maincol}{self.version}""") - - def __init__(self, module, version): - self.module = module - self.version = version - try: self.latest_ver = requests.get("https://api.github.com/repos/ItsChasa/Discord-Backup/releases/latest").json()['tag_name'] - except: self.latest_ver = version - - self.white = Fore.WHITE - self.gray = Fore.LIGHTBLACK_EX - self.red = Fore.RED - self.green = Fore.LIGHTGREEN_EX - self.yellow = Fore.YELLOW - self.blue = Fore.LIGHTBLUE_EX - - try: cwd = sys.argv[2] - except: pass - else: os.chdir(cwd) - - try: self.cfg = Config() - except: - print("missing config file, redownload") - time.sleep(3) - os._exit(1) - - try: self.maincol = colours[self.cfg.colour] - except: self.maincol = Fore.MAGENTA - - self.welcome() - ctypes.windll.kernel32.SetConsoleTitleW(f"chasa.wtf | {self.module}") - - if self.latest_ver != version: - print(f"\n{self.yellow}>{white} You are running an outdated version! Download the new version on GitHub: {self.latest_ver}") - \ No newline at end of file