├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── chrome ├── icons │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── bump-version.sh └── manifest.json ├── .babelrc ├── .prettierrc.js ├── .gitignore ├── .release-it.yml ├── src ├── misc │ ├── bg.js │ ├── helper.js │ ├── Interactions.js │ ├── insert-css.js │ └── api.js ├── automations │ ├── Anonymous.js │ ├── Instagram.js │ ├── HideUnanswered.js │ ├── Swiper.js │ └── Messenger.js ├── index.js └── views │ ├── Sidebar.js │ └── templates.js ├── webpack.config.js ├── .eslintrc.js ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Lunars] 4 | -------------------------------------------------------------------------------- /chrome/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geczy/tinder-autopilot/HEAD/chrome/icons/icon128.png -------------------------------------------------------------------------------- /chrome/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geczy/tinder-autopilot/HEAD/chrome/icons/icon16.png -------------------------------------------------------------------------------- /chrome/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geczy/tinder-autopilot/HEAD/chrome/icons/icon48.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | trailingComma: 'none' 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | node_modules 7 | yarn-error.log 8 | dist 9 | .env 10 | zips 11 | -------------------------------------------------------------------------------- /.release-it.yml: -------------------------------------------------------------------------------- 1 | npm: 2 | publish: false 3 | hooks: 4 | after:bump: 5 | - mkdir -p zips 6 | - rm -f zips/tinder-autopilot-v*.zip || true 7 | - bash chrome/bump-version.sh ${version} 8 | - yarn build 9 | - cd dist/ && bestzip tinder-autopilot-v${version}.zip * && mv tinder-autopilot-v* ../zips/ && cd .. 10 | after:release: mkdir -p zips && rm -f zips/tinder-autopilot-v*.zip || true 11 | git: 12 | requireCleanWorkingDir: false 13 | github: 14 | assets: 15 | - zips/tinder-autopilot-v*.zip 16 | release: true 17 | -------------------------------------------------------------------------------- /src/misc/bg.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 2 | fetch(request.url, request.options).then( 3 | function (response) { 4 | return response.text().then(function (text) { 5 | sendResponse([ 6 | { 7 | body: text, 8 | status: response.status, 9 | statusText: response.statusText 10 | }, 11 | null 12 | ]); 13 | }); 14 | }, 15 | function (error) { 16 | sendResponse([null, error]); 17 | } 18 | ); 19 | return true; 20 | }); 21 | -------------------------------------------------------------------------------- /src/automations/Anonymous.js: -------------------------------------------------------------------------------- 1 | import { insertCss, removeCss } from '../misc/insert-css'; 2 | 3 | class Anonymous { 4 | selector = '.tinderAutopilotAnonymous'; 5 | 6 | start = () => { 7 | insertCss( 8 | `.messageListItem [aria-label], 9 | .Expand[aria-label] { 10 | filter: blur(3px); 11 | } 12 | 13 | .profileCard__slider__img, 14 | .StretchedBox[aria-label] { 15 | filter: blur(20px); 16 | }` 17 | ); 18 | }; 19 | 20 | stop = () => { 21 | removeCss(); 22 | }; 23 | } 24 | 25 | export default Anonymous; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | new CopyPlugin({ 6 | patterns: [ 7 | { from: 'chrome/icons', to: 'icons' }, 8 | { from: 'chrome/manifest.json', to: 'manifest.json' }, 9 | { from: 'src/misc/bg.js', to: 'bg.js' } 10 | ] 11 | }) 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader' 20 | } 21 | } 22 | ] 23 | }, 24 | output: { 25 | chunkLoading: false, 26 | wasmLoading: false 27 | }, 28 | target: ['web', 'es5'] 29 | }; 30 | -------------------------------------------------------------------------------- /chrome/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script updates the "version" field in the "chrome/manifest.json" file. 4 | 5 | # Define the file path for the manifest. 6 | manifestPath="chrome/manifest.json" 7 | 8 | # Define the function to update the version field in the manifest. 9 | updateVersion() { 10 | # Use jq to update the version field with the specified version number. 11 | jq ".version = \"$1\"" $manifestPath >tmp_manifest.json 12 | mv tmp_manifest.json $manifestPath 13 | } 14 | 15 | # Check if the version argument was provided. 16 | if [[ -z $1 ]]; then 17 | echo "Error: Version number argument is required." 18 | exit 1 19 | fi 20 | 21 | # Update the version field in the manifest. 22 | updateVersion "$1" 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'], 3 | parser: 'babel-eslint', 4 | env: { 5 | browser: true, 6 | commonjs: true, 7 | es6: true, 8 | jest: true, 9 | node: true 10 | }, 11 | rules: { 12 | 'arrow-body-style': 'off', 13 | 'consistent-return': 'off', 14 | 'jsx-a11y/href-no-hash': ['off'], 15 | 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }], 16 | 'max-len': [ 17 | 'warn', 18 | { 19 | code: 100, 20 | tabWidth: 2, 21 | comments: 100, 22 | ignoreComments: false, 23 | ignoreTrailingComments: true, 24 | ignoreUrls: true, 25 | ignoreStrings: true, 26 | ignoreTemplateLiterals: true, 27 | ignoreRegExpLiterals: true 28 | } 29 | ] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Autopilot for Tinder", 3 | "short_name": "Tinder Auto", 4 | "homepage_url": "https://github.com/Geczy/tinder-autopilot/", 5 | "icons": { 6 | "16": "icons/icon16.png", 7 | "48": "icons/icon48.png", 8 | "128": "icons/icon128.png" 9 | }, 10 | "description": "Don't waste any more time doing manual tasks on Tinder. Autopilot will do it for you.", 11 | "manifest_version": 2, 12 | "update_url": "https://clients2.google.com/service/update2/crx", 13 | "version": "3.0.0", 14 | "author": "Geczy", 15 | "background": { 16 | "scripts": [ 17 | "bg.js" 18 | ], 19 | "persistent": false 20 | }, 21 | "content_scripts": [ 22 | { 23 | "js": [ 24 | "main.js" 25 | ], 26 | "matches": [ 27 | "https://tinder.com/*", 28 | "https://*.gotinder.com/*" 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get'; 2 | import { logger } from './misc/helper'; 3 | import Sidebar from './views/Sidebar'; 4 | import { getMyProfile } from './misc/api'; 5 | import Instagram from './automations/Instagram'; 6 | 7 | class TinderAssistant { 8 | boostRemaining = false; 9 | 10 | isBoosting = false; 11 | 12 | constructor() { 13 | getMyProfile().then((profileData) => { 14 | const d = new Date(); 15 | const n = d.getTime(); 16 | 17 | const expires = get(profileData, 'data.boost.expires_at'); 18 | this.boostRemaining = get(profileData, 'data.boost.allotment_remaining'); 19 | const boostMinutesLeft = Math.round(expires - n) / 1000 / 60; 20 | if (boostMinutesLeft > 0) { 21 | this.isBoosting = true; 22 | } 23 | 24 | localStorage.setItem('TinderAutopilot/ProfileData', JSON.stringify(profileData)); 25 | 26 | const sidebar = new Sidebar(); 27 | const instagram = new Instagram(); 28 | logger('Welcome to Tinder Autopilot'); 29 | }); 30 | } 31 | } 32 | 33 | setTimeout(() => { 34 | const Tinder = new TinderAssistant(); 35 | }, 500); 36 | -------------------------------------------------------------------------------- /src/misc/helper.js: -------------------------------------------------------------------------------- 1 | const generateRandomNumber = (min = 800, max = 1500) => { 2 | return Math.random() * (max - min) + min; 3 | }; 4 | 5 | const randomDelay = async () => { 6 | const rand = generateRandomNumber(350, 600); 7 | return new Promise((resolve) => setTimeout(resolve, rand)); 8 | }; 9 | 10 | const logger = (v) => { 11 | console.log(v); 12 | const now = new Date(); 13 | const txt = document.querySelector('.txt'); 14 | const message = /* html */ `

