Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mrDoctorWho/vk4xmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormrDoctorWho <mrdoctorwho@gmail.com>2015-04-21 11:41:49 +0300
committermrDoctorWho <mrdoctorwho@gmail.com>2015-04-21 11:41:49 +0300
commit06e919ce0eaece47634ef83d1615f4b24e2eb9aa (patch)
treef3c40a3a84ce55817342c77221801e658bcea110 /library/vkapi.py
parent57e4db68cd28d7d6400e93016ef9755bdefcb484 (diff)
Okay, I'm doing this again.
The most hard thing was rewriting the core and modules to follow PEP8 guidelines. Still not complete. And will not be completed. Changed config defaults (especially port and pidFile fields) New config option: RUN_AS to run the transport as a specified user (root access required, I think). Plugin variables removed as well. Parts of the code in core was split into the modules: defaults (for default variables), settings (for user/transport settings), longpoll (for longpoll, obviously), utils (for cross-module used functions) Removed unneeded API-call in avatar_hash plugin. Now we hash all user's friends avatars Removed plugin-specific variables from the core. Fixed watcher messages The code of vkapi module (APIBinding class) was split into a PasswordLogin and APIBinding classes. New error handling by vkapi: if it receives “too fast” error, it increases waiting timeout for 0.05 sec until it'll be fine Brand new API for modules In conclusion, I can say that the transport has gone through a huge code cleanup process. I cleaned up especially the core, library and modules. Well, you can see the diff anyways. Meet a brand new VK4XMPP v3.0 And I really hope it will be the last commit. Really, it's enough, I'm done.
Diffstat (limited to 'library/vkapi.py')
-rw-r--r--library/vkapi.py280
1 files changed, 152 insertions, 128 deletions
diff --git a/library/vkapi.py b/library/vkapi.py
index c7d73ca..4af0bb3 100644
--- a/library/vkapi.py
+++ b/library/vkapi.py
@@ -1,10 +1,18 @@
# coding: utf-8
# © simpleApps, 2013 — 2015.
+"""
+Manages VK API requests
+Provides password login and direct VK API calls
+Designed for huge number of clients (per ip)
+Which is why it has request retries
+"""
+
+__author__ = "mrDoctorWho <mrdoctorwho@gmail.com>"
+
import cookielib
import httplib
import logging
-import mimetools
import re
import socket
import ssl
@@ -17,14 +25,25 @@ import webtools
SOCKET_TIMEOUT = 30
REQUEST_RETRIES = 6
+# VK APP ID
+APP_ID = 3789129
+# VK APP scope
+SCOPE = 69638
+
socket.setdefaulttimeout(SOCKET_TIMEOUT)
logger = logging.getLogger("vk4xmpp")
token_exp = re.compile("(([\da-f]+){11,})", re.IGNORECASE)
+ERRORS = (httplib.BadStatusLine,
+ urllib2.URLError,
+ socket.gaierror,
+ socket.timeout,
+ socket.error,
+ ssl.SSLError)
-## Trying to use faster library usjon instead of simplejson
+# Trying to use faster library usjon instead of simplejson
try:
import ujson as json
logger.debug("vkapi: using ujson instead of simplejson")
@@ -52,7 +71,8 @@ def attemptTo(maxRetries, resultType, *errors):
data = func(*args, **kwargs)
except errors as exc:
retries += 1
- logger.warning("vkapi: trying to execute \"%s\" in #%d time" % (func.func_name, retries))
+ logger.warning("vkapi: trying to execute \"%s\" in #%d time",
+ func.func_name, retries)
time.sleep(0.2)
else:
break
@@ -60,7 +80,7 @@ def attemptTo(maxRetries, resultType, *errors):
if hasattr(exc, "errno") and exc.errno == 101:
raise NetworkNotFound()
data = resultType()
- logger.warning("vkapi: Error %s occurred on executing %s" % (exc, func))
+ logger.warning("vkapi: Error %s occurred on executing %s", exc, func)
return data
wrapper.__name__ = func.__name__
@@ -71,8 +91,10 @@ def attemptTo(maxRetries, resultType, *errors):
class AsyncHTTPRequest(httplib.HTTPConnection):
"""
- Provides easy method to make asynchronous http requests and getting socket object from it
+ A method to make asynchronous http request
+ Provides a way to get a socket object to use in select()
"""
+
def __init__(self, url, data=None, headers=(), timeout=SOCKET_TIMEOUT):
host = urllib.splithost(urllib.splittype(url)[1])[0]
httplib.HTTPConnection.__init__(self, host, timeout=timeout)
@@ -80,9 +102,11 @@ class AsyncHTTPRequest(httplib.HTTPConnection):
self.data = data
self.headers = headers or {}
+ @attemptTo(REQUEST_RETRIES, None, *ERRORS)
def open(self):
self.connect()
- self.request(("POST" if self.data else "GET"), self.url, self.data, self.headers)
+ self.request(("POST" if self.data else "GET"), self.url, self.data,
+ self.headers)
return self
def read(self):
@@ -95,51 +119,58 @@ class AsyncHTTPRequest(httplib.HTTPConnection):
def __exit__(self, *args):
self.close()
+ @classmethod
+ def getOpener(cls, url, query={}):
+ """
+ Opens a connection to url and returns AsyncHTTPRequest() object
+ """
+ if query:
+ url += "?%s" % urllib.urlencode(query)
+ return AsyncHTTPRequest(url).open()
+
class RequestProcessor(object):
"""
- Processes base requests: POST (application/x-www-form-urlencoded and multipart/form-data) and GET.
+ Processes base requests:
+ POST (application/x-www-form-urlencoded and multipart/form-data)
+ GET
"""
headers = {"User-agent": "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:21.0)"
" Gecko/20130309 Firefox/21.0",
"Accept-Language": "ru-RU, utf-8"}
- boundary = mimetools.choose_boundary()
-
- def __init__(self):
- self.cookieJar = cookielib.CookieJar()
- cookieProcessor = urllib2.HTTPCookieProcessor(self.cookieJar)
- self.open = urllib2.build_opener(cookieProcessor).open
-
- def getCookie(self, name):
- """
- Gets cookie from cookieJar
- """
- for cookie in self.cookieJar:
- if cookie.name == name:
- return cookie.value
+ boundary = "github.com/mrDoctorWho/vk4xmpp"
+
+ def __init__(self, cook=False):
+ if cook:
+ cookieJar = cookielib.CookieJar()
+ cookieProcessor = urllib2.HTTPCookieProcessor(cookieJar)
+ self.open = urllib2.build_opener(cookieProcessor).open
+ self.getCookie = lambda name: [c.value for c in cookieJar if c.name == name]
+ else:
+ self.open = urllib2.build_opener().open
def multipart(self, key, name, ctype, data):
"""
Makes multipart/form-data encoding
Parameters:
- key: a form key (is there a form?)
- name: file name
- ctype: Content-Type
- data: just data you want to send
+ key: form key (is there a form?)
+ name: filename
+ ctype: content type
+ data: the data you want to send
"""
- start = ["--" + self.boundary, "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"" % (key, name), \
- "Content-Type: %s" % ctype, "", ""] ## We already have content type so maybe we shouldn't detect it
- end = ["", "--" + self.boundary + "--", ""]
- start = "\n".join(start)
- end = "\n".join(end)
- data = start + data + end
- return data
+ boundary = "--%s" % self.boundary
+ disposition = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\""\
+ % (key, name)
+ ctype = "Content-Type: %s" % ctype
+ header = "%(boundary)s\n%(disposition)s\n%(ctype)s\n\n" % vars()
+ footer = "\n%s--\n" % boundary
+ return header + data + footer
def request(self, url, data=None, headers=None, urlencode=True):
"""
Makes a http(s) request
Parameters:
- url: a request url
+ url: a request url
data: a request data
headers: a request headers (if not set, self.headers will be used)
urlencode: urlencode flag
@@ -153,7 +184,7 @@ class RequestProcessor(object):
request = urllib2.Request(url, data, headers)
return request
- @attemptTo(REQUEST_RETRIES, tuple, urllib2.URLError, ssl.SSLError, httplib.BadStatusLine)
+ @attemptTo(REQUEST_RETRIES, tuple, *ERRORS)
def post(self, url, data="", urlencode=True):
"""
POST request
@@ -162,7 +193,7 @@ class RequestProcessor(object):
body = resp.read()
return (body, resp)
- @attemptTo(REQUEST_RETRIES, tuple, urllib2.URLError, ssl.SSLError, httplib.BadStatusLine)
+ @attemptTo(REQUEST_RETRIES, tuple, *ERRORS)
def get(self, url, query={}):
"""
GET request
@@ -173,102 +204,66 @@ class RequestProcessor(object):
body = resp.read()
return (body, resp)
-## todo: move getOpener the hell out of here
- @attemptTo(REQUEST_RETRIES, None, socket.gaierror, socket.timeout, socket.error)
- def getOpener(self, url, query={}):
- """
- Opens a connection to url and returns AsyncHTTPRequest() object
- """
- if query:
- url += "?%s" % urllib.urlencode(query)
- return AsyncHTTPRequest(url).open()
-
-class APIBinding:
+class PasswordLogin(object):
"""
- Provides simple VK API binding
- Translates VK errors to python exceptions
- Allows to make a password authorization
+ Provides a way to log-in by a password
"""
- def __init__(self, number=None, password=None, token=None, app_id=3789129,
- scope=69638, debug=None):
- self.password = password
+ def __init__(self, number, password):
self.number = number
- self.token = token
- self.app_id = app_id
- self.scope = scope
-
- self.sid = None
- self.captcha = {}
- self.last = []
- self.lastMethod = ()
-
- self.RIP = RequestProcessor()
- self.debug = debug or []
+ self.password = password
+ self.RIP = RequestProcessor(cook=True)
- def loginByPassword(self):
+ def login(self):
"""
Logging in using password
"""
url = "https://login.vk.com/"
values = {"act": "login",
- "utf8": "1",
- "email": self.number,
- "pass": self.password}
+ "utf8": "1",
+ "email": self.number,
+ "pass": self.password}
body, response = self.RIP.post(url, values)
- remixSID = self.RIP.getCookie("remixsid")
- if remixSID:
- self.sid = remixSID
-
- elif "sid=" in response.url:
+ if "sid=" in response.url:
+ logger.error("vkapi: PasswordLogin ran into captcha! (number: %s)",
+ self.number)
raise AuthError("Captcha!")
- else:
+
+ if not self.RIP.getCookie("remixsid"):
raise AuthError("Invalid password")
if "security_check" in response.url:
- # This code should be rewritten
+ logger.warning("vkapi: PasswordLogin ran into a security check (number: %s)",
+ self.number)
hash = re.search("security_check.*?hash: '(.*?)'\};", body).group(0)
if not self.number[0] == "+":
self.number = "+" + self.number
- code = self.number[2:-2]
+ code = self.number[2:-2] # valid for Russia only. Unfrotunately.
values = {"act": "security_check",
- "al": "1",
- "al_page": "3",
- "code": code,
- "hash": hash,
- "to": ""}
+ "al": "1",
+ "al_page": "3",
+ "code": code,
+ "hash": hash,
+ "to": ""}
post = self.RIP.post("https://vk.com/login.php", values)
body, response = post
if response and not body.split("<!>")[4] == "4":
raise AuthError("Incorrect number")
+ return self.confirm()
- def checkSid(self):
- """
- Checks sid to set the logged-in flag
+ def confirm(self):
"""
- if self.sid:
- url = "https://vk.com/feed2.php"
- get = self.RIP.get(url)
- if get:
- body, response = get
- if body and response:
- data = json.loads(body)
- if data["user"]["id"] != -1:
- return data
-
- def confirmThisApp(self):
- """
- Confirms your application and receives the token
+ Confirms the application and receives the token
"""
url = "https://oauth.vk.com/authorize/"
values = {"display": "mobile",
- "scope": self.scope,
- "client_id": self.app_id,
- "response_type": "token",
- "redirect_uri": "https://oauth.vk.com/blank.html"}
+ "scope": SCOPE,
+ "client_id": APP_ID,
+ "response_type": "token",
+ "redirect_uri": "https://oauth.vk.com/blank.html"}
token = None
body, response = self.RIP.get(url, values)
@@ -276,84 +271,109 @@ class APIBinding:
if "access_token" in response.url:
token = token_exp.search(response.url).group(0)
else:
- postTarget = webtools.getTagArg("form method=\"post\"", "action", body, "form")
+ # What is it?
+ postTarget = webtools.getTagArg("form method=\"post\"", "action",
+ body, "form")
if postTarget:
body, response = self.RIP.post(postTarget)
token = token_exp.search(response.url).group(0)
else:
- raise AuthError("Couldn't execute confirmThisApp()!")
+ raise AuthError("Couldn't confirm the application!")
+ return token
+
+
+class APIBinding(object):
+ """
+ Provides simple VK API binding
+ Translates VK errors to python exceptions
+ Allows to make a password authorization
+ """
+ def __init__(self, token, debug=[]):
self.token = token
+ self.debug = debug
+ self.captcha = {}
+ self.last = []
+ self.lastMethod = ()
+
+ self.timeout = 1.00
+
+ self.RIP = RequestProcessor()
def method(self, method, values=None, nodecode=False):
"""
Issues the VK method
Parameters:
method: vk method
- values: method parameters (no captcha_{sid,key}, access_token or v parameters needed)
- nodecode: decode flag
+ values: method parameters
"""
- values = values or {}
url = "https://api.vk.com/method/%s" % method
+ values = values or {}
values["access_token"] = self.token
values["v"] = "3.0"
- if self.captcha and self.captcha.has_key("key"):
+ if "key" in self.captcha:
values["captcha_sid"] = self.captcha["sid"]
values["captcha_key"] = self.captcha["key"]
self.captcha = {}
+
self.lastMethod = (method, values)
self.last.append(time.time())
if len(self.last) > 2:
- if (self.last.pop() - self.last.pop(0)) <= 1.25:
- time.sleep(0.37)
+ if (self.last.pop() - self.last.pop(0)) <= self.timeout:
+ time.sleep(self.timeout / 3.0)
if method in self.debug or self.debug == "all":
start = time.time()
- print "issuing method %s with values %s in thread: %s" % (method, str(values), threading.currentThread().name)
+ print "issuing method %s with values %s in thread: %s" % (method,
+ str(values), threading.currentThread().name)
response = self.RIP.post(url, values)
- if response and not nodecode:
+ if response:
body, response = response
if body:
try:
body = json.loads(body)
except ValueError:
return {}
-# Debug:
+
if method in self.debug or self.debug == "all":
- print "response for method %s: %s in thread: %s (%0.2fs)" % (method, str(body), threading.currentThread().name, (time.time() - start))
+ print "response for method %s: %s in thread: %s (%0.2fs)" % (method,
+ str(body), threading.currentThread().name, (time.time() - start))
if "response" in body:
return body["response"] or {}
- ## according to vk.com/dev/errors
+ # according to vk.com/dev/errors
elif "error" in body:
error = body["error"]
eCode = error["error_code"]
eMsg = error.get("error_msg", "")
- logger.error("vkapi: error occured on executing method (%(method)s, code: %(eCode)s, msg: %(eMsg)s)" % vars())
-
- if eCode == 5: # auth failed / invalid session(?)
- raise VkApiError(eMsg)
- elif eCode == 6: # too fast
- time.sleep(1.25)
- return self.method(method, values)
- elif eCode == 7: # not allowed
+ logger.error("vkapi: error occured on executing method"
+ " (%(method)s, code: %(eCode)s, msg: %(eMsg)s)" % vars())
+
+ if eCode == 7: # not allowed
raise NotAllowed(eMsg)
- elif eCode == 10: # internal server error
+ elif eCode == 10: # internal server error
raise InternalServerError(eMsg)
- elif eCode == 13: # runtime error
+ elif eCode == 13: # runtime error
raise RuntimeError(eMsg)
- elif eCode == 14: # captcha
+ elif eCode == 14: # captcha
if "captcha_sid" in error:
self.captcha = {"sid": error["captcha_sid"], "img": error["captcha_img"]}
raise CaptchaNeeded()
elif eCode == 15:
raise AccessDenied(eMsg)
- elif eCode in (1, 9, 100): ## 1 is an unknown error / 9 is flood control / 100 is wrong method or parameters loss
+ # 1 - unknown error / 100 - wrong method or parameters loss
+ elif eCode in (1, 6, 9, 100):
+ if eCode in (6, 9): # 6 - too fast / 9 - flood control
+ self.timeout += 0.05
+ logger.warning("vkapi: got code 9, increasing timeout to %0.2f",
+ self.timeout)
+ time.sleep(self.timeout)
+ return self.method(method, values)
return {"error": eCode}
- raise VkApiError(body["error"])
+ raise VkApiError(eMsg)
def retry(self):
"""
@@ -378,12 +398,14 @@ class NetworkNotFound(Exception):
"""
pass
+
class LongPollError(Exception):
"""
Should be raised when longpoll exception occurred
"""
pass
+
class VkApiError(Exception):
"""
Base VK API Error
@@ -394,7 +416,8 @@ class VkApiError(Exception):
class AuthError(VkApiError):
"""
Happens when user is trying to login using password
- And there's one of possible errors: captcha, invalid password and wrong phone
+ And there's one of possible errors: captcha,
+ invalid password and wrong phone
"""
pass
@@ -416,7 +439,7 @@ class CaptchaNeeded(VkApiError):
class TokenError(VkApiError):
"""
- Will be raised when happens error with code 5 and 3 retries to make request are failed
+ Will be raised if Token Error occurred
"""
pass
@@ -428,6 +451,7 @@ class NotAllowed(VkApiError):
"""
pass
+
class AccessDenied(VkApiError):
"""
This one should be ignored as well.