Greasy Fork is available in English.

Tinder Deblur

Simple script using the official Tinder API to get clean photos of the users who liked you

// ==UserScript==
// @name        Tinder Deblur
// @namespace   Violentmonkey Scripts
// @match       https://tinder.com/*
// @grant       none
// @version     3.0
// @author      Tajnymag
// @description Simple script using the official Tinder API to get clean photos of the users who liked you
// ==/UserScript==

// enable type checking
// @ts-check
// @filename: types/tampermonkey.d.ts

class UserCacheItem {
	/**
	 * @param {string} userId
	 * @param {object} user
	 */
	constructor(userId, user) {
		this.userId = userId;
		this.user = user;

		this.updater = null;
		this.photoIndex = 0;
	}

	/**
	 * @returns {string}
	 */
	getNextPhoto() {
		this.photoIndex = (this.photoIndex + 1) % this.user.photos.length;
		return this.user.photos[this.photoIndex].url;
	}

	/**
	 * @returns {number}
	 */
	getAge() {
		if (!this.user.birth_date) return 0;

		const currentYear = new Date().getFullYear();
		const birthDate = Date.parse(this.user.birth_date);
		const birthYear = new Date(birthDate).getFullYear();

		return currentYear - birthYear;
	}

	/** @param {number} interval */
	setUpdater(interval) {
		this.clearUpdater();
		this.interval = interval;
	}

	clearUpdater() {
		if (!this.updater) return;
		clearInterval(this.updater);
	}
}

class UserCache {
	constructor() {
		/** @type {Map<string, UserCacheItem>} */
		this.cache = new Map();
	}

	/**
	 * @param {string} userId
	 * @param {object} user
	 */
	add(userId, user) {
		this.delete(userId);
		this.cache.set(userId, new UserCacheItem(userId, user));
	}

	/**
	 * @param {string} userId
	 */
	has(userId) {
		return this.cache.has(userId);
	}

	/**
	 * @param {string} userId
	 * @returns UserCacheItem | undefined
	 */
	get(userId) {
		return this.cache.get(userId);
	}

	/**
	 * @param {string} userId
	 */
	delete(userId) {
		const existingUser = this.cache.get(userId);

		if (!existingUser) return;

		existingUser.clearUpdater();
		this.cache.delete(userId);
	}

	clear() {
		for (const userItem of this.cache.values()) {
			userItem.clearUpdater();
			this.cache.delete(userItem.userId);
		}
	}
}

/**
 * Holds a persistent cache of fetched users and intervals for updating their photos
 */
const cache = new UserCache();

/**
 * Core logic of the script
 */
async function unblur() {
	const teasers = await fetchTeasers();
	/** @type {NodeListOf<HTMLElement>} */
	const teaserEls = document.querySelectorAll('.Expand.enterAnimationContainer > div:nth-child(1)');

	for (let i = 0; i < teaserEls.length; ++i) {
		const teaser = teasers[i];
		const teaserEl = teaserEls[i];
		const teaserImage = teaser.user.photos[0].url;

		if (teaserImage.includes('unknown')) continue;

		if (teaserImage.includes('images-ssl')) {
			const userId = teaserImage.slice(32, 56);

			if (cache.has(userId)) continue;

			try {
				const user = await fetchUser(userId);

				if (!user) {
					console.error(`Could not load user '${userId}'`);
					continue;
				}

				// save user to cache
				cache.add(userId, user);
				const userItem = cache.get(userId);

				// log user info + photos
				console.debug(`${user.name} (${user.bio})`);

				// update info container
				const infoContainer = teaserEl.parentNode?.lastElementChild;

				if (!infoContainer) {
					console.error(`Could not find info container for '${userId}'`);
					continue;
				}

				infoContainer.outerHTML = `
					<div class="Pos(a) Start(0) End(0) TranslateZ(0) Pe(n) H(30%) B(0)" style="background-image: linear-gradient(to top, rgb(0, 0, 0) 0%, rgba(255, 255, 255, 0) 100%);">
						<div style="opacity: 0; transition: opacity 0.5s ease-out;" class="like-user-info Pos(a) D(f) Jc(sb) C($c-ds-text-primary-overlay) Ta(start) W(100%) Ai(fe) B(0) P(8px)--xs P(16px) P(20px)--l Cur(p) focus-button-style" tabindex="0">
							<div class="Tsh($tsh-s) D(f) Fx($flx1) Fxd(c) Miw(0)">
								<div class="Pos(a) Fz($l) B(0) Trsdu($fast) Maw(80%) D(f) Fxd(c) like-user-name">
									<div class="D(f) Ai(c) Miw(0)">
										<div class="Ov(h) Ws(nw) As(b) Ell">
											<span class="Typs(display-2-strong)" itemprop="name">${user.name}</span>
										</div>
										<span class="As(b) Pend(8px)"></span>
										<span class="As(b)" itemprop="age">${userItem.getAge()}</span>
									</div>
								</div>
							<div>
								<span class="like-user-bio" style="-webkit-box-orient: vertical; display: -webkit-box; -webkit-line-clamp: 3; max-height: 63px; overflow-y: hidden; text-overflow: ellipsis;">${user.bio}</span>
							</div>
						</div>
					</div>
				`;

				// switch images automatically
				userItem.setUpdater(
					setInterval(() => {
						teaserEl.style.backgroundImage = `url(${userItem.getNextPhoto()})`;
					}, 2_500)
				);
			} catch (err) {
				console.error(`Failed to load user '${userId}'`);
				console.error(err);
			}
		}
	}

	// update user infos
	setTimeout(() => {
		/** @type {NodeListOf<HTMLElement>} */
		const infoContainerEls = document.querySelectorAll('.like-user-info');

		for (let infoContainerEl of infoContainerEls) {
			/** @type {HTMLElement | null} */
			const userNameEl = infoContainerEl.querySelector('.like-user-name');
			/** @type {HTMLElement | null} */
			const userBioEl = infoContainerEl.querySelector('.like-user-bio');

			if (!userNameEl || !userBioEl) continue;

			const userBioElHeight = userBioEl.getBoundingClientRect().height;

			userNameEl.style.transform = `translateY(-${userBioElHeight + 20}px)`;
			infoContainerEl.style.opacity = `1`;
		}
	}, 2_500);
}