15 | ${`0${now.getHours()}`.slice(-2)}:${`0${now.getMinutes()}`.slice( 16 | -2 17 | )}:${`0${now.getSeconds()}`.slice(-2)}. 18 | ${v}

`; 19 | txt.innerHTML = message + txt.innerHTML; 20 | }; 21 | 22 | const waitUntilElementExists = (selector, callback) => { 23 | const el = document.querySelector(selector); 24 | if (el) { 25 | callback(el); 26 | } 27 | setTimeout(() => waitUntilElementExists(selector, callback), 500); 28 | }; 29 | 30 | export { logger, randomDelay, generateRandomNumber, waitUntilElementExists }; 31 | -------------------------------------------------------------------------------- /src/automations/Instagram.js: -------------------------------------------------------------------------------- 1 | import { insertCss } from '../misc/insert-css'; 2 | 3 | class Instagram { 4 | imageSelector = `${this.modalSelector} div[style*="instagram"]`; 5 | 6 | observer; 7 | 8 | constructor() { 9 | return; 10 | insertCss(` 11 | #modal-manager div[style*="instagram"] { cursor: zoom-in; } 12 | #modal-manager div[style*="instagram"]:hover { 13 | border: 3px solid #40a9ff; 14 | } 15 | `); 16 | 17 | try { 18 | const target = document.querySelector('[role="dialog"]').parentElement.parentElement; 19 | const observer = new MutationObserver(this.start); 20 | observer.observe(target, { childList: true }); 21 | 22 | this.observer = observer; 23 | } catch (e) {} 24 | } 25 | 26 | start = () => { 27 | setTimeout(() => { 28 | document.querySelectorAll(this.imageSelector).forEach((ig) => { 29 | const url = ig.style.backgroundImage.slice(4, -1).replace(/"/g, ''); 30 | ig.addEventListener('click', () => { 31 | window.open(url); 32 | }); 33 | }); 34 | }, 500); 35 | }; 36 | 37 | stop = () => { 38 | this.observer.disconnect(); 39 | }; 40 | } 41 | 42 | export default Instagram; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinder-autopilot", 3 | "version": "3.0.0", 4 | "description": "Don't waste any more time doing manual tasks on Tinder. Autopilot will do it for you.", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/Geczy/tinder-autopilot.git", 7 | "author": "Matthew ", 8 | "license": "MIT", 9 | "scripts": { 10 | "major": "dotenv release-it -- major --ci", 11 | "minor": "dotenv release-it -- minor --ci", 12 | "patch": "dotenv release-it -- patch --ci", 13 | "start": "webpack --mode development --watch", 14 | "build": "webpack --mode production" 15 | }, 16 | "dependencies": { 17 | "lodash": "^4.17.21" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.12.13", 21 | "@babel/plugin-proposal-class-properties": "^7.12.13", 22 | "@babel/plugin-transform-runtime": "^7.12.15", 23 | "@babel/runtime": "^7.12.13", 24 | "babel-eslint": "^10.1.0", 25 | "babel-loader": "^8.2.2", 26 | "bestzip": "^2.2.0", 27 | "copy-webpack-plugin": "^9.0.1", 28 | "dotenv-cli": "^4.0.0", 29 | "eslint": "^7.19.0", 30 | "eslint-config-airbnb": "^18.2.1", 31 | "eslint-config-prettier": "^8.3.0", 32 | "eslint-plugin-import": "^2.24.2", 33 | "eslint-plugin-jsx-a11y": "^6.4.1", 34 | "eslint-plugin-prettier": "^4.0.0", 35 | "eslint-plugin-react": "^7.22.0", 36 | "eslint-plugin-react-hooks": "4.2.0", 37 | "invariant": "^2.2.4", 38 | "prettier": "^2.4.1", 39 | "release-it": "^14.3.0", 40 | "webpack": "^5.76.0", 41 | "webpack-cli": "^4.5.0" 42 | }, 43 | "browserslist": [ 44 | "defaults", 45 | "not IE 11", 46 | "maintained node versions" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tinder Autopilot 🔥 2 | · 3 | ![Version](https://img.shields.io/badge/version-2.0.8-blue.svg?cacheSeconds=2592000) 4 | ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) 5 | ===== 6 | 7 | > Don't waste any more time doing manual tasks on Tinder. Autopilot will do it for you. 8 | 9 | [Install on the Chrome Web Store](https://chrome.google.com/webstore/detail/autopilot-for-tinder/bfpgbjekakfijondlfloonhdkcjlhehg?hl=en) 10 | 11 |

