├── .nvmrc ├── .ruby-version ├── .eslintignore ├── commitlint.config.js ├── .eslintrc ├── src ├── __mocks__ │ └── axios.js ├── index.js ├── createHTTPClient.js ├── index.integration.test.js └── createHTTPClient.unit.test.js ├── .huskyrc ├── jest.config.shared.js ├── jest.config.unit.js ├── .npmignore ├── jest.config.integration.js ├── .babelrc ├── LICENSE ├── .gitignore ├── rollup.config.js ├── .github └── workflows │ └── workflows.yaml ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.1 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.5 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-angular'] }; 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "airbnb-base", 4 | "env": { 5 | "jest": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/__mocks__/axios.js: -------------------------------------------------------------------------------- 1 | import mockAxios from 'jest-mock-axios'; 2 | 3 | export default mockAxios; 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm run lint", 4 | "commit-msg": "npm run lint-commit-msg", 5 | "pre-push": "npm run unit-test" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.shared.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: [ 5 | '/build/', 6 | '/node_modules/', 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /jest.config.unit.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./jest.config.shared'); 2 | 3 | const config = Object.assign( 4 | { 5 | testMatch: ['**/**.unit.test.js'], 6 | }, 7 | sharedConfig, 8 | ); 9 | 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | 3 | src/** 4 | test/** 5 | coverage/** 6 | 7 | npm-debug.log 8 | commitlint.config.js 9 | *.test.js 10 | 11 | .DS_Store 12 | .eslintcache 13 | .travis.yml 14 | .babelrc 15 | .eslintignore 16 | .eslintrc 17 | -------------------------------------------------------------------------------- /jest.config.integration.js: -------------------------------------------------------------------------------- 1 | const sharedConfig = require('./jest.config.shared'); 2 | 3 | const config = Object.assign( 4 | { 5 | testMatch: ['**/**.integration.test.js'], 6 | }, 7 | sharedConfig, 8 | ); 9 | 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { "modules": "umd" } 8 | ] 9 | ], 10 | "plugins": [ 11 | "@babel/transform-runtime", 12 | "@babel/transform-async-to-generator" 13 | ] 14 | }, 15 | "production": { 16 | "presets": [ 17 | [ 18 | "@babel/preset-env", 19 | { "modules": false } 20 | ] 21 | ], 22 | "plugins": [ 23 | "@babel/transform-runtime", 24 | "@babel/transform-async-to-generator" 25 | ] 26 | } 27 | }, 28 | "ignore": [ 29 | "node_modules/**", 30 | "*.test.js" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jae Bradley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build 61 | .vscode 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | generateToken, 4 | } from 'tinder-access-token-generator'; 5 | 6 | import createHTTPClient from './createHTTPClient'; 7 | 8 | /** 9 | * https://github.com/fbessez/Tinder 10 | * https://gist.github.com/rtt/10403467 11 | */ 12 | 13 | const GENDERS = Object.freeze({ 14 | male: 0, 15 | female: 1, 16 | }); 17 | 18 | const GENDER_SEARCH_OPTIONS = Object.freeze({ 19 | male: 0, 20 | female: 1, 21 | both: -1, 22 | }); 23 | 24 | async function createClientFromFacebookAccessToken(facebookAccessToken) { 25 | const loginResponse = await axios.post( 26 | 'https://api.gotinder.com/v2/auth/login/facebook', 27 | { 28 | token: facebookAccessToken, 29 | }, 30 | ); 31 | return createHTTPClient(loginResponse.data.data.api_token); 32 | } 33 | 34 | async function createClientFromFacebookLogin({ emailAddress, password }) { 35 | const { 36 | apiToken, 37 | } = await generateToken({ 38 | facebookEmailAddress: emailAddress, 39 | facebookPassword: password, 40 | }); 41 | 42 | return createHTTPClient(apiToken); 43 | } 44 | 45 | export { 46 | createClientFromFacebookAccessToken, 47 | createClientFromFacebookLogin, 48 | GENDERS, 49 | GENDER_SEARCH_OPTIONS, 50 | }; 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import localResolve from 'rollup-plugin-local-resolve'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | import minify from 'rollup-plugin-babel-minify'; 7 | import { terser } from 'rollup-plugin-terser'; 8 | 9 | import pkg from './package.json'; 10 | 11 | const config = { 12 | external: ['axios'], 13 | input: 'src/index.js', 14 | output: [ 15 | { 16 | file: pkg.browser, 17 | format: 'umd', 18 | name: pkg.name, 19 | globals: { 20 | axios: 'axios', 21 | }, 22 | }, 23 | { 24 | file: pkg.main, 25 | format: 'cjs', 26 | name: pkg.name, 27 | globals: { 28 | axios: 'axios', 29 | }, 30 | }, 31 | { 32 | file: pkg.module, 33 | format: 'es', 34 | name: pkg.name, 35 | globals: { 36 | axios: 'axios', 37 | }, 38 | }, 39 | ], 40 | plugins: [ 41 | babel({ exclude: 'node_modules/**', runtimeHelpers: true }), 42 | localResolve(), 43 | resolve({ 44 | module: true, 45 | jsnext: true, 46 | main: true, 47 | preferBuiltins: true, 48 | browser: true, 49 | modulesOnly: true, 50 | }), 51 | minify(), 52 | terser(), 53 | commonjs(), 54 | filesize(), 55 | ], 56 | }; 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /.github/workflows/workflows.yaml: -------------------------------------------------------------------------------- 1 | name: Tinder Client 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | schedule: 11 | - cron: '0 12 * * *' 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest] 20 | node: [10, 12, 14] 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | ref: ${{ github.ref }} 25 | - name: Setup Node 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node }} 29 | - name: Cache dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: ~/.npm 33 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: | 35 | ${{ runner.OS }}-node- 36 | ${{ runner.OS }}- 37 | - name: Install dependencies 38 | run: npm ci 39 | - name: Run Linting 40 | run: npm run lint 41 | - name: Run Build 42 | run: npm run build 43 | - name: Check ES5 Compatible 44 | run: npm run is-es5 45 | coverage: 46 | name: Code Coverage 47 | runs-on: ubuntu-latest 48 | needs: build 49 | steps: 50 | - uses: actions/checkout@v2 51 | with: 52 | ref: ${{ github.ref }} 53 | - name: Setup Node 54 | uses: actions/setup-node@v1 55 | - name: Install dependencies 56 | run: npm ci 57 | - name: Run Tests 58 | run: npm run unit-test 59 | - name: Codecov 60 | uses: codecov/codecov-action@v1 61 | release: 62 | name: Release 63 | runs-on: ubuntu-latest 64 | if: ${{ github.ref == 'refs/heads/master' }} 65 | needs: build 66 | steps: 67 | - uses: actions/checkout@v2 68 | with: 69 | ref: ${{ github.ref }} 70 | - name: Setup Node 71 | uses: actions/setup-node@v1 72 | - name: Install dependencies 73 | run: npm ci 74 | - name: Run Build 75 | run: npm run build 76 | - name: Semantic Release 77 | uses: cycjimmy/semantic-release-action@v2 78 | with: 79 | semantic_version: 16 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 82 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinder-client", 3 | "version": "0.0.0-development", 4 | "description": "NodeJS client to access the Tinder API", 5 | "files": [ 6 | "build" 7 | ], 8 | "engines": { 9 | "node": "^8 || ^10 || ^11" 10 | }, 11 | "browser": "build/index.js", 12 | "main": "build/index.cjs.js", 13 | "module": "build/index.esm.js", 14 | "global": false, 15 | "scripts": { 16 | "build": "BABEL_ENV=production rollup -c", 17 | "codecov": "codecov", 18 | "deploy": "npm run travis-deploy-once 'npm run semantic-release'", 19 | "gc": "commit", 20 | "is-es5": "es-check es5 build/index.js", 21 | "lint": "eslint --ext .js .", 22 | "lint-commit-msg": "commitlint -e $GIT_PARAMS", 23 | "prepare": "npm run build", 24 | "semantic-release": "semantic-release", 25 | "test": "BABEL_ENV=test jest src/ -c jest.config.shared.js", 26 | "unit-test": "BABEL_ENV=test jest src/ -c jest.config.unit.js", 27 | "integration-test": "BABEL_ENV=test jest src/ -c jest.config.integration.js", 28 | "travis-deploy-once": "travis-deploy-once" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/jaebradley/tinder-client.git" 33 | }, 34 | "keywords": [ 35 | "tinder", 36 | "client", 37 | "tinder client", 38 | "tinder api", 39 | "tinder nodejs" 40 | ], 41 | "author": "jae.b.bradley@gmail.com", 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/jaebradley/tinder-client/issues" 45 | }, 46 | "homepage": "https://github.com/jaebradley/tinder-client#readme", 47 | "dependencies": { 48 | "axios": "0.21.0", 49 | "tinder-access-token-generator": "3.0.4" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.2.3", 53 | "@babel/core": "^7.3.3", 54 | "@babel/plugin-transform-async-to-generator": "^7.2.0", 55 | "@babel/plugin-transform-runtime": "^7.2.0", 56 | "@babel/preset-env": "^7.3.1", 57 | "@babel/runtime": "^7.3.1", 58 | "@commitlint/cli": "^8.3.5", 59 | "@commitlint/config-angular": "^7.5.0", 60 | "@commitlint/prompt": "^8.3.5", 61 | "@commitlint/prompt-cli": "^8.3.5", 62 | "ajv": "^6.9.2", 63 | "axios-debug": "0.0.4", 64 | "babel-core": "^6.26.3", 65 | "babel-jest": "^24.7.1", 66 | "babel-preset-minify": "^0.5.0", 67 | "codecov": "^3.2.0", 68 | "dotenv": "^7.0.0", 69 | "es-check": "^5.0.0", 70 | "eslint": "^5.14.1", 71 | "eslint-config-airbnb-base": "^13.1.0", 72 | "eslint-plugin-import": "^2.16.0", 73 | "husky": "^2.1.0", 74 | "jest": "^24.7.1", 75 | "jest-mock-axios": "^3.0.0", 76 | "rollup": "^1.29.0", 77 | "rollup-plugin-babel": "^4.3.2", 78 | "rollup-plugin-babel-minify": "^8.0.0", 79 | "rollup-plugin-commonjs": "^9.2.1", 80 | "rollup-plugin-filesize": "^6.0.1", 81 | "rollup-plugin-local-resolve": "^1.0.7", 82 | "rollup-plugin-node-resolve": "^4.0.1", 83 | "rollup-plugin-terser": "^5.2.0", 84 | "semantic-release": "^17.2.3", 85 | "travis-deploy-once": "^5.0.11" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/createHTTPClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /** 4 | * https://github.com/fbessez/Tinder 5 | * https://gist.github.com/rtt/10403467 6 | */ 7 | 8 | // Headers from https://github.com/fbessez/Tinder/blob/8bf8612e93702844b640fb2d79b2918238d376e9/tinder_api.py#L7-L13 9 | const SHARED_HEADERS = Object.freeze({ 10 | app_version: '6.9.4', 11 | platform: 'ios', 12 | 'User-Agent': 'Tinder/7.5.3 (iPhone; iOS 10.3.2; Scale/2.00)', 13 | Accept: 'application/json', 14 | }); 15 | 16 | export default function createHTTPClient(accessToken) { 17 | const client = axios.create({ 18 | baseURL: 'https://api.gotinder.com', 19 | headers: { 20 | 'X-Auth-Token': accessToken, 21 | ...SHARED_HEADERS, 22 | }, 23 | }); 24 | return { 25 | getProfile() { 26 | return client.get('/profile').then(response => response.data); 27 | }, 28 | 29 | updateProfile({ userGender, searchPreferences }) { 30 | const { 31 | maximumAge, 32 | minimumAge, 33 | genderPreference, 34 | maximumRangeInKilometers, 35 | } = searchPreferences; 36 | 37 | return client.post( 38 | '/profile', 39 | { 40 | age_filter_min: minimumAge, 41 | age_filter_max: maximumAge, 42 | gender_filter: genderPreference, 43 | gender: userGender, 44 | distance_filter: maximumRangeInKilometers, 45 | }, 46 | ).then(response => response.data); 47 | }, 48 | 49 | getRecommendations() { 50 | return client.get('/user/recs').then(response => response.data); 51 | }, 52 | 53 | getUser(userId) { 54 | return client.get(`/user/${userId}`).then(response => response.data); 55 | }, 56 | 57 | getMetadata() { 58 | return client.get('/meta').then(response => response.data); 59 | }, 60 | 61 | changeLocation({ latitude, longitude }) { 62 | return client 63 | .post('/user/ping', { lat: latitude, lon: longitude }) 64 | .then(response => response.data); 65 | }, 66 | 67 | like(userId) { 68 | return client.get(`/like/${userId}`).then(response => response.data); 69 | }, 70 | 71 | pass(userId) { 72 | return client.get(`/pass/${userId}`).then(response => response.data); 73 | }, 74 | 75 | superLike(userId) { 76 | return client.post(`/like/${userId}/super`).then(response => response.data); 77 | }, 78 | 79 | messageMatch({ matchId, message }) { 80 | return client.post(`/user/matches/${matchId}`, { message }).then(response => response.data); 81 | }, 82 | 83 | getMatch(matchId) { 84 | return client.get(`/matches/${matchId}`).then(response => response.data); 85 | }, 86 | 87 | getMessage(messageId) { 88 | return client.get(`/message/${messageId}`).then(response => response.data); 89 | }, 90 | 91 | getCommonConnections(userId) { 92 | return client.get(`/user/${userId}/common_connections`).then(response => response.data); 93 | }, 94 | 95 | getUpdates(sinceTimestamp = '') { 96 | return client 97 | .post('/updates', { last_activity_date: sinceTimestamp }) 98 | .then(response => response.data); 99 | }, 100 | 101 | resetTemporaryLocation() { 102 | return client.post('/passport/user/reset').then(response => response.data); 103 | }, 104 | 105 | temporarilyChangeLocation({ latitude, longitude }) { 106 | return client.post( 107 | '/passport/user/travel', 108 | { 109 | lat: latitude, 110 | lon: longitude, 111 | }, 112 | ).then(response => response.data); 113 | }, 114 | 115 | unmatch(matchId) { 116 | return client.delete(`/user/matches/${matchId}`).then(response => response.data); 117 | }, 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/index.integration.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import dotenv from 'dotenv'; 3 | import { 4 | createClientFromFacebookLogin, 5 | } from './index'; 6 | 7 | jest.setTimeout(15000); 8 | 9 | dotenv.config(); 10 | 11 | describe('TinderClient', () => { 12 | let client; 13 | 14 | beforeAll(async () => { 15 | const actualAxios = require.requireActual('axios'); 16 | axios.post.mockImplementation(actualAxios.post); 17 | axios.create.mockImplementation(actualAxios.create); 18 | axios.get.mockImplementation(actualAxios.get); 19 | client = await createClientFromFacebookLogin({ 20 | emailAddress: process.env.FACEBOOK_EMAIL_ADDRESS, 21 | password: process.env.FACEBOOK_PASSWORD, 22 | }); 23 | }); 24 | 25 | afterAll(() => { 26 | client = null; 27 | }); 28 | 29 | describe('Integration tests', () => { 30 | describe('Profile', () => { 31 | it('should fetch profile', async () => { 32 | const response = await client.getProfile(); 33 | expect(response).toBeDefined(); 34 | }); 35 | 36 | it('should have profile with some expected keys', async () => { 37 | const response = await client.getProfile(); 38 | expect(response).toEqual( 39 | expect.objectContaining({ 40 | _id: expect.any(String), 41 | age_filter_max: expect.any(Number), 42 | age_filter_min: expect.any(Number), 43 | bio: expect.any(String), 44 | birth_date: expect.any(String), 45 | blend: expect.any(String), 46 | create_date: expect.any(String), 47 | discoverable: expect.any(Boolean), 48 | distance_filter: expect.any(Number), 49 | email: expect.any(String), 50 | gender: expect.any(Number), 51 | gender_filter: expect.any(Number), 52 | name: expect.any(String), 53 | }), 54 | ); 55 | }); 56 | }); 57 | 58 | describe('Recommendations', () => { 59 | it('should fetch recommendations', async () => { 60 | const response = await client.getRecommendations(); 61 | expect(response).toBeDefined(); 62 | }); 63 | }); 64 | 65 | describe('User', () => { 66 | it('should fetch user', async () => { 67 | const response = await client.getUser(process.env.TINDER_USER_ID); 68 | expect(response).toBeDefined(); 69 | }); 70 | }); 71 | 72 | describe('Metadata', () => { 73 | it('should fetch metadata', async () => { 74 | const response = await client.getMetadata(); 75 | expect(response).toBeDefined(); 76 | }); 77 | }); 78 | 79 | xdescribe('#like', () => { 80 | it('likes recommendation', async () => { 81 | const recommendations = await client.getRecommendations(); 82 | // eslint-disable-next-line no-underscore-dangle 83 | const firstRecommendationUserId = recommendations[0]._id; 84 | const response = await client.like(firstRecommendationUserId); 85 | expect(response).toBeDefined(); 86 | }); 87 | }); 88 | 89 | xdescribe('#pass', () => { 90 | it('passes on recommendation', async () => { 91 | const recommendations = await client.getRecommendations(); 92 | // eslint-disable-next-line no-underscore-dangle 93 | const firstRecommendationUserId = recommendations[0]._id; 94 | const response = await client.pass(firstRecommendationUserId); 95 | expect(response).toBeDefined(); 96 | }); 97 | }); 98 | 99 | describe('#temporarilyChangeLocation', () => { 100 | it('temporarily changes location', async () => { 101 | const response = await client.changeLocation({ latitude: 42.3601, longitude: 71.0589 }); 102 | expect(response).toBeDefined(); 103 | }); 104 | }); 105 | 106 | describe('#getUpdates', () => { 107 | describe('when timestamp is defined', () => { 108 | it('gets updates', async () => { 109 | const response = await client.getUpdates('2017-03-25T20:58:00.404Z'); 110 | expect(response).toBeDefined(); 111 | }); 112 | }); 113 | 114 | describe('when timestamp is not defined', () => { 115 | it('gets updates', async () => { 116 | const response = await client.getUpdates(); 117 | expect(response).toBeDefined(); 118 | }); 119 | }); 120 | }); 121 | 122 | describe('#getCommonConnections', () => { 123 | it('gets no common connections with current user', async () => { 124 | const response = await client.getCommonConnections(process.env.TINDER_USER_ID); 125 | expect(response).toBeDefined(); 126 | expect(response).toEqual({}); 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinder Client 2 | 3 | ![Tinder Client](https://github.com/jaebradley/tinder-client/workflows/Tinder%20Client/badge.svg) 4 | [![npm](https://img.shields.io/npm/dt/tinder-client.svg)](https://www.npmjs.com/package/tinder-client) 5 | [![npm](https://img.shields.io/npm/v/tinder-client.svg)](https://www.npmjs.com/package/tinder-client) 6 | 7 | - [Tinder Client](#tinder-client) 8 | - [Introduction](#introduction) 9 | - [Dependencies](#dependencies) 10 | - [API](#api) 11 | - [Creating a client](#creating-a-client) 12 | - [`createClientFromFacebookAccessToken`](#createclientfromfacebookaccesstoken) 13 | - [`createClientFromFacebookLogin`](#createclientfromfacebooklogin) 14 | - [`getProfile`](#getprofile) 15 | - [`updateProfile`](#updateprofile) 16 | - [`getRecommendations`](#getrecommendations) 17 | - [`getUser`](#getuser) 18 | - [`getMetadata`](#getmetadata) 19 | - [`changeLocation`](#changelocation) 20 | - [`like`](#like) 21 | - [`pass`](#pass) 22 | - [`superLike`](#superlike) 23 | - [`getUpdates`](#getupdates) 24 | - [`messageMatch`](#messagematch) 25 | - [`getMatch`](#getmatch) 26 | - [`getMessage`](#getmessage) 27 | - [`getCommonConnections`](#getcommonconnections) 28 | - [`resetTemporaryLocation`](#resettemporarylocation) 29 | - [`temporarilyChangeLocation`](#temporarilychangelocation) 30 | - [Local Development](#local-development) 31 | - [Git Hooks](#git-hooks) 32 | - [Commit Linting](#commit-linting) 33 | - [Retrieving Facebook User ID and Facebook Access Token](#retrieving-facebook-user-id-and-facebook-access-token) 34 | 35 | ## Introduction 36 | 37 | Tinder has an unofficial API that has been documented by [this gist](https://gist.github.com/rtt/10403467) and [`fbessez/Tinder`](https://github.com/fbessez/Tinder). 38 | 39 | There is also an existing Node Client, [`tinderjs`](https://www.npmjs.com/package/tinderjs). This is a `Promise`-based equivalent. 40 | 41 | ## Dependencies 42 | 43 | `tinder-client` has two dependencies: 44 | 45 | - [`axios`](https://github.com/axios/axios) (`^0.18.0`) 46 | - [`tinder-access-token-generator`](https://github.com/jaebradley/tinder-access-token-generator) (`^2.0.0`) - this is used to generate Tinder API access tokens 47 | 48 | ## API 49 | 50 | ### Creating a client 51 | 52 | There are two ways to create a client 53 | 54 | - If you have access to a user's Facebook access token, then you can use the `createClientFromFacebookAccessToken` factory function 55 | - If you have access to a user's Facebook email & password, then you can use the `createClientFromFacebookLogin` factory function 56 | 57 | The returned client object will have a variety of methods that will provide access to the Tinder API. 58 | 59 | #### `createClientFromFacebookAccessToken` 60 | 61 | ```javascript 62 | import { createClientFromFacebookAccessToken } from 'tinder-client'; 63 | 64 | const client = await createClientFromFacebookLogin('some facebook access token'); 65 | ``` 66 | 67 | #### `createClientFromFacebookLogin` 68 | 69 | ```javascript 70 | import { createClientFromFacebookLogin } from 'tinder-client'; 71 | 72 | const client = await createClientFromFacebookLogin({ 73 | emailAddress: 'your facebook email address', 74 | password: 'your facebook password', 75 | }); 76 | ``` 77 | 78 | ### `getProfile` 79 | 80 | ```javascript 81 | const profile = await client.getProfile(); 82 | ``` 83 | 84 | ### `updateProfile` 85 | 86 | ```javascript 87 | import { GENDERS, GENDER_SEARCH_OPTIONS } from 'tinder-client'; 88 | 89 | const userGender = GENDERS.female; 90 | const searchPreferences = { 91 | maximumAge: 100, 92 | minimumAge: 99, 93 | genderPreference: GENDER_SEARCH_OPTIONS.both, 94 | maximumRangeInKilometers: 100, 95 | }; 96 | const profile = await client.updateProfile({ userGender, searchPreferences }) 97 | ``` 98 | 99 | ### `getRecommendations` 100 | 101 | ```javascript 102 | const recommendations = await client.getRecommendations(); 103 | ``` 104 | 105 | ### `getUser` 106 | 107 | ```javascript 108 | const user = await client.getUser('someUserId'); 109 | ``` 110 | 111 | ### `getMetadata` 112 | 113 | Get metadata for authenticated user 114 | 115 | ```javascript 116 | const myMetadata = await client.getMetadata(); 117 | ``` 118 | 119 | ### `changeLocation` 120 | 121 | ```javascript 122 | await client.changeLocation({ latitude: 'someLatitude', longitude: 'someLongitude' }); 123 | ``` 124 | 125 | ### `like` 126 | 127 | ```javascript 128 | await client.like('someUserId'); 129 | ``` 130 | 131 | ### `pass` 132 | 133 | ```javascript 134 | await client.pass('someUserId'); 135 | ``` 136 | 137 | ### `superLike` 138 | 139 | ```javascript 140 | await client.superLike('someUserId'); 141 | ``` 142 | 143 | ### `getUpdates` 144 | 145 | ```javascript 146 | await client.getUpdates(); 147 | await client.getUpdates('2019-02-05T00:00:00.004Z'); 148 | ``` 149 | 150 | ### `messageMatch` 151 | 152 | ```javascript 153 | await client.messageMatch({ matchId: 'someMatch', message: 'someMessage' }); 154 | ``` 155 | 156 | ### `getMatch` 157 | 158 | ```javascript 159 | await client.getMatch('someMatchId'); 160 | ``` 161 | 162 | ### `getMessage` 163 | 164 | ```javascript 165 | await client.getMessage('someMessageId'); 166 | ``` 167 | 168 | ### `getCommonConnections` 169 | 170 | ```javascript 171 | await client.getCommonConnections('someTinderUserId'); 172 | ``` 173 | 174 | ### `resetTemporaryLocation` 175 | 176 | ```javascript 177 | await client.resetTemporaryLocation(); 178 | ``` 179 | 180 | ### `temporarilyChangeLocation` 181 | 182 | ```javascript 183 | await client.temporarilyChangeLocation({ latitude: 'someLatitude', longitude: 'someLongitude' }); 184 | ``` 185 | 186 | ## Local Development 187 | 188 | After cloning the repository, use `nvm` / `npm` to install dependencies. 189 | 190 | To run both all tests, execute `npm run test`. 191 | 192 | To only run unit tests, execute `npm run unit-test`. 193 | 194 | To only run integration tests, execute `npm run integration-test`. 195 | 196 | In order to execute local integration tests successfully, you'll need to specify the following environment variables in the `.env` file 197 | 198 | - `FACEBOOK_EMAIL_ADDRESS` 199 | - `FACEBOOK_PASSWORD` 200 | - `TINDER_USER_ID` (Your Tinder user id) 201 | 202 | To build the production bundle, execute `npm run build`. 203 | 204 | ### Git Hooks 205 | 206 | This project uses [`husky`](https://github.com/typicode/husky) to maintain git hooks. 207 | 208 | - `pre-commit` - run `eslint` 209 | - `commit-msg` - run commit message linting 210 | - `pre-push` - run unit tests 211 | 212 | ### Commit Linting 213 | 214 | This project uses [`semantic-release`](https://github.com/semantic-release/semantic-release) and [`commitlint`](https://github.com/conventional-changelog/commitlint) (specifically the [Angular commit convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)) to automatically enforce semantic versioning. 215 | 216 | ### Retrieving Facebook User ID and Facebook Access Token 217 | 218 | For local development, you might want to test the client out at on an ad-hoc basis or maybe even for integration testing. 219 | 220 | In order to do so, you'll need to get your Facebook Access Token. 221 | 222 | To retrieve a Facebook Access Token, you'll need to 223 | 224 | - Go to [this URL](https://www.facebook.com/v2.8/dialog/oauth?app_id=464891386855067&channel_url=https%3A%2F%2Fstaticxx.facebook.com%2Fconnect%2Fxd_arbiter%2Fr%2Fd_vbiawPdxB.js%3Fversion%3D44%23cb%3Df213b0a5a606e94%26domain%3Dtinder.com%26origin%3Dhttps%253A%252F%252Ftinder.com%252Ff14b12c5d35c01c%26relation%3Dopener&client_id=464891386855067&display=popup&domain=tinder.com&e2e=%7B%7D&fallback_redirect_uri=200ee73f-9eb7-9632-4fdb-432ed0c670fa&locale=en_US&origin=1&redirect_uri=https%3A%2F%2Fstaticxx.facebook.com%2Fconnect%2Fxd_arbiter%2Fr%2Fd_vbiawPdxB.js%3Fversion%3D44%23cb%3Df20cfec000032b4%26domain%3Dtinder.com%26origin%3Dhttps%253A%252F%252Ftinder.com%252Ff14b12c5d35c01c%26relation%3Dopener%26frame%3Df2cc4d71cc96f9&response_type=token%2Csigned_request&scope=user_birthday%2Cuser_photos%2Cemail%2Cuser_friends%2Cuser_likes&sdk=joey&version=v2.8&ret=login) 225 | - Open the `Network` tab 226 | - Look for a request to `/confirm` 227 | 228 | ![confirm-request](https://user-images.githubusercontent.com/8136030/52327616-93f08e00-29a1-11e9-8438-3174ad663f17.png) 229 | 230 | - Look at the response, specifically the `for.jsmods.require[3][0]` value, and search the text for `access_token` 231 | 232 | ![confirm-request-response](https://user-images.githubusercontent.com/8136030/52327797-2e50d180-29a2-11e9-90b3-d801816290b9.png) 233 | -------------------------------------------------------------------------------- /src/createHTTPClient.unit.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'jest-mock-axios'; 2 | import createHTTPClient from './createHTTPClient'; 3 | 4 | describe('Unit tests', () => { 5 | let client; 6 | 7 | const accessToken = 'some access token'; 8 | 9 | beforeEach(() => { 10 | axios.post.mockResolvedValue({ data: 'default response' }); 11 | axios.get.mockResolvedValue({ data: 'default response' }); 12 | axios.delete.mockResolvedValue({ data: 'default delete response' }); 13 | client = createHTTPClient(accessToken); 14 | }); 15 | 16 | afterEach(() => { 17 | axios.reset(); 18 | }); 19 | 20 | describe('factory function', () => { 21 | it('generates client from static constructor', async () => { 22 | expect(client).toBeDefined(); 23 | expect(axios.create).toHaveBeenCalledTimes(1); 24 | expect(axios.create).toHaveBeenCalledWith({ 25 | baseURL: 'https://api.gotinder.com', 26 | headers: { 27 | 'X-Auth-Token': 'some access token', 28 | app_version: '6.9.4', 29 | platform: 'ios', 30 | 'User-Agent': 'Tinder/7.5.3 (iPhone; iOS 10.3.2; Scale/2.00)', 31 | Accept: 'application/json', 32 | }, 33 | }); 34 | }); 35 | }); 36 | 37 | describe('client methods', () => { 38 | describe('getProfile', () => { 39 | it('gets profile', async () => { 40 | const profile = await client.getProfile(); 41 | expect(axios.get).toHaveBeenCalledTimes(1); 42 | expect(axios.get).toHaveBeenCalledWith('/profile'); 43 | expect(profile).toBeDefined(); 44 | expect(profile).toBe('default response'); 45 | }); 46 | }); 47 | 48 | describe('updateProfile', () => { 49 | it('updates profile', async () => { 50 | const userGender = 'userGender'; 51 | const maximumAge = 'maximumAge'; 52 | const minimumAge = 'minimumAge'; 53 | const genderPreference = 'genderPreference'; 54 | const maximumRangeInKilometers = 'maximumRangeInKilometers'; 55 | const searchPreferences = { 56 | maximumAge, 57 | minimumAge, 58 | genderPreference, 59 | maximumRangeInKilometers, 60 | }; 61 | const response = await client.updateProfile({ 62 | userGender, 63 | searchPreferences, 64 | }); 65 | expect(axios.post).toHaveBeenCalledTimes(1); 66 | expect(axios.post).toHaveBeenCalledWith( 67 | '/profile', 68 | { 69 | age_filter_min: minimumAge, 70 | age_filter_max: maximumAge, 71 | gender_filter: genderPreference, 72 | gender: userGender, 73 | distance_filter: maximumRangeInKilometers, 74 | }, 75 | ); 76 | expect(response).toBe('default response'); 77 | }); 78 | }); 79 | 80 | describe('getRecommendations', () => { 81 | it('gets recommendations', async () => { 82 | const recommendations = await client.getRecommendations(); 83 | expect(axios.get).toHaveBeenCalledTimes(1); 84 | expect(axios.get).toHaveBeenCalledWith('/user/recs'); 85 | expect(recommendations).toBe('default response'); 86 | }); 87 | }); 88 | 89 | describe('getUser', () => { 90 | it('gets user data', async () => { 91 | const userId = 'userId'; 92 | const userData = await client.getUser(userId); 93 | expect(axios.get).toHaveBeenCalledTimes(1); 94 | expect(axios.get).toHaveBeenCalledWith('/user/userId'); 95 | expect(userData).toBe('default response'); 96 | }); 97 | }); 98 | 99 | describe('getMetadata', () => { 100 | it('gets metadata', async () => { 101 | const metadata = await client.getMetadata(); 102 | expect(axios.get).toHaveBeenCalledTimes(1); 103 | expect(axios.get).toHaveBeenCalledWith('/meta'); 104 | expect(metadata).toBe('default response'); 105 | }); 106 | }); 107 | 108 | describe('changeLocation', () => { 109 | it('changes location', async () => { 110 | const latitude = 'latitude'; 111 | const longitude = 'longitude'; 112 | const response = await client.changeLocation({ latitude, longitude }); 113 | expect(axios.post).toHaveBeenCalledTimes(1); 114 | expect(axios.post).toHaveBeenCalledWith( 115 | '/user/ping', 116 | { lat: latitude, lon: longitude }, 117 | ); 118 | expect(response).toBe('default response'); 119 | }); 120 | }); 121 | 122 | describe('like', () => { 123 | it('likes user', async () => { 124 | const userId = 'userId'; 125 | const response = await client.like(userId); 126 | expect(axios.get).toHaveBeenCalledTimes(1); 127 | expect(axios.get).toHaveBeenCalledWith(`/like/${userId}`); 128 | expect(response).toBe('default response'); 129 | }); 130 | }); 131 | 132 | describe('pass', () => { 133 | it('passes on user', async () => { 134 | const userId = 'userId'; 135 | const response = await client.pass(userId); 136 | expect(axios.get).toHaveBeenCalledTimes(1); 137 | expect(axios.get).toHaveBeenCalledWith(`/pass/${userId}`); 138 | expect(response).toBe('default response'); 139 | }); 140 | }); 141 | 142 | describe('superLike', () => { 143 | it('super likes a user', async () => { 144 | const userId = 'userId'; 145 | const response = await client.superLike(userId); 146 | expect(axios.post).toHaveBeenCalledTimes(1); 147 | expect(axios.post).toHaveBeenCalledWith(`/like/${userId}/super`); 148 | expect(response).toBe('default response'); 149 | }); 150 | }); 151 | 152 | describe('messageMatch', () => { 153 | it('messages a match', async () => { 154 | const matchId = 'matchId'; 155 | const message = 'message'; 156 | const response = await client.messageMatch({ matchId, message }); 157 | expect(axios.post).toHaveBeenCalledTimes(1); 158 | expect(axios.post).toHaveBeenCalledWith(`/user/matches/${matchId}`, { message }); 159 | expect(response).toBe('default response'); 160 | }); 161 | }); 162 | 163 | describe('getCommonConnections', () => { 164 | it('gets common connections with another user', async () => { 165 | const response = await client.getCommonConnections('some user id'); 166 | expect(axios.get).toHaveBeenCalledTimes(1); 167 | expect(axios.get).toHaveBeenCalledWith('/user/some user id/common_connections'); 168 | expect(response).toBe('default response'); 169 | }); 170 | }); 171 | 172 | describe('getMatch', () => { 173 | it('gets data for a match', async () => { 174 | const matchId = 'matchId'; 175 | const response = await client.getMatch(matchId); 176 | expect(axios.get).toHaveBeenCalledTimes(1); 177 | expect(axios.get).toHaveBeenCalledWith(`/matches/${matchId}`); 178 | expect(response).toBe('default response'); 179 | }); 180 | }); 181 | 182 | describe('getMessage', () => { 183 | it('gets data for a message', async () => { 184 | const messageId = 'messageId'; 185 | const response = await client.getMessage(messageId); 186 | expect(axios.get).toHaveBeenCalledTimes(1); 187 | expect(axios.get).toHaveBeenCalledWith(`/message/${messageId}`); 188 | expect(response).toBe('default response'); 189 | }); 190 | }); 191 | 192 | describe('getUpdates', () => { 193 | describe('when timestamp is defined', () => { 194 | it('get all updates since the given date', async () => { 195 | const timestamp = '2017-03-25T20:58:00.404Z'; 196 | const response = await client.getUpdates(timestamp); 197 | expect(axios.post).toBeCalledTimes(1); 198 | expect(axios.post).toHaveBeenCalledWith( 199 | '/updates', 200 | { last_activity_date: timestamp }, 201 | ); 202 | expect(response).toBe('default response'); 203 | }); 204 | }); 205 | 206 | describe('when timestamp is not defined', () => { 207 | it('get all updates', async () => { 208 | const response = await client.getUpdates(); 209 | expect(axios.post).toBeCalledTimes(1); 210 | expect(axios.post).toHaveBeenCalledWith( 211 | '/updates', 212 | { last_activity_date: '' }, 213 | ); 214 | expect(response).toBe('default response'); 215 | }); 216 | }); 217 | }); 218 | 219 | describe('resetTemporaryLocation', () => { 220 | it('resets temporary location', async () => { 221 | const response = await client.resetTemporaryLocation(); 222 | expect(axios.post).toHaveBeenCalledTimes(1); 223 | expect(axios.post).toHaveBeenCalledWith('/passport/user/reset'); 224 | expect(response).toBe('default response'); 225 | }); 226 | }); 227 | 228 | describe('temporarilyChangeLocation', () => { 229 | it('temporarily changes location', async () => { 230 | const latitude = 'latitude'; 231 | const longitude = 'longitude'; 232 | const response = await client.temporarilyChangeLocation({ latitude, longitude }); 233 | expect(axios.post).toHaveBeenCalledTimes(1); 234 | expect(axios.post).toHaveBeenCalledWith( 235 | '/passport/user/travel', 236 | { lat: latitude, lon: longitude }, 237 | ); 238 | expect(response).toBe('default response'); 239 | }); 240 | }); 241 | 242 | describe('#unmatch', () => { 243 | it('unmatches with specified match', async () => { 244 | const response = await client.unmatch('someMatchId'); 245 | expect(axios.delete).toHaveBeenCalledTimes(1); 246 | expect(axios.delete).toHaveBeenCalledWith('/user/matches/someMatchId'); 247 | expect(response).toBe('default delete response'); 248 | }); 249 | }); 250 | }); 251 | }); 252 | --------------------------------------------------------------------------------