From 0142c8ff41effc29576c5f83153975e4ab50acd5 Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 14:32:45 +0100 Subject: [PATCH 01/19] getThreadInfo fix. Stripping fbid: from string --- fbchat/client.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index c82561a..616a80e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -789,13 +789,24 @@ class Client(object): except requests.exceptions.Timeout: pass - def getUserInfo(self,*user_ids): + def getUserInfo(self, *user_ids): """Get user info from id. Unordered. :param user_ids: one or more user id(s) to query """ - data = {"ids[{}]".format(i):user_id for i,user_id in enumerate(user_ids)} + def fbidStrip(_fbid): + # Stripping of `fbid:` from author_id + if type(_fbid) == int: + return _fbid + + if type(_fbid) == str and 'fbid:' in _fbid: + return int(_fbid[5:]) + + user_ids = [fbidStrip(uid) for uid in user_ids] + + + data = {"ids[{}]".format(i):uid for i,uid in enumerate(user_ids)} r = self._post(UserInfoURL, data) info = get_json(r.text) full_data= [details for profile,details in info['payload']['profiles'].items()] From d2d77501e407c2d1e33701a5e58a1da55c748715 Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 14:37:00 +0100 Subject: [PATCH 02/19] getThreadInfo fix: refactored `end` parameter, using instead length, because of behaviour --- fbchat/client.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 616a80e..0e75cad 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -512,17 +512,16 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(r._content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, userID, start, end=None, thread_type='user'): + def getThreadInfo(self, userID, start, length=20, thread_type='user'): """Get the info of one Thread :param userID: ID of the user you want the messages from :param start: the start index of a thread - :param end: (optional) the last index of a thread + :param length: (optional) number of retrieved messages from start :param thread_type: (optional) change from 'user' for group threads """ - - if not end: end = start + 20 - if end <= start: end = start + end + + assert(length > 0, 'length must be positive integer, got %d'%length) data = {} if thread_type == 'user': @@ -531,7 +530,7 @@ class Client(object): key = 'thread_fbids' data['messages[{}][{}][offset]'.format(key, userID)] = start - data['messages[{}][{}][limit]'.format(key, userID)] = end + data['messages[{}][{}][limit]'.format(key, userID)] = length data['messages[{}][{}][timestamp]'.format(key, userID)] = now() r = self._post(MessagesURL, query=data) From f4cac4b8dbd6d1bb310d0240edb7fa585fbede2e Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 14:39:10 +0100 Subject: [PATCH 03/19] getThreadInfo fix: removed parens from assertion --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index 0e75cad..b8f8c80 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -521,7 +521,7 @@ class Client(object): :param thread_type: (optional) change from 'user' for group threads """ - assert(length > 0, 'length must be positive integer, got %d'%length) + assert length > 0, 'length must be positive integer, got %d'%length data = {} if thread_type == 'user': From 2580cf557744d7320bef3bf09352cdfc37178b63 Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 14:54:04 +0100 Subject: [PATCH 04/19] getThreadInfo fix: start is deprecated, method always returns with 0 offset --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index b8f8c80..a7b62d7 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -521,7 +521,7 @@ class Client(object): :param thread_type: (optional) change from 'user' for group threads """ - assert length > 0, 'length must be positive integer, got %d'%length + assert length > 0, 'length must be positive integer, got %d' % length data = {} if thread_type == 'user': From 3ea27ea49a4652c19e99cca551f594779892bbfc Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 15:55:03 +0100 Subject: [PATCH 05/19] getThreadList fix: `end` is ignored by the querry --- fbchat/client.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index a7b62d7..40f444e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -512,25 +512,27 @@ class Client(object): # Strip the start and parse out the returned image_id return json.loads(r._content[9:])['payload']['metadata'][0]['image_id'] - def getThreadInfo(self, userID, start, length=20, thread_type='user'): + def getThreadInfo(self, userID, last_n=20, start=None, thread_type='user'): """Get the info of one Thread :param userID: ID of the user you want the messages from - :param start: the start index of a thread - :param length: (optional) number of retrieved messages from start + :param last_n: (optional) number of retrieved messages from start + :param start: (optional) the start index of a thread (Deprecated) :param thread_type: (optional) change from 'user' for group threads """ - assert length > 0, 'length must be positive integer, got %d' % length - + assert last_n > 0, 'length must be positive integer, got %d' % last_n + assert start is None, '`start` is deprecated, always 0 offset querry is returned' data = {} if thread_type == 'user': key = 'user_ids' else: key = 'thread_fbids' - - data['messages[{}][{}][offset]'.format(key, userID)] = start - data['messages[{}][{}][limit]'.format(key, userID)] = length + assert + # deprecated + # `start` doesn't matter, always returns from the last + # data['messages[{}][{}][offset]'.format(key, userID)] = start + data['messages[{}][{}][limit]'.format(key, userID)] = last_n data['messages[{}][{}][timestamp]'.format(key, userID)] = now() r = self._post(MessagesURL, query=data) @@ -553,10 +555,14 @@ class Client(object): :param start: the start index of a thread :param end: (optional) the last index of a thread """ - - if not end: end = start + 20 - if end <= start: end = start + end - + + # deprecated + # end does not limit the length of the returned threads + # if not end: end = start + 20 + # if end <= start: end = start + end + + assert end is None, 'end is deprecated' + timestamp = now() date = datetime.now() data = { From 3f3929fcdec89c6c34577b63af086c4e28879f54 Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 15:58:47 +0100 Subject: [PATCH 06/19] getThreadList fix: `end` is ignored by the querry --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index 40f444e..351894b 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -528,7 +528,7 @@ class Client(object): key = 'user_ids' else: key = 'thread_fbids' - assert + # deprecated # `start` doesn't matter, always returns from the last # data['messages[{}][{}][offset]'.format(key, userID)] = start From 43fbe4c6555de0e71138dccd1108b8a68fafcb5f Mon Sep 17 00:00:00 2001 From: botcs Date: Tue, 7 Mar 2017 16:09:00 +0100 Subject: [PATCH 07/19] assertion added for getThreadList end --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index 351894b..63ae8f0 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -561,7 +561,7 @@ class Client(object): # if not end: end = start + 20 # if end <= start: end = start + end - assert end is None, 'end is deprecated' + assert end is None, '`end` is deprecated, always return last 20 threads' timestamp = now() date = datetime.now() From 78928fda7733aaf7d5fc763434e2f6ed61546327 Mon Sep 17 00:00:00 2001 From: botcs Date: Wed, 8 Mar 2017 10:28:13 +0100 Subject: [PATCH 08/19] fix None issue --- fbchat/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index 63ae8f0..c21190c 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -532,6 +532,7 @@ class Client(object): # deprecated # `start` doesn't matter, always returns from the last # data['messages[{}][{}][offset]'.format(key, userID)] = start + data['messages[{}][{}][offset]'.format(key, userID)] = 0 data['messages[{}][{}][limit]'.format(key, userID)] = last_n data['messages[{}][{}][timestamp]'.format(key, userID)] = now() @@ -568,7 +569,7 @@ class Client(object): data = { 'client' : self.client, 'inbox[offset]' : start, - 'inbox[limit]' : end, + 'inbox[limit]' : 19, } r = self._post(ThreadsURL, data) From eb1ff3ffaad15539581e0ba862260637771d7278 Mon Sep 17 00:00:00 2001 From: botcs Date: Wed, 8 Mar 2017 10:48:57 +0100 Subject: [PATCH 09/19] !!! getThreadList length MAX 20 --- fbchat/client.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index c21190c..195d7dd 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -550,26 +550,21 @@ class Client(object): return list(reversed(messages)) - def getThreadList(self, start, end=None): + def getThreadList(self, start, length=20): """Get thread list of your facebook account. :param start: the start index of a thread :param end: (optional) the last index of a thread """ - # deprecated - # end does not limit the length of the returned threads - # if not end: end = start + 20 - # if end <= start: end = start + end - - assert end is None, '`end` is deprecated, always return last 20 threads' + assert length < 21, '`length` is deprecated, max. last 20 threads are returned' timestamp = now() date = datetime.now() data = { 'client' : self.client, 'inbox[offset]' : start, - 'inbox[limit]' : 19, + 'inbox[limit]' : length, } r = self._post(ThreadsURL, data) From 462c21d2ef1d00b2266a3c96044d74e87c4dcd07 Mon Sep 17 00:00:00 2001 From: Taehoon Kim Date: Wed, 29 Mar 2017 13:00:21 +0900 Subject: [PATCH 10/19] version up thanks to the PR #115 --- fbchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 8ca376f..0b0f46c 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from .client import * __copyright__ = 'Copyright 2015 by Taehoon Kim' -__version__ = '0.7.1' +__version__ = '0.8.0' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan' __email__ = 'carpedm20@gmail.com' From f66e9c98f3dace4935db11400edea61cee585015 Mon Sep 17 00:00:00 2001 From: Thiago Date: Mon, 3 Apr 2017 18:34:21 -0300 Subject: [PATCH 11/19] fixed a bug that prevented fbchat from returning the list of all users in some accounts --- fbchat/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 195d7dd..e7a5952 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -356,8 +356,12 @@ class Client(object): payload = j['payload'] users = [] - for k in payload.keys(): - user = self._adapt_user_in_chat_to_user_model(payload[k]) + for k in payload.keys(): + try: + user = self._adapt_user_in_chat_to_user_model(payload[k]) + except KeyError: + continue + users.append(User(user)) return users From d7f2bb3e8b1e64226b7fc4213fe30013da522a77 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:05:50 +1000 Subject: [PATCH 12/19] Adding 2FA Support Rudamentary 2FA support --- fbchat/client.py | 68 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 195d7dd..62a018e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -46,6 +46,7 @@ RemoveUserURL="https://www.facebook.com/chat/remove_participants/" LogoutURL ="https://www.facebook.com/logout.php" AllUsersURL ="https://www.facebook.com/chat/user_info_all" SaveDeviceURL="https://m.facebook.com/login/save-device/cancel/" +CheckpointURL="https://m.facebook.com/login/checkpoint/" facebookEncoding = 'UTF-8' # Log settings @@ -228,17 +229,78 @@ class Client(object): data['login'] = 'Log In' r = self._cleanPost(LoginURL, data) - - # Sometimes Facebook tries to show the user a "Save Device" dialog + log.info(r.text) + + if 'checkpoint' in r.url: + r = self._2FA(r) + if 'save-device' in r.url: r = self._cleanGet(SaveDeviceURL) + log.info(r.url) + if 'home' in r.url: self._post_login() return True else: return False + + def _2FA(self,r): + soup = bs(r.text, "lxml") + data = dict() + s = raw_input('Please enter your 2FA code --> ') + data['approvals_code'] = s + data['fb_dtsg'] = soup.find("input", {'name':'fb_dtsg'})['value'] + data['nh'] = soup.find("input", {'name':'nh'})['value'] + data['submit[Submit Code]'] = 'Submit Code' + data['codes_submitted'] = 0 + log.info('Submitting 2FA code') + r = self._cleanPost(CheckpointURL, data) + + log.info(r.url) + if 'home' in r.url: + return r + + del(data['approvals_code']) + del(data['submit[Submit Code]']) + del(data['codes_submitted']) + + data['name_action_selected'] = 'save_device' + data['submit[Continue]'] = 'Continue' + log.info('Saving browser') #At this stage, we have dtsg, nh, name_action_selected, submit[Continue] + r = self._cleanPost(CheckpointURL, data) + + log.info(r.url) + if 'home' in r.url: + return r + + del(data['name_action_selected']) + log.info(data.values()) #At this stage, we have dtsg, nh, submit[Continue] + r = self._cleanPost(CheckpointURL, data) + + log.info(r.url) + if 'home' in r.url: + return r + + del(data['submit[Continue]']) + data['submit[This was me]'] = 'This Was Me' + log.info('Verifying login attempt') #At this stage, we have dtsg, nh, submit[This was me] + r = self._cleanPost(CheckpointURL, data) + + log.info(r.url) + if 'home' in r.url: + return r + + del(data['submit[This was me]']) + data['submit[Continue]'] = 'Continue' + data['name_action_selected'] = 'save_device' + log.info('Saving device again') #At this stage, we have dtsg, nh, submit[Continue], name_action_selected + r = self._cleanPost(CheckpointURL, data) + + log.info(r.url) + return r + def saveSession(self, sessionfile): """Dumps the session cookies to (sessionfile). WILL OVERWRITE ANY EXISTING FILE @@ -280,7 +342,7 @@ class Client(object): for i in range(1,max_retries+1): if not self._login(): log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i<5,''))) - time.sleep(1) + time.sleep(1000) continue else: log.info("Login successful") From de6880d933c1e743ada173aa48fd8af026c617a0 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:08:07 +1000 Subject: [PATCH 13/19] Update client.py --- fbchat/client.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 62a018e..33c9e7e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -229,7 +229,7 @@ class Client(object): data['login'] = 'Log In' r = self._cleanPost(LoginURL, data) - log.info(r.text) + if 'checkpoint' in r.url: r = self._2FA(r) @@ -237,8 +237,6 @@ class Client(object): if 'save-device' in r.url: r = self._cleanGet(SaveDeviceURL) - log.info(r.url) - if 'home' in r.url: self._post_login() return True @@ -257,8 +255,6 @@ class Client(object): log.info('Submitting 2FA code') r = self._cleanPost(CheckpointURL, data) - - log.info(r.url) if 'home' in r.url: return r @@ -271,7 +267,6 @@ class Client(object): log.info('Saving browser') #At this stage, we have dtsg, nh, name_action_selected, submit[Continue] r = self._cleanPost(CheckpointURL, data) - log.info(r.url) if 'home' in r.url: return r @@ -279,7 +274,6 @@ class Client(object): log.info(data.values()) #At this stage, we have dtsg, nh, submit[Continue] r = self._cleanPost(CheckpointURL, data) - log.info(r.url) if 'home' in r.url: return r @@ -288,7 +282,6 @@ class Client(object): log.info('Verifying login attempt') #At this stage, we have dtsg, nh, submit[This was me] r = self._cleanPost(CheckpointURL, data) - log.info(r.url) if 'home' in r.url: return r @@ -297,8 +290,6 @@ class Client(object): data['name_action_selected'] = 'save_device' log.info('Saving device again') #At this stage, we have dtsg, nh, submit[Continue], name_action_selected r = self._cleanPost(CheckpointURL, data) - - log.info(r.url) return r def saveSession(self, sessionfile): From 10430a5c5fa1e30c61ac25eb72ee650c1bddfbe4 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:09:28 +1000 Subject: [PATCH 14/19] Update client.py --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index 33c9e7e..37c5138 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -333,7 +333,7 @@ class Client(object): for i in range(1,max_retries+1): if not self._login(): log.warning("Attempt #{} failed{}".format(i,{True:', retrying'}.get(i<5,''))) - time.sleep(1000) + time.sleep(1) continue else: log.info("Login successful") From 83b5918a6d5886e30354601955543f0a62d7a109 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:12:00 +1000 Subject: [PATCH 15/19] Update client.py --- fbchat/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 37c5138..1dbbe4c 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -230,11 +230,12 @@ class Client(object): r = self._cleanPost(LoginURL, data) - + # Usually, 'Checkpoint' will refer to 2FA if 'checkpoint' in r.url: r = self._2FA(r) - if 'save-device' in r.url: + # Sometimes Facebook tries to show the user a "Save Device" dialog + if 'save-device' in r.url and 'Enter Security Code to Continue' in r.text: r = self._cleanGet(SaveDeviceURL) if 'home' in r.url: From fd6a3ab3e4752766fcc977ae1355ec47ac604807 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:17:13 +1000 Subject: [PATCH 16/19] Added 2FA Support Added new function _2FA. Specified that two things that are definitely shown when 2FA is enabled --- fbchat/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 1dbbe4c..dcc40b5 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -231,11 +231,11 @@ class Client(object): r = self._cleanPost(LoginURL, data) # Usually, 'Checkpoint' will refer to 2FA - if 'checkpoint' in r.url: + if 'checkpoint' in r.url and 'Enter Security Code to Continue' in r.text: r = self._2FA(r) # Sometimes Facebook tries to show the user a "Save Device" dialog - if 'save-device' in r.url and 'Enter Security Code to Continue' in r.text: + if 'save-device' in r.url: r = self._cleanGet(SaveDeviceURL) if 'home' in r.url: From ca606b04b86320eabeb819afef1c754174772006 Mon Sep 17 00:00:00 2001 From: Tim Chan Date: Tue, 4 Apr 2017 20:18:55 +1000 Subject: [PATCH 17/19] Added logging --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index dcc40b5..3cad503 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -272,7 +272,7 @@ class Client(object): return r del(data['name_action_selected']) - log.info(data.values()) #At this stage, we have dtsg, nh, submit[Continue] + log.info('Starting Facebook checkup flow') #At this stage, we have dtsg, nh, submit[Continue] r = self._cleanPost(CheckpointURL, data) if 'home' in r.url: From 2c4b9fbf80257e88218f4db0ed26022aece73071 Mon Sep 17 00:00:00 2001 From: Thiago Date: Tue, 4 Apr 2017 11:14:12 -0300 Subject: [PATCH 18/19] added the hability to control fbchat from an external event loop --- fbchat/client.py | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index e7a5952..7e2f2f3 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -74,6 +74,7 @@ class Client(object): raise Exception("id and password or config is needed") self.debug = debug + self.sticky, self.pool = (None, None) self._session = requests.session() self.req_counter = 1 self.seq = "0" @@ -776,23 +777,44 @@ class Client(object): self.on_message_error(sys.exc_info(), m) - def listen(self, markAlive=True): + def start_listening(self): + """Start listening from an external event loop.""" self.listening = True - sticky, pool = self._getSticky() + self.sticky, self.pool = self._getSticky() + + + def do_one_listen(self, markAlive=True): + """Does one cycle of the listening loop. + This method is only useful if you want to control fbchat from an + external event loop.""" + try: + if markAlive: self.ping(self.sticky) + try: + content = self._pullMessage(self.sticky, self.pool) + if content: self._parseMessage(content) + except requests.exceptions.RequestException as e: + pass + except KeyboardInterrupt: + self.listening = False + except requests.exceptions.Timeout: + pass + + + def stop_listening(self): + """Cleans up the variables from start_listening.""" + self.listening = False + self.sticky, self.pool = (None, None) + + + def listen(self, markAlive=True): + self.start_listening() log.info("Listening...") while self.listening: - try: - if markAlive: self.ping(sticky) - try: - content = self._pullMessage(sticky, pool) - if content: self._parseMessage(content) - except requests.exceptions.RequestException as e: - continue - except KeyboardInterrupt: - break - except requests.exceptions.Timeout: - pass + self.do_one_listen(markAlive) + + self.stop_listening() + def getUserInfo(self, *user_ids): """Get user info from id. Unordered. From 1475d8c4db48552aa4c8d50a1d89a271bb12576b Mon Sep 17 00:00:00 2001 From: Taehoon Kim Date: Wed, 5 Apr 2017 00:04:56 +0900 Subject: [PATCH 19/19] update pypi dist --- fbchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 0b0f46c..6e81747 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from .client import * __copyright__ = 'Copyright 2015 by Taehoon Kim' -__version__ = '0.8.0' +__version__ = '0.8.2' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan' __email__ = 'carpedm20@gmail.com'