diff --git a/src/main.py b/src/main.py index d4c7792..177f2f8 100644 --- a/src/main.py +++ b/src/main.py @@ -14,7 +14,7 @@ import output import logging import keys import application -sys.excepthook = lambda x, y, z: logging.critical(''.join(traceback.format_exception(x, y, z))) +#sys.excepthook = lambda x, y, z: logging.critical(''.join(traceback.format_exception(x, y, z))) from mysc.thread_utils import call_threaded from wxUI import commonMessages diff --git a/src/session.defaults b/src/session.defaults index ac21004..96054f0 100644 --- a/src/session.defaults +++ b/src/session.defaults @@ -2,6 +2,7 @@ user = string(default="") password = string(default="") token = string(default="") +secret = string(default="") use_alternative_tokens = boolean(default=False) invited_to_group = boolean(default=False) diff --git a/src/sessionmanager/core.py b/src/sessionmanager/core.py index 10b1c99..e5dc65c 100644 --- a/src/sessionmanager/core.py +++ b/src/sessionmanager/core.py @@ -3,6 +3,29 @@ import webbrowser import random import requests import string +from uuid import getnode + +def create_mac_string(num, splitter=':'): + """Return the mac address interpretation of num, + in the form eg '00:11:22:33:AA:BB'. + + :param num: a 48-bit integer (eg from uuid.getnode) + :param spliiter: a string to join the hex pairs with + """ + + mac = hex(num)[2:] + + # trim trailing L for long consts + if mac[-1] == 'L': + mac = mac[:-1] + + pad = max(12 - len(mac), 0) + mac = '0' * pad + mac + mac = splitter.join([mac[x:x + 2] for x in range(0, 12, 2)]) + mac = mac.upper() + + return mac + from . import _sslfixer from .wxUI import two_factor_auth @@ -20,7 +43,13 @@ client_secret = 'lxhD8OD7dMsqtXIm5IUY' api_ver='5.92' scope = 'all' user_agent = 'KateMobileAndroid/47-427 (Android 6.0.1; SDK 23; armeabi-v7a; samsung SM-G900F; ru)' -android_id = '4119748609680577006' + +mac_int = getnode() +device_id = create_mac_string(mac_int) +android_id = device_id.replace(':', '') + +#android_id = '4119748609680577006' + android_token = '5228540069896927210' api_url = 'https://api.vk.com/method/' diff --git a/src/sessionmanager/gettokens.py b/src/sessionmanager/gettokens.py new file mode 100644 index 0000000..d190239 --- /dev/null +++ b/src/sessionmanager/gettokens.py @@ -0,0 +1,80 @@ +""" Set of methods used to retrieve access tokens by simulating an official VK application. """ +import random +import requests +from hashlib import md5 +from .wxUI import two_factor_auth + +class AuthenticationError(Exception): + pass + +# Data extracted from official VK android APP. +client_id = '2274003' +client_secret = 'hHbZxrka2uZ6jB1inYsH' +api_ver='5.93' +scope = 'nohttps,all' +user_agent = 'VKAndroidApp/5.23-2978 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en; 320x240)' + +api_url = 'https://api.vk.com/method/' + +def get_device_id(): + """ Generate a random device ID, consisting in 16 alphanumeric characters.""" + return "".join(random.choice(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f"]) for _ in range(16)) + +def get_non_refreshed(login, password, scope=scope): + """ Retrieves a non-refreshed token, this should be the first token needed to authenticate in VK. + returns the access_token which still needs to be refreshed, current user_id, and secret code, needed to sign all petitions in VK.""" + if not (login or password): + raise ValueError + device_id = get_device_id() + # Let's authenticate. + url = "https://oauth.vk.com/token" + params = dict(grant_type="password", lang="en", + client_id=client_id, client_secret=client_secret, username=login, + password=password, v=api_ver, scope=scope, device_id=device_id) + # Add two factor auth later due to python's syntax. + params["2fa_supported"] = 1 + headers = {'User-Agent': user_agent} + r = requests.get(url, params=params, headers=headers) + # If a 401 error is raised, we need to use 2FA here. + # see https://vk.com/dev/auth_direct (switch lang to russian, english docs are very incomplete in the matter) + # ToDo: this needs testing after implemented official VK tokens. + if r.status_code == 401 and "phone_mask" in r.text: + t = r.json() + code, remember = two_factor_auth() + url = "https://oauth.vk.com/token" + params = dict(grant_type="password", lang="en", + client_id=client_id, client_secret=client_secret, username=login, + password=password, v=api_ver, scope=scope, device_id=device_id, code=code) + r = requests.get(url, params=params, headers=headers) + if r.status_code == 200 and 'access_token' in r.text: + res = r.json() + # Retrieve access_token, user_id and secret. + access_token = res['access_token'] + print(access_token) + user_id = str(res['user_id']) + secret = res['secret'] +# return access_token, user_id, secret + response = refresh_token(access_token, secret, device_id) + print(response["response"]["token"]) + return response["response"]["token"], secret, device_id + else: + raise AuthenticationError(r.text) + +def perform_request(method, postdata, secret): + """ Send a request to VK servers by signing the data with the 'sig' parameter.""" + url = "https://api.vk.com/method/"+method + sig = md5(b"/method/"+method.encode("utf-8")+b"?"+postdata.encode("utf-8")+secret.encode("utf-8")) + postdata = postdata+"&sig="+sig.hexdigest() + headers = { + 'User-Agent': user_agent + } + r = requests.post(url+"?"+postdata, headers=headers) + return r.json() + +def refresh_token(token, secret, device_id): + method = "execute.getUserInfo" + postdata = "v=5.93&https=1&androidVersion=19&androidModel=Android SDK built for x86&info_fields=audio_ads,audio_background_limit,country,discover_design_version,discover_preload,discover_preload_not_seen,gif_autoplay,https_required,inline_comments,intro,lang,menu_intro,money_clubs_p2p,money_p2p,money_p2p_params,music_intro,audio_restrictions,profiler_settings,raise_to_record_enabled,stories,masks,subscriptions,support_url,video_autoplay,video_player,vklive_app,community_comments,webview_authorization,story_replies,animated_stickers,community_stories,live_section,playlists_download,calls,security_issue,eu_user,wallet,vkui_community_create,vkui_profile_edit,vkui_community_manage,vk_apps,stories_photo_duration,stories_reposts,live_streaming,live_masks,camera_pingpong,role,video_discover&device_id="+device_id+"&lang=en&func_v=11&androidManufacturer=unknown&fields=photo_100,photo_50,exports,country,sex,status,bdate,first_name_gen,last_name_gen,verified,trending&access_token="+token + perform_request(method, postdata, secret) + method = "auth.refreshToken" + postdata = "v=5.93&https=1×tamp=0&receipt=''&receipt2=''&device_id="+device_id+"&lang=en&nonce=''&access_token="+token + return perform_request(method, postdata, secret) \ No newline at end of file diff --git a/src/sessionmanager/session.py b/src/sessionmanager/session.py index 2d979e9..542b89a 100644 --- a/src/sessionmanager/session.py +++ b/src/sessionmanager/session.py @@ -113,6 +113,7 @@ class vkSession(object): config_filename = os.path.join(paths.config_path(), self.session_id, "vkconfig.json") self.vk.login(self.settings["vk"]["user"], self.settings["vk"]["password"], token=self.settings["vk"]["token"], alt_token=self.settings["vk"]["use_alternative_tokens"], filename=config_filename) self.settings["vk"]["token"] = self.vk.session_object.token["access_token"] + self.settings["vk"]["secret"] = self.vk.session_object.secret self.settings.write() self.logged = True self.get_my_data() diff --git a/src/sessionmanager/vkSessionHandler.py b/src/sessionmanager/vkSessionHandler.py index dd9cfa6..0112b07 100644 --- a/src/sessionmanager/vkSessionHandler.py +++ b/src/sessionmanager/vkSessionHandler.py @@ -1,7 +1,7 @@ #!/usr/bin/python import keys import logging -from . import core +from . import gettokens from vk_api.audio import VkAudio from . wxUI import two_factor_auth @@ -19,12 +19,12 @@ class vkObject(object): from . import vk_api_patched as vk_api if token == "" or token == None: log.info("Token is not valid. Generating one...") - token = core.requestAuth(user, password) - token = token[0] - receipt = core.getReceipt(token) - token = core.validateToken(token, receipt) + original_token = gettokens.get_non_refreshed(user, password) + token = original_token[0] + secret = original_token[1] + device_id = original_token[2] log.info("Token validated...") - self.session_object = vk_api.VkApi(app_id=self.api_key, login=user, password=password, token=token, scope="offline, wall, notify, friends, photos, audio, video, docs, notes, pages, status, groups, messages, notifications, stats", config_filename=filename) + self.session_object = vk_api.VkApi(app_id=self.api_key, login=user, password=password, token=token, secret=secret, device_id=device_id, scope="offline, wall, notify, friends, photos, audio, video, docs, notes, pages, status, groups, messages, notifications, stats", config_filename=filename) else: import vk_api self.session_object = vk_api.VkApi(app_id=self.api_key, login=user, password=password, scope="offline, wall, notify, friends, photos, audio, video, docs, notes, pages, status, groups, messages, notifications, stats", config_filename=filename, auth_handler=two_factor_auth) diff --git a/src/sessionmanager/vk_api_patched.py b/src/sessionmanager/vk_api_patched.py index 3319451..4a73bc4 100644 --- a/src/sessionmanager/vk_api_patched.py +++ b/src/sessionmanager/vk_api_patched.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """ this is a patched version of vk_api to use a different user agent for authenticating against VK. Everything else looks the same, the only change in the module is the new user agent, emulating a kate mobile session.""" +import time +import hashlib import logging import vk_api import threading @@ -13,7 +15,7 @@ DEFAULT_USER_SCOPE = sum(VkUserPermissions) class VkApi(vk_api.VkApi): - def __init__(self, login=None, password=None, token=None, + def __init__(self, login=None, password=None, token=None, secret=None, device_id=None, auth_handler=None, captcha_handler=None, config=jconfig.Config, config_filename='vk_config.v2.json', api_version='5.92', app_id=2685278, scope=DEFAULT_USER_SCOPE, @@ -23,7 +25,8 @@ class VkApi(vk_api.VkApi): self.password = password self.token = {'access_token': token} - + self.secret = secret + self.device_id = device_id self.api_version = api_version self.app_id = app_id self.scope = scope @@ -32,7 +35,7 @@ class VkApi(vk_api.VkApi): self.storage = config(self.login, filename=config_filename) self.http = requests.Session() - self.http.headers.update({'User-agent': 'KateMobileAndroid/47-427 (Android 6.0.1; SDK 23; armeabi-v7a; samsung SM-G900F; ru)'}) + self.http.headers.update({'User-agent': 'VKAndroidApp/5.23-2978 (Android 4.4.2; SDK 19; x86; unknown Android SDK built for x86; en; 320x240)'}) self.last_request = 0.0 @@ -46,4 +49,96 @@ class VkApi(vk_api.VkApi): self.lock = threading.Lock() self.logger = logging.getLogger('vk_api_patched') - self.logger.info('Started patched VK API client...') \ No newline at end of file + self.logger.info('Started patched VK API client...') + + def method(self, method, values=None, captcha_sid=None, captcha_key=None, + raw=False): + """ Вызов метода API + + :param method: название метода + :type method: str + + :param values: параметры + :type values: dict + + :param captcha_sid: id капчи + :type captcha_key: int or str + + :param captcha_key: ответ капчи + :type captcha_key: str + + :param raw: при False возвращает `response['response']` + при True возвращает `response` + (может понадобиться для метода execute для получения + execute_errors) + :type raw: bool + """ + + values = values.copy() if values else {} + + if 'v' not in values: + values['v'] = self.api_version + + if self.token: + values['access_token'] = self.token['access_token'] + + if captcha_sid and captcha_key: + values['captcha_sid'] = captcha_sid + values['captcha_key'] = captcha_key + + with self.lock: + # Ограничение 3 запроса в секунду + delay = self.RPS_DELAY - (time.time() - self.last_request) + + if delay > 0: + time.sleep(delay) + values.update(https=1, device_id=self.device_id) + sig = self.get_sig(method, values, self.secret) + values.update(sig=sig) + response = self.http.post( + 'https://api.vk.com/method/' + method, + values + ) + self.last_request = time.time() + + if response.ok: + response = response.json() + else: + error = ApiHttpError(self, method, values, raw, response) + response = self.http_handler(error) + + if response is not None: + return response + + raise error + + if 'error' in response: + error = ApiError(self, method, values, raw, response['error']) + + if error.code in self.error_handlers: + if error.code == CAPTCHA_ERROR_CODE: + error = Captcha( + self, + error.error['captcha_sid'], + self.method, + (method,), + {'values': values, 'raw': raw}, + error.error['captcha_img'] + ) + + response = self.error_handlers[error.code](error) + + if response is not None: + return response + + raise error + + return response if raw else response['response'] + + def get_sig(self, method, values, secret): + postdata = "" + for key in values: + postdata = postdata + "{key}={value}&".format(key=key, value=values[key]) + postdata = postdata[:-1] + sig = hashlib.md5(b"/method/"+method.encode("utf-8")+b"?"+postdata.encode("utf-8")+secret.encode("utf-8")) + return sig.hexdigest() \ No newline at end of file