/**
 * Fetches teaser cards using Tinder API
 * @returns {Promise<any>}
 */
async function fetchTeasers() {
	return fetch('https://api.gotinder.com/v2/fast-match/teasers', {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
			platform: 'android',
		},
	})
		.then((res) => res.json())
		.then((res) => res.data.results);
}

/**
 * Fetches information about specific user using Tinder API
 * @param {string} id
 * @returns {Promise<any>}
 */
async function fetchUser(id) {
	return fetch(`https://api.gotinder.com/user/${id}`, {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
			platform: 'android',
		},
	})
		.then((res) => res.json())
		.then((res) => res.results);
}

/**
 * Awaits the first event of the specified listener
 * @param {EventTarget} target
 * @param {string} eventType
 * @returns {Promise<void>}
 */
async function once(target, eventType) {
	return new Promise((resolve) => {
		const resolver = () => {
			target.removeEventListener(eventType, resolver);
			resolve();
		};
		target.addEventListener(eventType, resolver);
	});
}

/**
 * Awaits until the main app element is found in DOM and returns it
 * @returns {Promise<HTMLElement>}
 */
async function waitForApp() {
	const getAppEl = (parent) => parent.querySelector('.App');
	let appEl = getAppEl(document.body);

	if (appEl) return appEl;

	return new Promise((resolve) => {
		new MutationObserver((_, me) => {
			appEl = getAppEl(document.body);

			if (appEl) {
				me.disconnect();
				resolve(appEl);
			}
		}).observe(document.body, { subtree: true, childList: true });
	});
}

async function main() {
	// check if running as a userscript
	if (typeof GM_info === 'undefined') {
		console.warn(
			'[TINDER DEBLUR]: The only supported way of running this script is through a userscript management browser addons like Violentmonkey, Tampermonkey or Greasemonkey!'
		);
		console.warn(
			'[TINDER DEBLUR]: Script was not terminated, but you should really look into the correct way of running it.'
		);
	}

	// wait for a full page load
	await once(window, 'load');
	const appEl = await waitForApp();

	const pageCheckCallback = () => {
		if (['/app/likes-you', '/app/gold-home'].includes(location.pathname)) {
			console.debug('[TINDER DEBLUR]: Deblurring likes');
			unblur();
		} else {
			// clear the cache when not on likes page anymore
			cache.clear();
		}
	};

	// setup navigation observer
	const observer = new MutationObserver(pageCheckCallback);
	observer.observe(appEl, { subtree: true, childList: true });

	// setup loop based observer (every 5s)
	setInterval(pageCheckCallback, 5_000);
}

main().catch(console.error);