summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCyrille Bagard <nocbos@gmail.com>2017-06-18 17:41:33 (GMT)
committerCyrille Bagard <nocbos@gmail.com>2017-06-18 17:41:33 (GMT)
commit57e5e3cc81751622e66fc228baaad2d38c408cfa (patch)
tree353338c68f13c4df50ea9f818d21d2fe79ad09ec
Initial commit.
-rw-r--r--config.py22
-rw-r--r--cupinder.py159
-rw-r--r--db.py256
-rw-r--r--facebook.py185
-rw-r--r--nl.py76
-rw-r--r--profile.py193
-rw-r--r--tinder.py180
7 files changed, 1071 insertions, 0 deletions
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('<input[^>]*>')
+
+ type_exp = re.compile('type="hidden"')
+ id_exp = re.compile('id="([^"]*)"')
+ name_exp = re.compile('name="([^"]*)"')
+ value_exp = re.compile('value="([^"]*)"')
+
+ form = re.compile('<form.*</form>').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('<input[^>]*>')
+
+ 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()