12 | product demo 13 |

14 | 15 | ## Key features 16 | 17 | - 🔥 Auto liker in random intervals 18 | - 🤫 Only show unanswered messages 19 | - 🖼 Anonymous mode to blur profile images 20 | - 💌 Bulk send messages to all your matches, or just the new matches 21 | - [Missing a feature? Click to make request](https://github.com/Geczy/tinder-autopilot/issues/new) 22 | 23 | ## Contributing 24 | 25 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 26 | 27 | 1. [Fork](https://github.com/Geczy/tinder-autopilot/fork) this repo 28 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 29 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 30 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 31 | 5. Open a Pull Request 32 | 33 | ## Development 34 | 35 | 1. Clone this repo 36 | 2. Run these commands 37 | 38 | ```sh 39 | yarn install 40 | yarn run start 41 | ``` 42 | 43 | 3. Open `chrome://extensions/` in your browser 44 | 4. Click **Load unpacked** and point to the `dist` folder of this repo 45 | 46 | ## Author 47 | 48 | 👤 **Geczy ** 49 | 50 | ## Show your support 51 | 52 | Give a ⭐️ if this project helped you! Maybe even a sponsorship 😍 53 | -------------------------------------------------------------------------------- /src/misc/Interactions.js: -------------------------------------------------------------------------------- 1 | import { logger } from './helper'; 2 | 3 | class Interactions { 4 | isOnMatchesPage = () => { 5 | return ( 6 | window.location.toString().indexOf('app/recs') !== -1 || 7 | window.location.toString().indexOf('app/matches') !== -1 8 | ); 9 | }; 10 | 11 | goToMainPage = () => { 12 | const matchesLink = document.querySelectorAll("a[href='/app/matches']"); 13 | if (matchesLink && matchesLink.length) { 14 | return matchesLink[0].click(); 15 | } 16 | 17 | const mainMenuLink = document.querySelectorAll("a[href='/app/recs']"); 18 | if (mainMenuLink && mainMenuLink.length) { 19 | return mainMenuLink[0].click(); 20 | } 21 | 22 | const matchesTab = document.querySelector('nav div:nth-child(1) > span'); 23 | if (matchesTab) { 24 | return matchesTab.click(); 25 | } 26 | }; 27 | 28 | closeInstructions = () => { 29 | // Homescreen modal blocks us 30 | try { 31 | if (document.querySelector('[data-testid="addToHomeScreen"]')) { 32 | document 33 | .querySelector('[data-testid="addToHomeScreen"]') 34 | .parentElement.querySelector('button:nth-of-type(2)') 35 | .click(); 36 | logger('Closing add to homescreen modal'); 37 | return true; 38 | } 39 | } catch (e) { 40 | return false; 41 | } 42 | }; 43 | 44 | closeMatchFound = () => { 45 | try { 46 | const modal = document.querySelector('[title="Back to Tinder"]'); 47 | if (modal) { 48 | modal.click(); 49 | logger('Closing match found'); 50 | return true; 51 | } 52 | } catch (e) { 53 | return false; 54 | } 55 | }; 56 | 57 | closeModal = () => { 58 | try { 59 | const modal = document.querySelector('[role="dialog"]').parentElement.parentElement; 60 | if (modal) { 61 | document.querySelector('[role="dialog"]').parentElement.click(); 62 | logger('Closing modal'); 63 | return true; 64 | } 65 | } catch (e) { 66 | return false; 67 | } 68 | }; 69 | } 70 | 71 | export default Interactions; 72 | -------------------------------------------------------------------------------- /src/misc/insert-css.js: -------------------------------------------------------------------------------- 1 | const containers = []; // will store container HTMLElement references 2 | const styleElements = []; // will store {prepend: HTMLElement, append: HTMLElement} 3 | 4 | const usage = 5 | 'insert-css: You need to provide a CSS string. Usage: insertCss(cssString[, options]).'; 6 | 7 | function removeCss() { 8 | try { 9 | document.getElementById('TinderAutopilot-insert-css').remove(); 10 | } catch {} 11 | } 12 | 13 | function insertCss(css, options) { 14 | options = options || {}; 15 | 16 | if (css === undefined) { 17 | throw new Error(usage); 18 | } 19 | 20 | const position = options.prepend === true ? 'prepend' : 'append'; 21 | const container = 22 | options.container !== undefined ? options.container : document.querySelector('head'); 23 | let containerId = containers.indexOf(container); 24 | 25 | // first time we see this container, create the necessary entries 26 | if (containerId === -1) { 27 | containerId = containers.push(container) - 1; 28 | styleElements[containerId] = {}; 29 | } 30 | 31 | // try to get the correponding container + position styleElement, create it otherwise 32 | removeCss(); 33 | const styleElement = (styleElements[containerId][position] = createStyleElement()); 34 | 35 | if (position === 'prepend') { 36 | container.insertBefore(styleElement, container.childNodes[0]); 37 | } else { 38 | container.appendChild(styleElement); 39 | } 40 | 41 | // strip potential UTF-8 BOM if css was read from a file 42 | if (css.charCodeAt(0) === 0xfeff) { 43 | css = css.substr(1, css.length); 44 | } 45 | 46 | // actually add the stylesheet 47 | if (styleElement.styleSheet) { 48 | styleElement.styleSheet.cssText += css; 49 | } else { 50 | styleElement.textContent += css; 51 | } 52 | 53 | return styleElement; 54 | } 55 | 56 | function createStyleElement() { 57 | const styleElement = document.createElement('style'); 58 | styleElement.id = 'TinderAutopilot-insert-css'; 59 | styleElement.setAttribute('type', 'text/css'); 60 | return styleElement; 61 | } 62 | 63 | module.exports = insertCss; 64 | module.exports.insertCss = insertCss; 65 | module.exports.removeCss = removeCss; 66 | -------------------------------------------------------------------------------- /src/automations/HideUnanswered.js: -------------------------------------------------------------------------------- 1 | import { logger } from '../misc/helper'; 2 | 3 | class HideUnanswered { 4 | selector = '.tinderAutopilotHideMine'; 5 | 6 | totalMessages = 0; 7 | 8 | counter = 0; 9 | 10 | finishHiding = () => { 11 | document.querySelectorAll('.messageListItem__message svg').forEach((t) => { 12 | const messageItem = t.closest('.messageListItem'); 13 | const checkmarkIcon = messageItem.querySelector('svg[aria-label="Message Sent"]'); 14 | const replyMessage = messageItem.querySelector('.messageListItem__message:last-child'); 15 | 16 | if (!checkmarkIcon && !replyMessage) { 17 | // Hide the message item if it doesn't contain a checkmark icon and there is no reply message 18 | messageItem.style.display = 'none'; 19 | } 20 | }); 21 | 22 | const unansweredCount = Array.prototype.slice 23 | .call(document.querySelectorAll('.messageListItem')) 24 | .filter((item) => item.style.display !== 'none').length; 25 | 26 | // Scroll back to top of messages 27 | document.querySelector('.matchListTitle').parentElement.scrollTop = 0; 28 | 29 | logger(`Total matches that need a response: ${unansweredCount}`); 30 | }; 31 | 32 | 33 | 34 | scrollMatchesToEnd = (cb) => { 35 | const currHeight = document.querySelector('.matchListTitle').parentElement.scrollTop; 36 | const totalHeight = document.querySelector('.matchListTitle').parentElement.scrollHeight; 37 | const newTotal = document.querySelector('div.messageList').children.length; 38 | 39 | if (this.counter < 30 && currHeight < totalHeight) { 40 | this.counter += 1; 41 | document.querySelector('.matchListTitle').parentElement.scrollTop += window.outerHeight; 42 | setTimeout(() => this.scrollMatchesToEnd(cb), 100); 43 | } else { 44 | logger(`Finished scrolling, total matches found: ${newTotal}`); 45 | cb(); 46 | } 47 | 48 | if (newTotal > this.totalMessages) { 49 | this.counter = 0; 50 | } 51 | 52 | this.totalMessages = newTotal; 53 | }; 54 | 55 | start = () => { 56 | if (document.querySelector('#messages-tab')) { 57 | document.querySelector('#messages-tab').click(); 58 | } else { 59 | document.querySelector('a[href="/app/recs"]').click(); 60 | } 61 | 62 | this.totalMessages = document.querySelector('div.messageList').children.length; 63 | this.counter = 0; 64 | 65 | this.scrollMatchesToEnd(this.finishHiding); 66 | }; 67 | 68 | stop = () => { 69 | document.querySelectorAll('.messageListItem__message svg').forEach((t) => { 70 | t.closest('.messageListItem').style.display = 'flex'; 71 | }); 72 | }; 73 | } 74 | 75 | export default HideUnanswered; 76 | -------------------------------------------------------------------------------- /src/automations/Swiper.js: -------------------------------------------------------------------------------- 1 | import { logger, generateRandomNumber } from '../misc/helper'; 2 | import Interactions from '../misc/Interactions'; 3 | 4 | class Swiper { 5 | selector = '.tinderAutopilot'; 6 | 7 | isRunning = false; 8 | 9 | constructor() { 10 | this.interactions = new Interactions(); 11 | } 12 | 13 | start = () => { 14 | logger('Starting to swipe using a randomized interval'); 15 | this.isRunning = true; 16 | this.run(); 17 | }; 18 | 19 | stop = () => { 20 | this.isRunning = false; 21 | logger('Autopilot stopped ⛔️'); 22 | }; 23 | 24 | canSwipe = () => { 25 | return this.hasLike() && !document.querySelector('.beacon__circle'); 26 | }; 27 | 28 | hasLike = () => { 29 | const xpath = "//span[text()='Like']"; 30 | const matchingElement = document.evaluate( 31 | xpath, 32 | document, 33 | null, 34 | XPathResult.FIRST_ORDERED_NODE_TYPE, 35 | null 36 | ).singleNodeValue; 37 | return matchingElement.closest('button'); 38 | }; 39 | 40 | pressLike = () => { 41 | const likeButton = this.hasLike(); 42 | if (!likeButton && !this.canSwipe()) { 43 | return false; 44 | } 45 | 46 | likeButton.click(); 47 | document.getElementById('likeCount').innerHTML = 48 | parseInt(document.getElementById('likeCount').innerHTML, 10) + 1; 49 | return true; 50 | }; 51 | 52 | matchFound = () => { 53 | const found = document.querySelectorAll('button[aria-label="Close"]'); 54 | 55 | if (typeof found?.click !== 'function') { 56 | return false; 57 | } 58 | 59 | document.getElementById('matchCount').innerHTML = 60 | parseInt(document.getElementById('matchCount').innerHTML, 10) + 1; 61 | logger("Congrats! We've got a match! 🤡"); 62 | found.click(); 63 | return true; 64 | }; 65 | 66 | run = () => { 67 | if (!this.isRunning) { 68 | return; 69 | } 70 | 71 | // Must be on matches page 72 | if (!this.interactions.isOnMatchesPage()) { 73 | logger('Going to main page to start liking'); 74 | this.interactions.goToMainPage(); 75 | 76 | const waitForMatchPage = setInterval(() => { 77 | if (this.interactions.isOnMatchesPage()) { 78 | clearInterval(waitForMatchPage); 79 | setTimeout(this.run, generateRandomNumber()); 80 | } 81 | }, 250); 82 | } 83 | 84 | if (this.interactions.closeInstructions()) { 85 | setTimeout(this.run, generateRandomNumber()); 86 | return; 87 | } 88 | 89 | if (this.interactions.closeModal()) { 90 | setTimeout(this.run, generateRandomNumber()); 91 | return; 92 | } 93 | 94 | if (this.interactions.closeMatchFound()) { 95 | setTimeout(this.run, generateRandomNumber()); 96 | return; 97 | } 98 | 99 | if (!this.canSwipe()) { 100 | logger('No profiles found. Waiting 4s'); 101 | setTimeout(this.run, generateRandomNumber(3000, 4000)); 102 | return; 103 | } 104 | 105 | // Keep Swiping 106 | if (this.matchFound()) { 107 | setTimeout(this.run, generateRandomNumber(500, 900)); 108 | return; 109 | } 110 | 111 | // What we came here to do, swipe right! 112 | if (this.pressLike()) { 113 | setTimeout(this.run, generateRandomNumber(500, 900)); 114 | return; 115 | } 116 | 117 | logger('No profiles found. Waiting 4s'); 118 | setTimeout(this.run, generateRandomNumber(3000, 4000)); 119 | }; 120 | } 121 | 122 | export default Swiper; 123 | -------------------------------------------------------------------------------- /src/automations/Messenger.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get'; 2 | import keyBy from 'lodash/keyBy'; 3 | import { sendMessageToMatch, getMessagesForMatch, getMatches } from '../misc/api'; 4 | import { randomDelay, logger } from '../misc/helper'; 5 | import { getCheckboxValue, toggleCheckbox } from '../views/Sidebar'; 6 | 7 | class Messenger { 8 | selector = '.tinderAutopilotMessage'; 9 | 10 | newSelector = '.tinderAutopilotMessageNewOnly'; 11 | 12 | nextPageToken; 13 | 14 | isRunningMessage; 15 | 16 | allMatches = []; 17 | 18 | checkedMessage = 0; 19 | 20 | loopMatches = async () => { 21 | const response = await getMatches(getCheckboxValue(this.newSelector), this.nextPageToken); 22 | this.nextPageToken = get(response, 'data.next_page_token'); 23 | this.allMatches.push.apply(this.allMatches, get(response, 'data.matches', [])); 24 | }; 25 | 26 | start = () => { 27 | this.checkedMessage = 0; 28 | logger('Starting messages'); 29 | this.isRunningMessage = true; 30 | this.nextPageToken = true; 31 | this.runMessage(); 32 | }; 33 | 34 | stop = () => { 35 | setTimeout(() => { 36 | logger('Messaging stopped ⛔️'); 37 | this.isRunningMessage = false; 38 | toggleCheckbox(this.selector); 39 | }, 500); 40 | }; 41 | 42 | runMessage = async () => { 43 | await this.loopMatches(); 44 | while (this.nextPageToken) { 45 | logger(`Currently have ${this.allMatches.length} matches`); 46 | await this.loopMatches(); 47 | } 48 | 49 | logger(`Retrieved all match history: ${this.allMatches.length}`); 50 | 51 | // To start with old matches we can reverse the array 52 | // this.allMatches = this.allMatches.reverse(); 53 | 54 | logger(`Looking for matches we have not sent yet to`); 55 | this.sendMessagesTo(this.allMatches.reverse()); 56 | }; 57 | 58 | sendMessagesTo = async (r) => { 59 | const matchList = keyBy(r, 'id'); 60 | const pendingPromiseList = []; 61 | 62 | for (const matchID of Object.keys(matchList)) { 63 | await randomDelay(); 64 | if (!this.isRunningMessage) break; 65 | 66 | const match = matchList[matchID]; 67 | const messageToSend = get(document.getElementById('messageToSend'), 'value', '').replace( 68 | '{name}', 69 | get(match, 'person.name').toLowerCase() 70 | ); 71 | 72 | const messageToSendL = messageToSend 73 | .trim() 74 | .toLowerCase() 75 | .replace(/[^a-zA-Z0-9]+/g, '-') 76 | .replace('thanks', 'thank'); 77 | 78 | pendingPromiseList.push( 79 | getMessagesForMatch(match.id) 80 | .then((messageList) => { 81 | this.checkedMessage += 1; 82 | logger(`Checked ${this.checkedMessage}/${this.allMatches.length}`); 83 | return messageList ? !messageList.includes(messageToSendL) : false; 84 | }) 85 | .then((shouldSend) => { 86 | if (shouldSend) { 87 | sendMessageToMatch(match.id, { message: messageToSend }).then((b) => { 88 | if (get(b, 'sent_date')) { 89 | logger(`Message sent to ${get(match, 'person.name')}`); 90 | } 91 | }); 92 | } 93 | }) 94 | ); 95 | } 96 | 97 | if (pendingPromiseList.length === 0) { 98 | logger('No more matches to send message to'); 99 | this.stop(); 100 | } else { 101 | Promise.all(pendingPromiseList).then((r) => { 102 | logger('No more matches to send message to'); 103 | this.stop(); 104 | }); 105 | } 106 | }; 107 | } 108 | 109 | export default Messenger; -------------------------------------------------------------------------------- /src/views/Sidebar.js: -------------------------------------------------------------------------------- 1 | import { 2 | onToggle, 3 | offToggle, 4 | topBanner, 5 | autopilot, 6 | infoBanner, 7 | massMessage, 8 | loggerHeader, 9 | counterLogs, 10 | offToggleInner, 11 | onToggleInner 12 | } from './templates'; 13 | import Messenger from '../automations/Messenger'; 14 | import Swiper from '../automations/Swiper'; 15 | import HideUnanswered from '../automations/HideUnanswered'; 16 | import Anonymous from '../automations/Anonymous'; 17 | import { waitUntilElementExists } from '../misc/helper'; 18 | 19 | class Sidebar { 20 | constructor() { 21 | this.sidebar(); 22 | 23 | this.anonymous = new Anonymous(); 24 | this.hideUnanswered = new HideUnanswered(); 25 | this.swiper = new Swiper(); 26 | this.messenger = new Messenger(); 27 | 28 | this.events(); 29 | } 30 | 31 | insertBefore = (el, referenceNode) => { 32 | referenceNode.parentNode.insertBefore(el, referenceNode); 33 | }; 34 | 35 | sidebar = () => { 36 | const el = document.createElement('aside'); 37 | el.className = 'H(100%) Fld(c) Pos(r) Flxg(0) Fxs(0) Flxb(25%) Miw(325px) Maw(375px)'; 38 | el.style.cssText = 'background-color:#1f2937;z-index:9999999;'; 39 | el.innerHTML = infoBanner; 40 | this.insertBefore(el, document.querySelector('aside:first-of-type')); 41 | 42 | this.infoBanner = document.querySelector('#infoBanner'); 43 | 44 | this.infoBanner.innerHTML = 45 | ``; 52 | }; 53 | 54 | events = () => { 55 | // Auto unmatch 56 | waitUntilElementExists('img[alt="No Reason"]', () => { 57 | document.querySelector('ul li:last-of-type button').click(); 58 | document.querySelector('.modal-slide-up div button[type="button"]').click(); 59 | }); 60 | 61 | this.bindCheckbox(this.anonymous.selector, this.anonymous.start, this.anonymous.stop); 62 | 63 | this.bindCheckbox(this.messenger.newSelector); 64 | 65 | this.bindCheckbox(this.swiper.selector, this.swiper.start, this.swiper.stop); 66 | 67 | this.bindCheckbox(this.messenger.selector, this.messenger.start, this.messenger.stop); 68 | 69 | this.bindCheckbox( 70 | this.hideUnanswered.selector, 71 | this.hideUnanswered.start, 72 | this.hideUnanswered.stop 73 | ); 74 | 75 | document.getElementById('messageToSend').addEventListener('blur', (e) => { 76 | localStorage.setItem('TinderAutopilot/MessengerDefault', JSON.stringify(e.target.value)); 77 | }); 78 | }; 79 | 80 | bindCheckbox = (selector, start = false, stop = false) => { 81 | document.querySelector(selector).onclick = (e) => { 82 | e.preventDefault(); 83 | 84 | const isOn = getCheckboxValue(selector); 85 | toggleCheckbox(selector); 86 | if (isOn && stop) stop(); 87 | if (!isOn && start) start(); 88 | }; 89 | }; 90 | } 91 | 92 | const getCheckboxValue = (selector) => 93 | document.querySelector(`${selector} .toggleSwitch > div`).className === onToggle; 94 | 95 | const toggleCheckbox = (selector) => { 96 | const isOn = getCheckboxValue(selector); 97 | document.querySelector(`${selector} .toggleSwitch > div`).className = isOn ? offToggle : onToggle; 98 | document.querySelector(`${selector} .toggleSwitch > div > div`).className = isOn 99 | ? offToggleInner 100 | : onToggleInner; 101 | }; 102 | 103 | export { getCheckboxValue, toggleCheckbox }; 104 | 105 | export default Sidebar; 106 | -------------------------------------------------------------------------------- /src/misc/api.js: -------------------------------------------------------------------------------- 1 | import get from 'lodash/get'; 2 | 3 | const headers = { 4 | referrer: 'https://tinder.com/', 5 | referrerPolicy: 'origin', 6 | accept: 'application/json; charset=UTF-8', 7 | 'persistent-device-id': localStorage.getItem('TinderWeb/uuid'), 8 | platform: 'web', 9 | 'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') 10 | }; 11 | 12 | const defaultOptions = { 13 | headers, 14 | method: 'GET' 15 | }; 16 | 17 | const fetchResource = (url, body = false) => { 18 | return new Promise((resolve, reject) => { 19 | const options = defaultOptions; 20 | if (body) { 21 | options.headers['content-type'] = 'application/json'; 22 | options.body = JSON.stringify(body); 23 | options.method = 'POST'; 24 | } 25 | chrome.runtime.sendMessage({ url, options }, (messageResponse) => { 26 | const [response, error] = messageResponse; 27 | if (response === null) { 28 | reject(error); 29 | } else { 30 | // Use undefined on a 204 - No Content 31 | const body = response.body ? new Blob([response.body]) : undefined; 32 | resolve( 33 | new Response(body, { 34 | status: response.status, 35 | statusText: response.statusText 36 | }) 37 | ); 38 | } 39 | }); 40 | }) 41 | .then((response) => { 42 | return response.text(); 43 | }) 44 | .then((data) => { 45 | return data ? JSON.parse(data) : {}; 46 | }) 47 | .catch((error) => { 48 | console.log(error); 49 | }); 50 | }; 51 | 52 | const getMatches = async (newOnly, nextPageToken) => { 53 | return fetchResource( 54 | `https://api.gotinder.com/v2/matches?count=100&is_tinder_u=true&locale=en&message=${ 55 | newOnly ? 0 : 1 56 | }${typeof nextPageToken === 'string' ? `&page_token=${nextPageToken}` : ''}` 57 | ); 58 | }; 59 | 60 | const getMyProfile = () => 61 | fetchResource( 62 | `https://api.gotinder.com/v2/profile?locale=en&include=account%2Cboost%2Ccontact_cards%2Cemail_settings%2Cinstagram%2Clikes%2Cnotifications%2Cplus_control%2Cproducts%2Cpurchase%2Creadreceipts%2Cswipenote%2Cspotify%2Csuper_likes%2Ctinder_u%2Ctravel%2Ctutorials%2Cuser` 63 | ); 64 | 65 | const getMessagesForMatch = ({ id }) => 66 | fetchResource(`https://api.gotinder.com/v2/matches/${id}/messages?count=100`).then((data) => 67 | get(data, 'data.messages', []).map((r) => 68 | get(r, 'message', '') 69 | .trim() 70 | .toLowerCase() 71 | .replace(/[^a-zA-Z0-9]+/g, '-') 72 | .replace('thanks', 'thank') 73 | ) 74 | ); 75 | 76 | const getProfileData = () => { 77 | try { 78 | return JSON.parse(localStorage.getItem('TinderAutopilot/ProfileData')); 79 | } catch { 80 | return false; 81 | } 82 | }; 83 | 84 | const getSnapchatUsername = () => { 85 | const profile = getProfileData(); 86 | const snapAccount = get(profile, 'data.contact_cards.populated_cards', []).find( 87 | (a) => a.contact_type === 'snapchat' 88 | ); 89 | return snapAccount ? snapAccount.contact_id : false; 90 | }; 91 | 92 | // To send a snapchat username to a match id 93 | // sendMessageToMatch('1234', { type: 'snapchat' }); 94 | const sendMessageToMatch = (matchID, options) => { 95 | const user = getSnapchatUsername(); 96 | 97 | const body = options; 98 | if (options && options.type) { 99 | switch (options.type) { 100 | case 'snapchat': 101 | if (!user) throw new Error('No snapchat user exists'); 102 | body.message = user; 103 | body.type = 'contact_card'; 104 | body.contact_type = 'snapchat'; 105 | break; 106 | default: 107 | break; 108 | } 109 | } 110 | return fetchResource(`https://api.gotinder.com/user/matches/${matchID}?locale=en`, body); 111 | }; 112 | 113 | export { fetchResource, sendMessageToMatch, getMessagesForMatch, getMatches, getMyProfile }; 114 | -------------------------------------------------------------------------------- /src/views/templates.js: -------------------------------------------------------------------------------- 1 | let defaultMessage = `Hey {name}, this is an automated message to remind you of your upcoming "Netflix and Chill" appointment in the next week. To confirm your appointment text YES DADDY. To unsubscribe, please text WRONG HOLE. Standard text and bill rates do apply. Thanks for choosing Slide N Yo DMs`; 2 | 3 | const msg = JSON.parse(localStorage.getItem('TinderAutopilot/MessengerDefault')); 4 | if (msg) { 5 | defaultMessage = msg; 6 | } else { 7 | localStorage.setItem('TinderAutopilot/MessengerDefault', JSON.stringify(defaultMessage)); 8 | } 9 | 10 | const onToggle = `CenterAlign Bdrs(16px) W(50px) H(30px) Bd Pe(n) Trstf(eio) Trsdu($fast) Bdc($c-pink) Bg($c-pink)`; 11 | const onToggleInner = `Bdrs(50%) Bgc(#fff) Bd Sq(28px) Trstf(eio) Trsdu($fast) Bdc($c-pink) TranslateX(10px)`; 12 | 13 | const offToggle = `CenterAlign Bdrs(16px) W(50px) H(30px) Bd Pe(n) Trstf(eio) Trsdu($fast) Bdc($c-divider) Bgc($c-divider)`; 14 | const offToggleInner = `Bdrs(50%) Bgc(#fff) Bd Sq(28px) Trstf(eio) Trsdu($fast) Bdc($c-divider) TranslateX(-10px)`; 15 | 16 | const topBanner = ` 17 | 22 | `; 23 | 24 | const titleGenerator = (title) => 25 | `

