From 57e5e3cc81751622e66fc228baaad2d38c408cfa Mon Sep 17 00:00:00 2001 From: Cyrille Bagard Date: Sun, 18 Jun 2017 19:41:33 +0200 Subject: Initial commit. --- config.py | 22 ++++++ cupinder.py | 159 +++++++++++++++++++++++++++++++++++++ db.py | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ facebook.py | 185 +++++++++++++++++++++++++++++++++++++++++++ nl.py | 76 ++++++++++++++++++ profile.py | 193 +++++++++++++++++++++++++++++++++++++++++++++ tinder.py | 180 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1071 insertions(+) create mode 100644 config.py create mode 100644 cupinder.py create mode 100644 db.py create mode 100644 facebook.py create mode 100644 nl.py create mode 100644 profile.py create mode 100644 tinder.py diff --git a/config.py b/config.py new file mode 100644 index 0000000..2c4fdbf --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ + +facebook_email = 'XXXX@YYYYY.com' +facebook_password = 'XXXXXX' + +OUTPUT_DIR = 'output' + +DISTANCE_IN_KM = True + +MIN_AGE = 25.0 +MAX_AGE = 65.0 + +MAX_DISTANCE_IN_MI = 10 + +TARGET_LANGUAGE = 'english' + +BLACK_LIST_KEYWORDS = [ + 'God is busy' +] + +COLOR_RESET = "\033[0m" +COLOR_PASS = "\033[1;31m" +COLOR_LIKE = "\033[1;32m" diff --git a/cupinder.py b/cupinder.py new file mode 100644 index 0000000..e26c4f7 --- /dev/null +++ b/cupinder.py @@ -0,0 +1,159 @@ + +import argparse +from time import sleep + +import config +from db import HumanKind +from facebook import FacebookAPI +from nl import LanguageIdentifier +from tinder import TinderAPI + + +def connect(): + """Establish a connection to Facebook/Tinder.""" + + fb = FacebookAPI(config.facebook_email, config.facebook_password) + + fb_id = fb.get_user_id() + + print('[i] Facebook ID: ' + fb_id) + + fb_token = fb.get_tinder_token() + + print('[i] Tinder token: ' + fb_token) + + tinder = TinderAPI(fb_id, fb_token) + + print('[i] Tinder auth token: ' + tinder.get_auth_token()) + + print('[i] Remaining likes: %d' % tinder.get_remaining_likes()) + + return tinder + + +if __name__=='__main__': + + parser = argparse.ArgumentParser() + parser.add_argument('-g', '--grab', help='Grab profiles from Tinder', action='store_true') + parser.add_argument('-p', '--process', help='Process stored Tinder profiles', action='store_true') + parser.add_argument('--dump-new', help='Extract newly stored Tinder profiles', action='store_true') + parser.add_argument('--dump-all', help='Extract all stored Tinder profiles', action='store_true') + + args = parser.parse_args() + + + db = HumanKind() + + + if args.grab: + + tinder = connect() + + extra = 0 + + while True: + + profiles = tinder.get_recommendations(1) + + for p in profiles: + + if not(db.has_profile_id(p)): + + extra += 1 + + print(p) + db.store_profile(p) + + if extra == 0: + print('[!] No new profiles to grab! Exiting...') + break + + print('[i] Loaded ' + config.COLOR_LIKE + '%d' % extra + config.COLOR_RESET \ + + ' new profile%s... Taking some rest now!' % ('s' if extra > 2 else '')) + + sleep(5 * 60) + + + elif args.process: + + tinder = connect() + + lid = LanguageIdentifier() + + profiles = db.load_new_profiles() + + remaining = tinder.get_remaining_likes() + + for p in profiles: + + like = True + + age = p.compute_age() + + if not(age is None): + if age < config.MIN_AGE or age > config.MAX_AGE: + like = False + + if p._distance_mi > config.MAX_DISTANCE_IN_MI: + like = False + + if like and not(p._bio is None): + + language = lid.detect_language(p._bio) + + like = (language == config.TARGET_LANGUAGE) + + if like and not(p._bio is None): + + text = p._bio.lower() + + for kwd in config.BLACK_LIST_KEYWORDS: + if kwd in text: + like = False + break + + if like: + + if remaining == 0: + print('[!] No more likes available. Exiting...') + break + + remaining -= 1 + + print('[*] %s: ' % p._name + config.COLOR_LIKE + 'like!' + config.COLOR_RESET) + + tinder.like(p._id) + db.mark_profile_as_liked(p) + + else: + + print('[*] %s: ' % p._name + config.COLOR_PASS + 'pass' + config.COLOR_RESET) + + tinder.dislike(p._id) + db.mark_profile_as_passed(p) + + sleep(5) + + + elif args.dump_new: + + profiles = db.load_new_profiles() + + for p in profiles: + + print('[i] Extracting information about %s (id=%s)...' % (p._name, p._id)) + + p.output() + db.mark_profile_as_extracted(p) + + + elif args.dump_all: + + profiles = db.load_all_profiles() + + for p in profiles: + + print('[i] Extracting information about %s (id=%s)...' % (p._name, p._id)) + + p.output() + db.mark_profile_as_extracted(p) diff --git a/db.py b/db.py new file mode 100644 index 0000000..7986156 --- /dev/null +++ b/db.py @@ -0,0 +1,256 @@ + +import sqlite3 + +from profile import TinderProfile + + + +# See 'row_factory' in https://docs.python.org/3/library/sqlite3.html#connection-objects +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + + +class HumanKind(object): + + def __init__(self): + """Create a database for storing Tinder profiles.""" + + self._db = sqlite3.connect('profiles.db', detect_types=sqlite3.PARSE_DECLTYPES) + + sqlite3.register_adapter(bool, int) + sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) + + self._db.row_factory = dict_factory + + sql = ''' + CREATE TABLE IF NOT EXISTS People( + uid TEXT PRIMARY KEY, + name TEXT, + gender INTEGER, + birth_date TEXT, + ping_time TEXT, + school TEXT, + job_title TEXT, + job_company TEXT, + distance_mi INTEGER, + bio TEXT, + liked BOOLEAN, + superliked BOOLEAN, + passed BOOLEAN, + extracted BOOLEAN + ) + ''' + + cursor = self._db.cursor() + cursor.execute(sql) + self._db.commit() + + sql = ''' + CREATE TABLE IF NOT EXISTS Photos( + uid TEXT, + url TEXT, + main BOOLEAN + ) + ''' + + cursor = self._db.cursor() + cursor.execute(sql) + self._db.commit() + + + def has_profile_id(self, profile): + """Tell is a given Tinder profile is known in the database.""" + + values = (profile._id, ) + + cursor = self._db.cursor() + cursor.execute('SELECT uid FROM People WHERE uid = ?', values) + + found = cursor.fetchone() + + return not(found is None) + + + def store_profile(self, profile): + """Store a Tinder profile into the database.""" + + exist = self.has_profile_id(profile) + + if exist: + + fields = 'name = ?, gender = ?, birth_date = ?, ping_time = ?, school = ?,' \ + ' job_title = ?, job_company = ?, distance_mi = ?, bio = ?' + + values = (profile._name, profile._gender, profile._birth_date, + profile._ping_time, profile._school, profile._job_title, + profile._job_company, profile._distance_mi, profile._bio, + profile._id) + + cursor = self._db.cursor() + cursor.execute('UPDATE People SET ' + fields + ' WHERE uid = ?', values) + + else: + + values = (profile._id, profile._name, profile._gender, profile._birth_date, + profile._ping_time, profile._school, profile._job_title, + profile._job_company, profile._distance_mi, profile._bio, + False, False, False, False) + + cursor = self._db.cursor() + cursor.execute('INSERT INTO People VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', values) + + for p in profile._photos: + + values = (profile._id, p['url']) + + cursor = self._db.cursor() + cursor.execute('SELECT uid FROM Photos WHERE uid = ? AND url = ?', values) + + found = cursor.fetchone() + + if found is None: + + values = (profile._id, p['url'], p['main']) + + cursor = self._db.cursor() + cursor.execute('INSERT INTO Photos VALUES (?, ?, ?)', values) + + self._db.commit() + + + def load_profile(self, uid): + """Load a Tinder profile from the database.""" + + values = (uid, ) + + cursor = self._db.cursor() + cursor.execute('SELECT * FROM People WHERE uid = ?', values) + + found = cursor.fetchone() + + if not(found): + profile = None + + else: + + data = {} + data['jobs'] = {} + data['schools'] = {} + data['photos'] = [] + + data['ping_time'] = found['ping_time'] + + if found['job_title']: + data['jobs']['title'] = found['job_title'] + + if found['job_company']: + data['jobs']['company'] = found['job_company'] + + if found['school']: + data['schools']['name'] = found['school'] + + data['gender'] = found['gender'] + + data['distance_mi'] = found['distance_mi'] + + data['_id'] = found['uid'] + + if found['bio'] is None: + data['bio'] = '' + else: + data['bio'] = found['bio'] + + data['name'] = found['name'] + + data['birth_date'] = found['birth_date'] + + values = (uid, ) + + cursor = self._db.cursor() + cursor.execute('SELECT url, main FROM Photos WHERE uid = ?', values) + + for found in cursor.fetchall(): + + photo = { 'url': found['url'], 'width': 1, 'height': 1 } + + rec = {} + + rec['processedFiles'] = [ photo ] + + if not(found['main']): + rec['main'] = False + + data['photos'].append(rec) + + profile = TinderProfile(data) + + return profile + + + def load_all_profiles(self): + """Load all stored Tinder profiles.""" + + new = [] + + cursor = self._db.cursor() + cursor.execute('SELECT uid FROM People') + + for found in cursor.fetchall(): + + profile = self.load_profile(found['uid']) + + new.append(profile) + + return new + + + def load_new_profiles(self): + """Load all new Tinder profiles.""" + + new = [] + + values = (False, False, False) + + cursor = self._db.cursor() + cursor.execute('SELECT uid FROM People WHERE liked = ? AND superliked = ? AND passed = ?', values) + + for found in cursor.fetchall(): + + profile = self.load_profile(found['uid']) + + new.append(profile) + + return new + + + def mark_profile_as_liked(self, profile): + """Update information about a Tinder profile in the database.""" + + values = (True, profile._id) + + cursor = self._db.cursor() + cursor.execute('UPDATE People SET liked = ? WHERE uid = ?', values) + self._db.commit() + + + def mark_profile_as_passed(self, profile): + """Update information about a Tinder profile in the database.""" + + values = (True, profile._id) + + cursor = self._db.cursor() + cursor.execute('UPDATE People SET passed = ? WHERE uid = ?', values) + self._db.commit() + + + def mark_profile_as_extracted(self, profile): + """Update information about a Tinder profile in the database.""" + + values = (True, profile._id) + + cursor = self._db.cursor() + cursor.execute('UPDATE People SET extracted = ? WHERE uid = ?', values) + self._db.commit() diff --git a/facebook.py b/facebook.py new file mode 100644 index 0000000..c774ecf --- /dev/null +++ b/facebook.py @@ -0,0 +1,185 @@ + +import re +import requests + + +class FbAuthenticationError(Exception): + pass + + +class FacebookAPI(object): + + def __init__(self, email, password): + """Create a new Facebook interface.""" + + self._email = email + self._password = password + + self._session = None + + self._login() + + + def _build_login_params(self, content): + """Parse HTML code to get POST login parameters.""" + + params = {} + + # Get the form parameters + + input_exp = re.compile(']*>') + + type_exp = re.compile('type="hidden"') + id_exp = re.compile('id="([^"]*)"') + name_exp = re.compile('name="([^"]*)"') + value_exp = re.compile('value="([^"]*)"') + + form = re.compile('').search(content).group() + + for inp in input_exp.findall(form): + + match = type_exp.search(inp) + + if match is None: + continue + + name = name_exp.search(inp) + + if name is None: + continue + else: + name = name.group(1) + + value = value_exp.search(inp) + + if value is None: + value = '' + else: + value = value.group(1) + + params[name] = value + + # Extend with user login + + params["email"] = self._email + params["pass"] = self._password + params["login"] = "1" + + return params + + + def _login(self): + """Log into Facebook services.""" + + headers = { + "Host": "www.facebook.com", + "User-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "DNT": "1", + "Connection": "keep-alive", + "Cache-Control": "max-age=0", + } + + self._session = requests.Session() + self._session.headers.update(headers) + + response = self._session.post('https://www.facebook.com/login.php', data={}) + + params = self._build_login_params(str(response.content)) + + response = self._session.post('https://www.facebook.com/login.php', data=params) + + #print('headers', response.headers) + #print('cookies', requests.utils.dict_from_cookiejar(self._session.cookies)) + + # If c_user cookie is present, login was successful + + if not('c_user' in self._session.cookies): + raise FbAuthenticationError() + + + def get_user_id(self): + """Get the identifier for the registered user.""" + + if not('c_user' in self._session.cookies): + raise FbAuthenticationError() + + return self._session.cookies['c_user'] + + + def _build_access_params(self, content): + """Parse HTML code to get POST access parameters.""" + + params = {} + + # Get the form parameters + + input_exp = re.compile(']*>') + + type_exp = re.compile('type="hidden"') + id_exp = re.compile('id="([^"]*)"') + name_exp = re.compile('name="([^"]*)"') + value_exp = re.compile('value="([^"]*)"') + + form = re.compile('platformDialogForm.*form>').search(content).group() + + form = form.replace('\\u003C', '<') + form = form.replace('\\', '') + + for inp in input_exp.findall(form): + + match = type_exp.search(inp) + + if match is None: + continue + + name = name_exp.search(inp) + + if name is None: + continue + else: + name = name.group(1) + + value = value_exp.search(inp) + + if value is None: + value = '' + else: + value = value.group(1) + + params[name] = value + + return params + + + def get_tinder_token(self): + """Get an access token for Tinder.""" + + # Cf. https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow + + params = { + "redirect_uri": "fb464891386855067://authorize/", + "scope": "user_birthday,user_photos,user_education_history,email,user_relationship_details" \ + + ",user_friends,user_work_history,user_likes", + "response_type": "token,signed_request", + "client_id": "464891386855067" + } + + url = 'https://www.facebook.com/v2.6/dialog/oauth' + + response = self._session.get(url, params=params) + + params = self._build_access_params(str(response.content)) + + response = self._session.post('https://www.facebook.com/v2.6/dialog/oauth/confirm?dpr=1', data=params) + + token_exp = re.compile('access_token=([^&]*)&') + + match = token_exp.search(str(response.content)) + + if match is None: + raise FbAuthenticationError + + return match.group(1) diff --git a/nl.py b/nl.py new file mode 100644 index 0000000..7e697c4 --- /dev/null +++ b/nl.py @@ -0,0 +1,76 @@ + +import nltk +from nltk import wordpunct_tokenize +from nltk.corpus import stopwords + + +# +# See: http://www.nltk.org/ +# +# Code from: http://blog.alejandronolla.com/2013/05/15/detecting-text-language-with-python-and-nltk/ +# + + +class LanguageIdentifier(object): + + def __init__(self): + """Initialize the language detection.""" + + # Avoid the following LookupError: + # """ Resource 'corpora/stopwords.zip/stopwords/' not found. """ + + try: + languages = stopwords.fileids() + except: + nltk.download("stopwords") + + + def _calculate_languages_ratios(self, text): + """ + Calculate probability of given text to be written in several languages and + return a dictionary that looks like {'french': 2, 'spanish': 4, 'english': 0} + + @param text: Text whose language want to be detected + @type text: str + + @return: Dictionary with languages and unique stopwords seen in analyzed text + @rtype: dict + """ + + languages_ratios = {} + + tokens = wordpunct_tokenize(text) + words = [ word.lower() for word in tokens ] + + # Compute per language included in nltk number of unique stopwords appearing in analyzed text + for language in stopwords.fileids(): + + stopwords_set = set(stopwords.words(language)) + words_set = set(words) + common_elements = words_set.intersection(stopwords_set) + + languages_ratios[language] = len(common_elements) # language "score" + + return languages_ratios + + + def detect_language(self, text): + """ + Calculate probability of given text to be written in several languages and + return the highest scored. + + It uses a stopwords based approach, counting how many unique stopwords + are seen in analyzed text. + + @param text: Text whose language want to be detected + @type text: str + + @return: Most scored language guessed + @rtype: str + """ + + ratios = self._calculate_languages_ratios(text) + + most_rated_language = max(ratios, key=ratios.get) + + return most_rated_language diff --git a/profile.py b/profile.py new file mode 100644 index 0000000..e4d772b --- /dev/null +++ b/profile.py @@ -0,0 +1,193 @@ + +from datetime import datetime +import os +import requests + +import config + + +class TinderProfile(object): + + def _parse_processed_files(self, data, main): + """Keep the biggest photo from a list.""" + + best_size = 0 + best_url = None + + for p in data: + + size = p['width'] * p['height'] + + if size > best_size: + best_url = p['url'] + best_size = size + + if best_url: + self._photos.append({'url': best_url, 'main': main}) + + + def __init__(self, data): + """Load information about a Tinder user from its extracted data.""" + + self._ping_time = data['ping_time'] + + if 'title' in data['jobs']: + self._job_title = data['jobs']['title'] + else: + self._job_title = None + + if 'company' in data['jobs']: + self._job_company = data['jobs']['company'] + else: + self._job_company = None + + if 'name' in data['schools']: + self._school = data['schools']['name'] + else: + self._school = None + + self._photos = [] + + for p in data['photos']: + self._parse_processed_files(p['processedFiles'], not('main' in p.keys())) + + self._gender = data['gender'] + + self._distance_mi = data['distance_mi'] + + self._id = data['_id'] + + if 'bio' in data: + self._bio = data['bio'] if len(data['bio']) > 0 else None + else: + self._bio = None + + self._name = data['name'] + + self._birth_date = data['birth_date'] + + + def __str__(self): + """Output a summary of the profile.""" + + desc = self._name + + age = self.compute_age() + + if age: + desc += '\n age: %.1f years old' % age + + activity = self.compute_last_activity() + + unit = activity['unit'] if activity['value'] < 2 else activity['unit'] + 's' + + desc += '\n last seen: %.1f %s ago' % (activity['value'], unit) + + if self._school: + desc += '\n school: %' % self._school + + if self._job_title: + desc += '\n job: %s' % self._job_title + + if self._job_company: + desc += '\n company: %s' % self._job_company + + dist = self.compute_distance() + + unit = 'km' if config.DISTANCE_IN_KM else 'mile' + + if dist < 2: + desc += '\n distance: %d %s' % (dist, unit) + else: + desc += '\n distance: %d %ss' % (dist, unit) + + count = len(self._photos) + + if count < 2: + desc += '\n %d photo' % count + else: + desc += '\n %d photos' % count + + if self._bio: + + bio = self._bio.replace('\n', '\n ') + + while bio[-1] == '\n' or bio[-1] == ' ': + bio = bio[:-1] + + desc += '\n\n' + desc += ' ' + bio + + return desc + + + def output(self): + """Output the profile in a directory.""" + + dest = config.OUTPUT_DIR + os.sep + self._id + + if not os.path.exists(dest): + os.makedirs(dest) + + with open(dest + os.sep + 'info.txt', 'w') as f: + f.write(str(self)) + + for p in self._photos: + + response = requests.get(p['url'], stream=True) + + if response.status_code == 200: + + name = os.path.basename(p['url']) + + with open(dest + os.sep + name, 'wb') as f: + for chunk in response: + f.write(chunk) + + + def compute_age(self): + """Provide the age of the Tinder user.""" + + if not(self._birth_date): + age = None + + else: + + birth = datetime.strptime(self._birth_date, "%Y-%m-%dT%H:%M:%S.%fZ") + + diff = datetime.now() - birth + + age = float("%.1f" % round(diff.days / 365.0, 1)) + + return age + + + def compute_last_activity(self): + """Compute the last activity of the Tinder user.""" + + seen = datetime.strptime(self._ping_time, "%Y-%m-%dT%H:%M:%S.%fZ") + + diff = datetime.now() - seen + + if diff.days < (365.0 / 12): + activity = { 'value': diff.days, 'unit': 'day' } + + elif diff.days < 365: + activity = { 'value': float("%.1f" % round(diff.days / (365.0 / 12), 1)), 'unit': 'month' } + + else: + activity = { 'value': float("%.1f" % round(diff.days / 365.0, 1)), 'unit': 'year' } + + return activity + + + def compute_distance(self): + """Provide the distance in kms if needed.""" + + if config.DISTANCE_IN_KM: + dist = int(self._distance_mi * 1.60934) + + else: + dist = self._distance_mi + + return dist diff --git a/tinder.py b/tinder.py new file mode 100644 index 0000000..aaa94a4 --- /dev/null +++ b/tinder.py @@ -0,0 +1,180 @@ + +import json +import requests +from time import time + +from profile import TinderProfile + + +# +# References: +# - Tinder API documentation (https://gist.github.com/rtt/10403467) +# - pynder (https://github.com/charliewolf/pynder) +# + + +class TinderLoginError(Exception): + pass + + +class InitializationError(Exception): + pass + + +class RequestError(Exception): + pass + + +class ProcessingError(Exception): + pass + + +API_BASE = 'https://api.gotinder.com' + + +class TinderAPI(object): + + def __init__(self, fb_id, fb_token): + """Create a new Tinder interface.""" + + headers = { + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "Tinder Android Version 6.4.1", + "Host": API_BASE, + "os_version": "1935", + "app-version": "371", + "platform": "android", + "Accept-Encoding": "gzip" + } + + self._session = requests.Session() + self._session.headers.update(headers) + + self._auth(fb_id, fb_token) + + + def _url(self, path): + """Build a full URL to resources.""" + + return API_BASE + path + + + def _auth(self, fb_id, fb_token): + """Authenticate with Tinder services.""" + + params = { + "facebook_id": fb_id, + "facebook_token": fb_token + } + + params = json.dumps(params) + + response = self._session.post(self._url('/auth'), data=params) + + response = json.loads(response.content.decode('utf-8')) + + if 'token' not in response: + raise TinderLoginError() + + self._token = response['token'] + self._session.headers.update({"X-Auth-Token": str(response['token'])}) + + + def get_auth_token(self): + """Provide the Tider authentication token.""" + + if not hasattr(self, '_token'): + raise TinderInitializationError() + + return self._token + + + def _request(self, method, url, data={}): + """Perform a request to Tinder's services in a generic way.""" + + if not hasattr(self, '_token'): + raise TinderInitializationError() + + result = self._session.request(method, self._url(url), data=json.dumps(data)) + + while result.status_code == 429: + blocker = threading.Event() + blocker.wait(0.01) + result = self._session.request(method, self._url(url), data=data) + + if result.status_code != 200: + raise RequestError(result.status_code) + + return result.json() + + + def _get(self, url): + """Perform a GET request.""" + + return self._request("get", url) + + + def _post(self, url, data={}): + """Perform a POST request.""" + + return self._request("post", url, data=data) + + + def _meta(self): + """Get meta information about the current Tinder user.""" + + return self._get("/meta") + + + def get_remaining_likes(self): + """Provide the number of remaining likes for the current Tinder user.""" + + meta_dct = self._meta() + + return meta_dct['rating']['likes_remaining'] + + + def get_recommendations(self, limit=10): + """Get a fresh list of Tinder profiles.""" + + new = [] + + data = self._post("/user/recs", data={"limit": limit}) + + for d in data['results']: + if not d["_id"].startswith("tinder_rate_limited_id_"): + profile = TinderProfile(d) + new.append(profile) + + return new + + + def get_info(self, uid): + """Get information about a given Tinder user.""" + + data = self._get("/user/" + uid) + + if 'results' in data: + profile = TinderProfile(data['results']) + else: + profile = None + + return profile + + + def like(self, uid): + """Like a Tinder profile.""" + + self._get('/like/%s' % uid) + + if data['status'] != 200: + raise ProcessingError() + + + def dislike(self, uid): + """Dislike a Tinder profile.""" + + data = self._get('/pass/%s' % uid) + + if data['status'] != 200: + raise ProcessingError() -- cgit v0.11.2-87-g4458