diff --git a/addon.xml b/addon.xml index 08a355c..42aaab2 100644 --- a/addon.xml +++ b/addon.xml @@ -1,8 +1,8 @@ - + - - + + @@ -13,7 +13,7 @@ vk.com API Python wrapper Based on code taken from https://pypi.python.org/pypi/vk/ GPLv3 - https://github.com/inpos/script.module.vk - https://github.com/inpos/script.module.vk + https://git.ukamnya.ru/ukamnya/script.module.vk + https://git.ukamnya.ru/ukamnya/script.module.vk - \ No newline at end of file + diff --git a/lib/vk/__init__.py b/lib/vk/__init__.py index 052fb16..42caa51 100644 --- a/lib/vk/__init__.py +++ b/lib/vk/__init__.py @@ -1,9 +1,5 @@ +from .session import API, UserAPI, CommunityAPI -from vk.api import logger -from vk.api import Session, AuthSession, InteractiveSession, InteractiveAuthSession -from vk.api import VERSION -from vk.api import API +__version__ = '3.0-dev' -__version__ = version = VERSION - -# API = OAuthAPI +__all__ = (API, UserAPI, CommunityAPI) diff --git a/lib/vk/__init__.pyc b/lib/vk/__init__.pyc deleted file mode 100644 index e903b48..0000000 Binary files a/lib/vk/__init__.pyc and /dev/null differ diff --git a/lib/vk/api.py b/lib/vk/api.py index b51b9cb..13bf503 100644 --- a/lib/vk/api.py +++ b/lib/vk/api.py @@ -1,185 +1,38 @@ -# coding=utf8 - import logging -import logging.config -from vk.logs import LOGGING_CONFIG -from vk.utils import stringify_values, json_iter_parse, LoggingSession, str_type -from vk.exceptions import VkAuthError, VkAPIError -from vk.mixins import AuthMixin, InteractiveMixin -from sys import version_info +from vk.utils import stringify_values -VERSION = '2.0.2' - -if version_info[0] >= 2 and version_info[1] >= 7: # http://xbmc.ru/forum/showpost.php?p=107619&postcount=20 - logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger('vk') -class Session(object): - API_URL = 'https://api.vk.com/method/' - - def __init__(self, access_token=None): - - logger.debug('API.__init__(access_token=%(access_token)r)', {'access_token': access_token}) - - self.access_token = access_token - self.access_token_is_needed = False - - self.requests_session = LoggingSession() - self.requests_session.headers['Accept'] = 'application/json' - self.requests_session.headers['Content-Type'] = 'application/x-www-form-urlencoded' - - @property - def access_token(self): - logger.debug('Check that we need new access token') - if self.access_token_is_needed: - logger.debug('We need new access token. Try to get it.') - self.access_token = self.get_access_token() - else: - logger.debug('Use old access token') - return self._access_token - - @access_token.setter - def access_token(self, value): - self._access_token = value - if isinstance(value, str_type) and len(value) >= 12: - self.censored_access_token = '{0}***{1}'.format(value[:4], value[-4:]) # http://xbmc.ru/forum/showpost.php?p=107619&postcount=20 - else: - self.censored_access_token = value - logger.debug('access_token = %r', self.censored_access_token) - self.access_token_is_needed = not self._access_token - - def get_user_login(self): - logger.debug('Do nothing to get user login') - - def get_access_token(self): - """ - Dummy method - """ - logger.debug('API.get_access_token()') - return self._access_token - - def make_request(self, method_request, captcha_response=None): - - logger.debug('Prepare API Method request') - - response = self.send_api_request(method_request, captcha_response=captcha_response) - # todo Replace with something less exceptional - response.raise_for_status() - - # there are may be 2 dicts in one JSON - # for example: "{'error': ...}{'response': ...}" - for response_or_error in json_iter_parse(response.text): - if 'response' in response_or_error: - # todo Can we have error and response simultaneously - # for error in errors: - # logger.warning(str(error)) - - return response_or_error['response'] - - elif 'error' in response_or_error: - error_data = response_or_error['error'] - error = VkAPIError(error_data) - - if error.is_captcha_needed(): - captcha_key = self.get_captcha_key(error.captcha_img) - if not captcha_key: - raise error - - captcha_response = { - 'sid': error.captcha_sid, - 'key': captcha_key, - } - return self.make_request(method_request, captcha_response=captcha_response) - - elif error.is_access_token_incorrect(): - logger.info('Authorization failed. Access token will be dropped') - self.access_token = None - return self.make_request(method_request) - - else: - raise error - - def send_api_request(self, request, captcha_response=None): - url = self.API_URL + request._method_name - method_args = request._api._method_default_args.copy() - method_args.update(stringify_values(request._method_args)) - access_token = self.access_token - if access_token: - method_args['access_token'] = access_token - if captcha_response: - method_args['captcha_sid'] = captcha_response['sid'] - method_args['captcha_key'] = captcha_response['key'] - timeout = request._api._timeout - response = self.requests_session.post(url, method_args, timeout=timeout) - return response - - def get_captcha_key(self, captcha_image_url): - """ - Default behavior on CAPTCHA is to raise exception - Reload this in child - """ - return None - - def auth_code_is_needed(self, content, session): - """ - Default behavior on 2-AUTH CODE is to raise exception - Reload this in child - """ - raise VkAuthError('Authorization error (2-factor code is needed)') - - def auth_captcha_is_needed(self, content, session): - """ - Default behavior on CAPTCHA is to raise exception - Reload this in child - """ - raise VkAuthError('Authorization error (captcha)') - - def phone_number_is_needed(self, content, session): - """ - Default behavior on PHONE NUMBER is to raise exception - Reload this in child - """ - logger.error('Authorization error (phone number is needed)') - raise VkAuthError('Authorization error (phone number is needed)') - - -class API(object): - def __init__(self, session, timeout=10, **method_default_args): - self._session = session - self._timeout = timeout - self._method_default_args = method_default_args - - def __getattr__(self, method_name): - return Request(self, method_name) - - def __call__(self, method_name, **method_kwargs): - return getattr(self, method_name)(**method_kwargs) - - -class Request(object): - __slots__ = ('_api', '_method_name', '_method_args') - - def __init__(self, api, method_name): +class APINamespace: + def __init__(self, api, method_common_params): self._api = api - self._method_name = method_name + self._method_common_params = method_common_params - def __getattr__(self, method_name): - return Request(self._api, self._method_name + '.' + method_name) + def __call__(self, method): + return APIMethod(self._api, method, self._method_common_params) - def __call__(self, **method_args): - self._method_args = method_args - return self._api._session.make_request(self) + __getattr__ = __call__ -class AuthSession(AuthMixin, Session): - pass +class APIMethod: + def __init__(self, api, method, method_common_params): + self._api = api + self._method = method + self._method_common_params = method_common_params + + def __getattr__(self, method): + return self.__class__(self._api, self._method + '.' + method, self._method_common_params) + + def __call__(self, **method_params): + request_method_params = self._method_common_params.copy() + request_method_params.update(stringify_values(method_params)) + + return self._api.send(APIRequest(self._method, request_method_params)) -class InteractiveSession(InteractiveMixin, Session): - pass - - -class InteractiveAuthSession(InteractiveMixin, AuthSession): - pass +class APIRequest: + def __init__(self, method, method_params): + self.method = method + self.method_params = method_params diff --git a/lib/vk/api.pyc b/lib/vk/api.pyc deleted file mode 100644 index d57dad0..0000000 Binary files a/lib/vk/api.pyc and /dev/null differ diff --git a/lib/vk/exceptions.py b/lib/vk/exceptions.py index 19987fa..c5763dc 100644 --- a/lib/vk/exceptions.py +++ b/lib/vk/exceptions.py @@ -4,8 +4,7 @@ AUTHORIZATION_FAILED = 5 # Invalid access token PERMISSION_IS_DENIED = 7 CAPTCHA_IS_NEEDED = 14 ACCESS_DENIED = 15 # No access to call this method - # User deactivated -INVALID_USER_ID = 113 +INVALID_USER_ID = 113 # User deactivated class VkException(Exception): @@ -33,7 +32,7 @@ class VkAPIError(VkException): @staticmethod def get_pretty_request_params(error_data): request_params = error_data.get('request_params', ()) - request_params = dict((param['key'], param['value']) for param in request_params) # http://xbmc.ru/forum/showpost.php?p=107615&postcount=16 + request_params = {param['key']: param['value'] for param in request_params} return request_params def is_access_token_incorrect(self): diff --git a/lib/vk/exceptions.pyc b/lib/vk/exceptions.pyc deleted file mode 100644 index fe7e690..0000000 Binary files a/lib/vk/exceptions.pyc and /dev/null differ diff --git a/lib/vk/logs.py b/lib/vk/logs.py deleted file mode 100644 index 2a8cc35..0000000 --- a/lib/vk/logs.py +++ /dev/null @@ -1,27 +0,0 @@ - -import sys - - -LOGGING_CONFIG = { - 'version': 1, - 'disable_existing_loggers': False, - 'loggers': { - 'vk': { - 'level': 'INFO', - 'handlers': ['vk-stdout'], - 'propagate': False, - }, - }, - 'handlers': { - 'vk-stdout': { - 'class': 'logging.StreamHandler', - 'stream': sys.stdout, - 'formatter': 'vk-verbose', - }, - }, - 'formatters': { - 'vk-verbose': { - 'format': '%(asctime)s %(name) -5s %(module)s:%(lineno)d %(levelname)s: %(message)s', - }, - }, -} diff --git a/lib/vk/logs.pyc b/lib/vk/logs.pyc deleted file mode 100644 index 6aaeb57..0000000 Binary files a/lib/vk/logs.pyc and /dev/null differ diff --git a/lib/vk/mixins.py b/lib/vk/mixins.py deleted file mode 100644 index f8c40a4..0000000 --- a/lib/vk/mixins.py +++ /dev/null @@ -1,218 +0,0 @@ -# coding=utf8 - -import logging - -from vk.exceptions import VkAuthError -from vk.utils import raw_input, get_url_query, LoggingSession, get_form_action - - -logger = logging.getLogger('vk') - - -class AuthMixin(object): - LOGIN_URL = 'https://m.vk.com' - # REDIRECT_URI = 'https://oauth.vk.com/blank.html' - AUTHORIZE_URL = 'https://oauth.vk.com/authorize' - CAPTCHA_URI = 'https://m.vk.com/captcha.php' - - def __init__(self, app_id=None, user_login='', user_password='', scope='offline', **kwargs): - logger.debug('AuthMixin.__init__(app_id=%(app_id)r, user_login=%(user_login)r, user_password=%(user_password)r, **kwargs=%(kwargs)s)', - dict(app_id=app_id, user_login=user_login, user_password=user_password, kwargs=kwargs)) - - super(AuthMixin, self).__init__(**kwargs) - - self.app_id = app_id - self.user_login = user_login - self.user_password = user_password - self.scope = scope - - # Some API methods get args (e.g. user id) from access token. - # If we define user login, we need get access token now. - if self.user_login: - self.access_token = self.get_access_token() - - @property - def user_login(self): - if not self._user_login: - self._user_login = self.get_user_login() - return self._user_login - - @user_login.setter - def user_login(self, value): - self._user_login = value - - def get_user_login(self): - return self._user_login - - @property - def user_password(self): - if not self._user_password: - self._user_password = self.get_user_password() - return self._user_password - - @user_password.setter - def user_password(self, value): - self._user_password = value - - def get_user_password(self): - return self._user_password - - def get_access_token(self): - """ - Get access token using app id and user login and password. - """ - logger.debug('AuthMixin.get_access_token()') - - auth_session = LoggingSession() - with auth_session as self.auth_session: - self.auth_session = auth_session - self.login() - auth_response_url_query = self.oauth2_authorization() - - if 'access_token' in auth_response_url_query: - return auth_response_url_query['access_token'] - else: - raise VkAuthError('OAuth2 authorization error') - - def login(self): - """ - Login - """ - - response = self.auth_session.get(self.LOGIN_URL) - login_form_action = get_form_action(response.text) - if not login_form_action: - raise VkAuthError('VK changed login flow') - - login_form_data = { - 'email': self.user_login, - 'pass': self.user_password, - } - response = self.auth_session.post(login_form_action, login_form_data) - logger.debug('Cookies: %s', self.auth_session.cookies) - - response_url_query = get_url_query(response.url) - - if 'remixsid' in self.auth_session.cookies or 'remixsid6' in self.auth_session.cookies: - return - - if 'sid' in response_url_query: - self.auth_captcha_is_needed(response, login_form_data) - elif response_url_query.get('act') == 'authcheck': - self.auth_check_is_needed(response.text) - elif 'security_check' in response_url_query: - self.phone_number_is_needed(response.text) - else: - message = 'Authorization error (incorrect password)' - logger.error(message) - raise VkAuthError(message) - - def oauth2_authorization(self): - """ - OAuth2 - """ - auth_data = { - 'client_id': self.app_id, - 'display': 'mobile', - 'response_type': 'token', - 'scope': self.scope, - 'v': '5.28', - } - response = self.auth_session.post(self.AUTHORIZE_URL, auth_data) - response_url_query = get_url_query(response.url) - if 'access_token' in response_url_query: - return response_url_query - - # Permissions is needed - logger.info('Getting permissions') - # form_action = re.findall(r'
', auth_response.text)[0] - form_action = get_form_action(response.text) - logger.debug('Response form action: %s', form_action) - if form_action: - response = self.auth_session.get(form_action) - response_url_query = get_url_query(response.url) - return response_url_query - - try: - response_json = response.json() - except ValueError: # not JSON in response - error_message = 'OAuth2 grant access error' - else: - error_message = 'VK error: [{}] {}'.format(response_json['error'], response_json['error_description']) - logger.error('Permissions obtained') - raise VkAuthError(error_message) - - def auth_check_is_needed(self, html): - logger.info('User enabled 2 factors authorization. Auth check code is needed') - auth_check_form_action = get_form_action(html) - auth_check_code = self.get_auth_check_code() - auth_check_data = { - 'code': auth_check_code, - '_ajax': '1', - 'remember': '1' - } - response = self.auth_session.post(auth_check_form_action, data=auth_check_data) - - def auth_captcha_is_needed(self, response, login_form_data): - logger.info('Captcha is needed') - - response_url_dict = get_url_query(response.url) - - # form_url = re.findall(r'', response.text) - captcha_form_action = get_form_action(response.text) - logger.debug('form_url %s', captcha_form_action) - if not captcha_form_action: - raise VkAuthError('Cannot find form url') - - # todo Are we sure that `response_url_dict` doesn't contain CAPTCHA image url? - captcha_url = '%s?s=%s&sid=%s' % (self.CAPTCHA_URI, response_url_dict['s'], response_url_dict['sid']) - # logger.debug('Captcha url %s', captcha_url) - - login_form_data['captcha_sid'] = response_url_dict['sid'] - login_form_data['captcha_key'] = self.get_captcha_key(captcha_url) - - response = self.auth_session.post(captcha_form_action, login_form_data) - - # logger.debug('Cookies %s', self.auth_session.cookies) - # if 'remixsid' not in self.auth_session.cookies and 'remixsid6' not in self.auth_session.cookies: - # raise VkAuthError('Authorization error (Bad password or captcha key)') - - def phone_number_is_needed(self, text): - raise VkAuthError('Phone number is needed') - - def get_auth_check_code(self): - raise VkAuthError('Auth check code is needed') - - -class InteractiveMixin(object): - def get_user_login(self): - user_login = raw_input('VK user login: ') - return user_login.strip() - - def get_user_password(self): - import getpass - - user_password = getpass.getpass('VK user password: ') - return user_password - - def get_access_token(self): - logger.debug('InteractiveMixin.get_access_token()') - access_token = super(InteractiveMixin, self).get_access_token() - if not access_token: - access_token = raw_input('VK API access token: ') - return access_token - - def get_captcha_key(self, captcha_image_url): - """ - Read CAPTCHA key from shell - """ - print('Open CAPTCHA image url: ', captcha_image_url) - captcha_key = raw_input('Enter CAPTCHA key: ') - return captcha_key - - def get_auth_check_code(self): - """ - Read Auth code from shell - """ - auth_check_code = raw_input('Auth check code: ') - return auth_check_code.strip() diff --git a/lib/vk/mixins.pyc b/lib/vk/mixins.pyc deleted file mode 100644 index f8fb2e5..0000000 Binary files a/lib/vk/mixins.pyc and /dev/null differ diff --git a/lib/vk/session.py b/lib/vk/session.py new file mode 100644 index 0000000..0d97915 --- /dev/null +++ b/lib/vk/session.py @@ -0,0 +1,281 @@ +import re +import urllib +import logging + +import requests + +from .exceptions import VkAuthError, VkAPIError +from .api import APINamespace +from .utils import json_iter_parse, stringify + +logger = logging.getLogger('vk') + + +class APIBase: + METHOD_COMMON_PARAMS = {'v', 'lang', 'https', 'test_mode'} + + API_URL = 'https://api.vk.com/method/' + CAPTCHA_URL = 'https://m.vk.com/captcha.php' + + def __new__(cls, *args, **kwargs): + method_common_params = {key: kwargs.pop(key) for key in tuple(kwargs) if key in cls.METHOD_COMMON_PARAMS} + + api = object.__new__(cls) + api.__init__(*args, **kwargs) + + return APINamespace(api, method_common_params) + + def __init__(self, timeout=10): + self.timeout = timeout + + self.session = requests.Session() + self.session.headers['Accept'] = 'application/json' + self.session.headers['Content-Type'] = 'application/x-www-form-urlencoded' + + def send(self, request): + + logger.debug('Prepare API Method request') + + self.prepare_request(request) + + method_url = self.API_URL + request.method + response = self.session.post(method_url, request.method_params, timeout=self.timeout) + + # todo Replace with something less exceptional + response.raise_for_status() + + # TODO: there are may be 2 dicts in one JSON + # for example: "{'error': ...}{'response': ...}" + for response_or_error in json_iter_parse(response.text): + request.response = response_or_error + + if 'response' in response_or_error: + # todo Can we have error and response simultaneously + # for error in errors: + # logger.warning(str(error)) + return response_or_error['response'] + + elif 'error' in response_or_error: + api_error = VkAPIError(request.response['error']) + request.api_error = api_error + return self.handle_api_error(request) + + def prepare_request(self, request): + request.method_params['access_token'] = self.access_token + + def get_access_token(self): + raise NotImplementedError + + def handle_api_error(self, request): + logger.error('Handle API error: %s', request.api_error) + + api_error_handler_name = 'on_api_error_' + str(request.api_error.code) + api_error_handler = getattr(self, api_error_handler_name, self.on_api_error) + + return api_error_handler(request) + + def on_api_error_14(self, request): + """ + 14. Captcha needed + """ + request.method_params['captcha_key'] = self.get_captcha_key(request) + request.method_params['captcha_sid'] = request.api_error.captcha_sid + + return self.send(request) + + def on_api_error_15(self, request): + """ + 15. Access denied + - due to scope + """ + logger.error('Authorization failed. Access token will be dropped') + self.access_token = self.get_access_token() + return self.send(request) + + def on_api_error(self, request): + logger.error('API error: %s', request.api_error) + raise request.api_error + + def get_captcha_key(self, request): + """ + Default behavior on CAPTCHA is to raise exception + Reload this in child + """ + # request.api_error.captcha_img + raise request.api_error + + +class API(APIBase): + def __init__(self, access_token, **kwargs): + super().__init__(**kwargs) + self.access_token = access_token + + +class UserAPI(APIBase): + LOGIN_URL = 'https://m.vk.com' + AUTHORIZE_URL = 'https://oauth.vk.com/authorize' + + def __init__(self, user_login='', user_password='', app_id=None, scope='offline', **kwargs): + super().__init__(**kwargs) + + self.user_login = user_login + self.user_password = user_password + self.app_id = app_id + self.scope = scope + + self.access_token = self.get_access_token() + + @staticmethod + def get_form_action(response): + form_action = re.findall(r'= 12: + return '{}***{}'.format(access_token[:4], access_token[-4:]) + elif access_token: + return '***' + else: + return access_token diff --git a/lib/vk/utils.pyc b/lib/vk/utils.pyc deleted file mode 100644 index 52d64ac..0000000 Binary files a/lib/vk/utils.pyc and /dev/null differ