${title}

`; 26 | 27 | const textboxGenerator = ({ className, placeholder, helpText, defaultValue }) => ` 28 |
29 | 37 |
38 | ${helpText && 39 | `
${helpText}
` 40 | } 41 | `; 42 | 43 | const checkboxGenerator = (className, label, helpText = '') => ` 44 | 61 | ${helpText && 62 | `
${helpText}
` 63 | } 64 | `; 65 | 66 | const autopilot = ` 67 |
68 | ${titleGenerator('Main Settings')} 69 | ${checkboxGenerator( 70 | 'tinderAutopilot', 71 | 'Auto like', 72 | 'Begin automatically swiping right on all profiles.' 73 | )} 74 | ${checkboxGenerator( 75 | 'tinderAutopilotHideMine', 76 | 'Only show unanswered messages', 77 | 'Useful if you just sent an auto message to a ton of people and only want to see the ones that responded.' 78 | )} 79 | ${checkboxGenerator( 80 | 'tinderAutopilotAnonymous', 81 | 'Anonymous Mode', 82 | 'Hide profile pictures so you can take screenshots.' 83 | )} 84 |
85 | `; 86 | 87 | const massMessage = ` 88 |
89 | ${titleGenerator('Messaging Settings')} 90 | ${checkboxGenerator('tinderAutopilotMessage', 'Auto message')} 91 | ${checkboxGenerator('tinderAutopilotMessageNewOnly', 'New matches only')} 92 | ${textboxGenerator({ 93 | helpText: 'The message to send to matches.', 94 | placeholder: 'Your message to send', 95 | className: 'messageToSend', 96 | defaultValue: defaultMessage 97 | })} 98 |
99 | `; 100 | 101 | const loggerHeader = `
${titleGenerator('Activity')}
`; 102 | 103 | const counterLogs = (likeCount, matchCount) => ` 104 |
105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 |
113 |
114 |

${likeCount}
Liked

115 |
116 |
117 |
118 |
119 | 120 | 121 | 122 | 123 | 124 |
125 |
126 |

${matchCount}
Matched

127 |
128 |
129 |
130 | `; 131 | 132 | const infoBanner = ``; 133 | 134 | export { 135 | topBanner, 136 | autopilot, 137 | infoBanner, 138 | massMessage, 139 | loggerHeader, 140 | counterLogs, 141 | offToggle, 142 | offToggleInner, 143 | onToggle, 144 | onToggleInner 145 | }; 146 | --------------------------------------------------------------------------------