Advertisement
stuppid_bot

Untitled

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