// ==UserScript== // @name FuRecs // @namespace https://twitter.com/DeltaNoises // @version 1.4 // @description FurAffinity User Recommender // @author https://twitter.com/DeltaNoises // @match https://www.furaffinity.net/user/* // @icon https://www.google.com/s2/favicons?sz=64&domain=furaffinity.net // @grant GM_xmlhttpRequest // @grant GM.getValue // @grant GM.setValue // @connect refurrer.com // @connect localhost:3001 // @connect localhost // @updateURL https://refurrer.com/us/furecs.user.js // @downloadURL https://refurrer.com/us/furecs.user.js // ==/UserScript== 'use strict'; interface SimilarUserList { name: string; url_name: string; profile_thumb_url: string; external_url: string; refurrer_url: string; } type SimilarUsersErrorType = | 'recs_not_computed' | 'user_not_found' | 'exclude_user_not_found' | 'exclude_user_not_scanned'; type SimilarUsersResponse = | { all: SimilarUserList[]; not_followed: SimilarUserList[] | null; } | { error_type: SimilarUsersErrorType; error: string; }; function urlNameFromUserHref(href: string) { const url = new URL(href); if (url.host != 'www.furaffinity.net') { return null; } const userPageRegex = /^\/(user|gallery|scraps|favorites|journals)\/.+/; const match = url.pathname.match(userPageRegex); if (!match) { return null; } return url.pathname.split('/')[2]; } async function faUserRecs(): Promise { const HOST = 'https://refurrer.com'; if (!window.location.pathname.startsWith('/user/')) { return null; } const urlName = urlNameFromUserHref(window.location.href); if (!urlName) { return; } // if the user is logged in, get their user account, so we can // also provide a list of recommendations that don't overlap with their // current watchlist let loggedInUrlName: string | null = null; const loggedInLink = document.querySelector( '.mobile-nav-content-container h2 a', ) as HTMLAnchorElement | null; if (loggedInLink) { loggedInUrlName = urlNameFromUserHref(loggedInLink.href); } function buildRecommendedContainer(): HTMLElement | null { let leftColumn = document.querySelector( '.userpage-layout-left-col-content', ) as HTMLElement | null; if (!leftColumn) { console.error("didn't find left column container, bailing"); return null; } let section = document.createElement('section'); section.classList.add('userpage-left-column'); section.classList.add('gallery_container'); section.innerHTML = `

Similar Users

`; leftColumn.prepend(section); return section.querySelector('.section-body'); } const container = buildRecommendedContainer(); if (!container) { console.error('failed to build recommended container, bailing'); return; } function setContainerError(error: string) { container.innerHTML = `Error:
${error}
`; } async function requestSimilarUsers(): Promise { return new Promise((resolve, reject) => { container.innerHTML = `

(Loading...)

`; let url = `${HOST}/api/fa/similar_users`; url += `?url_name=${encodeURIComponent(urlName)}`; if (loggedInUrlName) { url += `&exclude_url_name=${encodeURIComponent(loggedInUrlName)}`; } GM_xmlhttpRequest({ url, method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, onabort: () => { reject(new Error('request aborted')); }, ontimeout: () => { reject(new Error('request timed out')); }, onerror: (response) => { reject(new Error('server error: ' + response.responseText)); }, onload: (response) => { const json_error_codes = [403, 404, 500]; if (response.status == 200) { let json = JSON.parse(response.responseText); resolve(json); } else if (json_error_codes.includes(response.status)) { let json = JSON.parse(response.responseText); reject(new Error(json.error)); } else { reject(new Error(response.responseText)); } }, }); }); } function populateRecommendations(recommendations: SimilarUsersResponse) { if ('error' in recommendations) { setContainerError(recommendations.error); } else { container.innerHTML = buildTwoColumns({ all: recommendations.all, not_followed: recommendations.not_followed, }); } } function buildTwoColumns({ all, not_followed, }: { all: SimilarUserList[]; not_followed: SimilarUserList[] | null; }) { const width = not_followed ? '50%' : '100%'; let content = ``; content += ``; content += ` `; content += ` `; if (not_followed) { content += ` `; } content += ` `; content += ` `; content += ` `; if (not_followed) { content += ` `; } content += ` `; content += `
Similar to ${urlName}Similar users you don't follow
`; content += ` ` + buildOneColumn(all); content += ` `; content += ` ` + buildOneColumn(not_followed); content += `
`; return content; } function buildOneColumn(userList: SimilarUserList[]) { let content = ``; content += ''; return content; } await requestSimilarUsers() .then(populateRecommendations) .catch((error) => { setContainerError(error.message); }); } (async function () { if (window.location.hostname == 'www.furaffinity.net') { await faUserRecs(); } else { console.log('unhandled domain ', window.location.hostname); } })();