Advertisement
stuppid_bot

Untitled

Jan 15th, 2016
240
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 19.17 KB | None | 0 0
  1. import hashlib
  2. import io
  3. import json
  4. import logging
  5. import os
  6. import platform
  7. import re
  8. import socket
  9. import time
  10. import urllib.parse
  11. import urllib.request
  12. import uuid
  13.  
  14. try:
  15.     from .structures import ObjectDict
  16. except SystemError:
  17.     from structures import ObjectDict
  18.  
  19. __author__ = "Sergei Snegirev (tz4678@gmail.com)"
  20. __copyright__ = "Copyright (C) 2013-2016 Sergei Snegirev"
  21. __license__ = "MIT"
  22. __version__ = "3.0"
  23. __url__ = "https://github.com/***/vkapi/"
  24.  
  25. API_DELAY = 0.34
  26. API_VERSION = 5.44
  27. TIMEOUT = 15
  28. TRIES = 3
  29. API_HOST = "api.vk.com"
  30. API_PATH = "/method/"
  31. HTTP_RE = re.compile('https?://', re.I)
  32. UA_FORMAT = "Mozilla/5.0 (vkapi.client/{ver} Python/{py_ver} +{url})"
  33. UA_NAME = UA_FORMAT.format(
  34.     ver=__version__,
  35.     py_ver=platform.python_version(),
  36.     url=__url__
  37. )
  38.  
  39.  
  40. class Client:
  41.     def __init__(self,
  42.                  access_token=None,
  43.                  api_version=API_VERSION,
  44.                  api_delay=API_DELAY,
  45.                  https=None,
  46.                  lang=None,
  47.                  opener=None,
  48.                  timeout=TIMEOUT,
  49.                  tries=TRIES):
  50.         """Клиент для работы с API Вконтакте.
  51.  
  52.        * Нативная поддержка методов API
  53.        * Работа с подписью
  54.        * Загрузка файлов
  55.  
  56.        :param access_token: Токен полученный при авторизации
  57.        :type access_token: vkapi.accesstoken.AccessToken instance
  58.  
  59.        :param api_version: Версия API
  60.        :type api_version: float
  61.  
  62.        :param api_delay: Задержка между вызовами методов API. Существуют
  63.            ограничения на количество обращений в секунду к методам API
  64.        :type api_delay: int or float
  65.  
  66.        :param https: Если равен 1, то методы API возвращают https ссылки на
  67.            фотографии и медиа файлы. Работает только если запросы
  68.            осуществляются через https.
  69.        :type https: int
  70.  
  71.        :param lang: Используемый язык (ru, ua, be, es, fi, de, it)
  72.        :type lang: str
  73.  
  74.        :param opener: Используется для отправки запросов
  75.        :type opener: urllib.request.OpenerDirector instance
  76.  
  77.        :param tries: Количество повторных запросов в случае ошибки таймаута.
  78.            Если значение равно -1, то запросы будут посылаться, пока не придет
  79.            ответ.
  80.        :type tries: int
  81.  
  82.        :param timeout: Таймаут
  83.        :type timeout: int or float
  84.        """
  85.         self.logger = logging.getLogger(
  86.             ".".join([self.__class__.__module__, self.__class__.__name__])
  87.         )
  88.         self.access_token = access_token
  89.         self.api_version = api_version
  90.         self.api_delay = api_delay
  91.         self.lang = lang
  92.         self.https = https
  93.         if not opener:
  94.             # Включаем поддержку cookies и используем системные прокси
  95.             opener = urllib.request.build_opener(
  96.                 urllib.request.HTTPCookieProcessor(),
  97.                 urllib.request.ProxyHandler()
  98.             )
  99.             opener.addheaders = [('User-Agent', UA_NAME)]
  100.         self.opener = opener
  101.         self.timeout = timeout
  102.         self.tries = tries
  103.         self.api = _Api(self)
  104.         self.last_api_request = 0
  105.  
  106.     @property
  107.     def is_authorized(self):
  108.         return bool(self.access_token)
  109.  
  110.     def api_request(self, method, params=None):
  111.         """Делает запрос к API.
  112.  
  113.        Список методов API: <https://vk.com/dev/methods>
  114.        Подробнее про запросы к API: <https://vk.com/dev/api_requests>
  115.  
  116.        Пример:
  117.        c = Client()
  118.        r = c.api_request('users.get', {'user_id': 1})
  119.        print("{u.first_name} {u.last_name}".format(u=r[0]))
  120.  
  121.        Использовать данный метод нет необходимости, так как к API можно
  122.        обращаться "нативно":
  123.  
  124.        Client.api.<method_name>(**kwargs)
  125.        Client.api.<method_name>(params_dict[, **kwargs])
  126.  
  127.        Пример выше можно было переписать так:
  128.  
  129.        r = c.api.users.get(user_id=1)
  130.        # либо так
  131.        r = c.api.users.get({'user_id': 1})
  132.        # или так
  133.        r = c.api.users.get({'user_id': 123, 'fields': 'photo'}, user_id=1)
  134.  
  135.        В последнем значение user_id в словаре будет заменено на 1. То есть,
  136.        значения словаря идущего первым аргументом заменяется содержимым
  137.        словаря с именованными параметрами (params_dict.update(kwargs)), и
  138.        будут переданы параметры:
  139.  
  140.        {'user_id': 1, 'fields': 'photo'}
  141.  
  142.        Если имя именованного параметра совпадает с ключевым словом, то к имени
  143.        нужно добавить подчеркивание (например, _from).
  144.  
  145.        :param method: Метод API
  146.        :type method: str
  147.  
  148.        :param params: Передаваемые параметры
  149.        :type params: dict
  150.  
  151.        :return: Возвращает результат запроса. Это содержимое поля `response`.
  152.            Заметьте, что к элементам словаря можно обращаться как к аттрибутам
  153.            через точечную нотацию.
  154.        """
  155.         params = dict(params or {})
  156.         if self.is_authorized:
  157.             params['access_token'] = self.access_token.value
  158.         # Позволяем в запросе выбрать версию API. Возможно, потребуется для
  159.         # обращения к каким-нибудь depractated методам
  160.         if 'v' not in params and self.api_version:
  161.             params['v'] = self.api_version
  162.         if 'lang' not in params and self.lang:
  163.             params['lang'] = self.lang
  164.         if 'https' not in params and self.https:
  165.             params['https'] = self.https
  166.         # Кстати, через http ответы сервера приходят заметно быстрее
  167.         # <https://vk.com/dev/api_nohttps>
  168.         if hasattr(self.access_token, "secret") and self.access_token.secret:
  169.             scheme = 'http'
  170.             # Исправлен баг с неверной подписью. См. <path/to/lib/notes.txt>.
  171.             # Нам важно чтобы порядок элементов словаря сохранился.
  172.             # Добавим новый элемент
  173.             params['sig'] = ''
  174.             # Сформировали query string из словаря
  175.             query_string = urllib.parse.urlencode(params)
  176.             # А теперь вырежем параметр sig
  177.             query_string = re.sub('^sig=&|&sig=', '', query_string)
  178.             uri = '{}{}?{}{}'.format(
  179.                 API_PATH,
  180.                 method,
  181.                 query_string,
  182.                 self.access_token.secret
  183.             )
  184.             params["sig"] = hashlib.md5(uri.encode('ascii')).hexdigest()
  185.         else:
  186.             scheme = 'https'
  187.         # !!! params не должен изменяться после добавления sig
  188.         delay = self.api_delay + self.last_api_request - time.time()
  189.         if delay > 0:
  190.             self.logger.debug("Wait %dms", delay * 1000)
  191.             time.sleep(delay)
  192.         self.logger.debug("Call method %r with parameters: %s", method, params)
  193.         api_url = '{}://{}{}{}'.format(scheme, API_HOST, API_PATH, method)
  194.         response = self.request(api_url, params)
  195.         error = ApiError(response) if 'error' in response else None
  196.         response = response.get('response')
  197.         return self._process_api_response(response, error, method, params)
  198.  
  199.     def _process_api_response(self, response, error, method, params):
  200.         """Можно переопределить в классах потомках, а можно что-то типа этого
  201.        сделать:
  202.  
  203.        def _process_api_response(self, response, error, method, params):
  204.            if error.error_code is error.TOO_MANY_REQUESTS_PER_SECOND:
  205.                time.sleep(5)
  206.                # Отправляем повторный запрос
  207.                return self.api_request(method, params)
  208.  
  209.            return super()._process_api_response(
  210.                response, error, method, params)
  211.        """
  212.         if error:
  213.             # Это для наглядности, а так можно просто писать:
  214.             # if error.error_code is error.CAPTCHA_NEEDED:
  215.             if error.error_code is ApiError.CAPTCHA_NEEDED:
  216.                 captcha_key = self.get_captcha_key(error.captcha_img)
  217.                 params.update({
  218.                     'captcha_key': captcha_key,
  219.                     'captcha_sid': error.captcha_sid
  220.                 })
  221.                 return self.api_request(method, params)
  222.             raise error
  223.         return response
  224.  
  225.     def get_captcha_key(self, captcha_img):
  226.         """Метод для переопределения в классах наследниках."""
  227.         raise NotImplementedError
  228.  
  229.     def test_access_token(self):
  230.         try:
  231.             return self.api.execute(code="return true;")
  232.         except ApiError:
  233.             return False
  234.  
  235.     def upload(self, upload_url, files):
  236.         """
  237.        Загружает файлы на сервер. Процесс расписан здесь
  238.        <https://vk.com/dev/upload_files>
  239.  
  240.        :param upload_url: Адрес сервера для загрузки файлов, который получают,
  241.            например, вызовом Client.api.docs.getWallUploadServer() и т.п.
  242.        :type upload_url: str
  243.  
  244.        :param files: Словарь где ключами выступают имена полей. Значения могут
  245.            быть строками (путь до файла либо ссылка на него), файловыми
  246.            объектами либо списками/кортежами,где первый элемент - имя файла,
  247.            а второй - его содержимое (бинарное либо текстовое)
  248.        :type files: dict
  249.  
  250.        :rtype: ObjectDict
  251.        """
  252.         boundary = uuid.uuid4().hex
  253.         buf = io.BytesIO()
  254.         for name, obj in files.items():
  255.             if isinstance(obj, str):
  256.                 # Загружаем по URL
  257.                 if HTTP_RE.match(obj):
  258.                     content = self.get_content(obj)
  259.                     # На некоторых ресурсах есть ссылки вида:
  260.                     # /image.jpg?w=x&h=y
  261.                     filename = urllib.parse.urlparse(obj).path
  262.                     filename = os.path.basename(filename)
  263.                 # Читаем файл
  264.                 else:
  265.                     with open(obj, 'rb') as fp:
  266.                         content = fp.read()
  267.                     filename = os.path.basename(obj)
  268.             else:
  269.                 # {'photo': open("path/to/photo.jpg", 'rb')}
  270.                 if isinstance(obj, io.IOBase):
  271.                     filename = os.path.basename(obj.name)
  272.                     content = obj.read()
  273.                 # {'photo': ('photo.jpg', b'<binary data>')}
  274.                 elif isinstance(obj, (list, tuple)):
  275.                     # нам нужны только два первых элемента остальные будем
  276.                     # игнорировать
  277.                     filename, content, *_ = obj
  278.                 else:
  279.                     raise ValueError("Unexcepted: {!r}".format(obj))
  280.                 if not isinstance(content, (bytearray, bytes)):
  281.                     content = str(content).encode('utf-8')
  282.             # Поле Content-Type передавать необязательно,  на сервере Вконтакте
  283.             # проверяется расширение файла из поля filename и его содержиме,
  284.             # читаются первые n байтов, и ищутся сигнатуры
  285.             header = ('--{}\r\n'
  286.                       'Content-Disposition: form-data; name="{}"; '
  287.                       'filename="{}"\r\n\r\n')
  288.             # У requests кстати баг, который заставил отказаться от его
  289.             # использования: он неправильно кодирует Non-ASCII поля в
  290.             # заголовках, поэтому файл "Мой файл.txt" в документах будет
  291.             # отображаться как "...???.txt"
  292.             header = header.format(boundary, name, filename)
  293.             buf.write(bytes(header, 'utf-8'))
  294.             buf.write(content)
  295.             buf.write(b'\r\n')
  296.         buf.write(bytes('--{}--\r\n'.format(boundary), 'utf-8'))
  297.         data = buf.getvalue()
  298.         headers = {'Content-Type': 'multipart/form-data; boundary=' + boundary}
  299.         response = self.request(upload_url, data, headers)
  300.         if 'error' in response:
  301.             raise UploadError(response.error)
  302.         return response
  303.  
  304.     def request(self, url, data=None, headers={}):
  305.         # Для работы с API Вконтакте другие методы кроме POST в принципе не
  306.         # нужны. Ему параметры можно даже через куки передавать (по крайней
  307.         # мере так можно было очень давно).
  308.         if isinstance(data, dict):
  309.             data = urllib.parse.urlencode(data).encode('ascii')
  310.         try:
  311.             content = self.raw_request(url, data, headers).read()
  312.         except urllib.request.HTTPError as e:
  313.             self.logger.debug(e)
  314.             content = e.read()
  315.         return json.loads(str(content, "utf-8"), object_hook=ObjectDict)
  316.  
  317.     def get_content(self, url):
  318.         return self.raw_request(url).read()
  319.  
  320.     def raw_request(self, url, body=None, headers={}):
  321.         start_time = time.time()
  322.         self.logger.debug("URL: %s", url)
  323.         request = urllib.request.Request(url, body, headers)
  324.         attempts = 0
  325.         while True:
  326.             try:
  327.                 response = self.opener.open(request, timeout=self.timeout)
  328.                 request_time = time.time() - start_time
  329.                 self.logger.debug("Request Time: %dms",
  330.                                   request_time * 1000)
  331.                 return response
  332.             except urllib.request.URLError as e:
  333.                 # Если ошибка не является ошибкой таймаута, то бросаем
  334.                 # исключение
  335.                 if not isinstance(e.reason, socket.timeout):
  336.                     raise
  337.                 # Если значение равно -1, то запросы будут отправляться пока
  338.                 # не будет получен ответ
  339.                 if self.tries != - 1:
  340.                     # Бросаем ошибку таймаута
  341.                     if self.tries <= 1:
  342.                         raise
  343.                     attempts += 1
  344.                     if attempts == self.tries:
  345.                         raise ClientError(
  346.                             "Maximun tries ({}) exceeded for URL: {}".format(
  347.                                 self.tries, url))
  348.                 self.logger.debug("Retry: %s", url)
  349.  
  350.  
  351. class _Api:
  352.     def __init__(self, client):
  353.         self._client = client
  354.  
  355.     def __getattr__(self, name):
  356.         if _ApiMethod.METHOD_RE.match(name):
  357.             return _ApiMethod(self._client, name)
  358.         raise AttributeError(name)
  359.  
  360.  
  361. class _ApiMethod:
  362.     METHOD_RE = re.compile("[a-z]+([A-Z][a-z]+)*$")
  363.  
  364.     def __init__(self, client, method):
  365.         self._client = client
  366.         self._method = method
  367.  
  368.     def __getattr__(self, name):
  369.         if self.METHOD_RE.match(name):
  370.             return _ApiMethod(self._client, '.'.join([self._method, name]))
  371.         raise AttributeError(name)
  372.  
  373.     def __call__(self, *args, **kwargs):
  374.         # Если имя именованного параметра совпадает с ключевым словом, то
  375.         # добавляем перед именем подчеркивание:
  376.         params = {k[1:] if len(k) > 1 and k.startswith('_')
  377.                   else k: v for k, v in kwargs.items()}
  378.         if len(args):
  379.             # Первым аргументом является словарь
  380.             d = dict(args[0])
  381.             # Если переданы именованные параметры, то обновляем словарь
  382.             d.update(params)
  383.             params = d
  384.         return self._client.api_request(self._method, params)
  385.  
  386.  
  387. class ClientError(Exception):
  388.     pass
  389.  
  390.  
  391. class ApiError(ClientError):
  392.     """Подробнее про ошибки API можно прочитать по ссылке
  393.    <https://vk.com/dev/errors>
  394.    """
  395.     UNKNOWN_ERROR_OCCURRED = 1
  396.     UNKNOWN_METHOD_PASSED = 3
  397.     INCORRECT_SIGNATURE = 4
  398.     USER_AUTHORIZATION_FAILED = 5
  399.     TOO_MANY_REQUESTS_PER_SECOND = 6
  400.     INVALID_REQUEST = 8
  401.     FLOOD_CONTROL = 9
  402.     INTERNAL_SERVER_ERROR = 10
  403.     CAPTCHA_NEEDED = 14
  404.     ACCESS_DENIED = 15
  405.     HTTP_AUTHORIZATION_FAILED = 16
  406.     VALIDATION_REQUIRED = 17
  407.     THIS_METHOD_WAS_DISABLED = 23
  408.     CONFIRMATION_REQUIRED = 24
  409.     INVALID_APPLICATION_API_ID = 101
  410.     INVALID_USER_ID = 113
  411.     INVALID_TIMESTAMP = 150
  412.     ACCESS_TO_ALBUM_DENIED = 200
  413.     ACCESS_TO_AUDIO_DENIED = 201
  414.     ACCESS_TO_GROUP_DENIED = 203
  415.     THIS_ALBUM_IS_FULL = 300
  416.     SOME_ADS_ERROR_OCCURED = 603
  417.  
  418.     def __init__(self, response):
  419.         self.error_code = response.error.error_code
  420.         self.error_msg = response.error.error_msg
  421.         self.captcha_img = response.error.get('captcha_img')
  422.         self.captcha_sid = response.error.get('captcha_sid')
  423.         self.redirect_uri = response.error.get('redirect_uri')
  424.         params = {v['key']: v['value'] for v in response.error.request_params}
  425.         self.method = params.pop('method')
  426.         # Что это?
  427.         # self.oauth = params.pop('oauth')
  428.         self.params = params
  429.  
  430.     def __str__(self):
  431.         return (
  432.             "Error Code: {}, Message: {!r}. An error occured while calling "
  433.             "method {!r} with parameters: {}"
  434.         ).format(self.error_code, self.error_msg, self.method, self.params)
  435.  
  436.  
  437. class UploadError(ClientError):
  438.     pass
  439.  
  440.  
  441. if __name__ == '__main__':
  442.     logging.basicConfig(level=logging.DEBUG)
  443.     c = Client()
  444.     r = c.api.users.get(user_id=1)
  445.     print("{u.first_name} {u.last_name}".format(u=r[0]))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement