Files
redux-scraper/user_scripts/furecs.user.js
2023-04-26 22:05:21 -07:00

230 lines
6.6 KiB
JavaScript

// ==UserScript==
// @name FuRecs
// @namespace https://twitter.com/DeltaNoises
// @version 1.3
// @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
// @updateURL https://refurrer.com/us/furecs.user.js
// @downloadURL https://refurrer.com/us/furecs.user.js
// ==/UserScript==
"use strict";
const API_HOST = "https://refurrer.com";
function urlNameFromUserHref(href) {
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 fa() {
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 = null;
const loggedInLink = document.querySelector(
".mobile-nav-content-container h2 a"
);
if (loggedInLink) {
loggedInUrlName = urlNameFromUserHref(loggedInLink.href);
}
function buildRecommendedContainer() {
let leftColumn = document.querySelector(
".userpage-layout-left-col-content"
);
if (!leftColumn) {
console.log("didn't find left column container, bailing");
return [];
}
let section = document.createElement("section");
section.classList = "userpage-left-column gallery_container";
section.innerHTML = `
<div class="userpage-section-left">
<div class="section-header" style='display:flex;align-items:center'>
<h2 style='flex-grow:1'>Similar Users</h2>
</div>
<div class="section-body">
</div>
</div>`;
leftColumn.prepend(section);
return [
section.querySelector(".section-body"),
section.querySelector(".section-header"),
];
}
let [container, header] = buildRecommendedContainer();
if (!container) {
return;
}
console.log(`getting recommended follows for ${urlName}`);
function addApiTokenSetForm() {
const form = document.createElement("form");
form.innerHTML = `
<input style='height:32px;min-width:20em' required type=text placeholder="API Token" name="api_token" />
<input class='button' type=submit value="Save" />
`;
header.appendChild(form);
const input = form.querySelector("input[type=text]");
form.onsubmit = async () => {
apiToken = input.value;
await GM.setValue("apiToken", apiToken);
header.removeChild(form);
addApiTokenClearButton();
run();
};
}
function addApiTokenClearButton() {
const button = document.createElement("button");
const apiTokenShort =
apiToken.length > 10 ? apiToken.slice(0, 7) + "..." : apiToken;
button.innerHTML = `Clear Token (${apiTokenShort})`;
header.appendChild(button);
button.onclick = async () => {
apiToken = null;
await GM.setValue("apiToken", null);
header.removeChild(button);
addApiTokenSetForm();
};
}
let apiToken = await GM.getValue("apiToken", null);
if (apiToken == null) {
container.innerHTML = `<h2 class='aligncenter'>Please set API token</h2>`;
addApiTokenSetForm();
} else {
addApiTokenClearButton();
run();
}
function run() {
container.innerHTML = `<h2 class='aligncenter'>(Loading...)</h2>`;
let url = `${API_HOST}/api/fa/similar_users`;
url += `?url_name=${encodeURIComponent(urlName)}`;
if (loggedInUrlName) {
url += `&exclude_url_name=${encodeURIComponent(loggedInUrlName)}`;
}
url += `&api_token=${encodeURIComponent(apiToken)}`;
function setContainerError(error) {
container.innerHTML = `Error: <pre>${error}</pre>`;
}
GM_xmlhttpRequest({
url,
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
onload: (response) => {
const json_error_codes = [403, 404, 500];
if (response.status == 200) {
let json = JSON.parse(response.response);
populateRecommendations(json);
} else if (json_error_codes.includes(response.status)) {
let json = JSON.parse(response.response);
setContainerError(json.error);
} else {
setContainerError(response.response);
}
},
});
}
function populateRecommendations(recommendations) {
if (recommendations.not_followed) {
container.innerHTML = buildTwoColumns(recommendations);
} else {
container.innerHTML = buildOneColumn(recommendations.all);
}
}
function buildTwoColumns({ all, not_followed }) {
let content = ``;
content += `<table>`;
content += ` <tr>`;
content += ` <th>Similar to ${urlName}</th>`;
content += ` <th>Users you don't follow</th>`;
content += ` </tr>`;
content += ` <tr>`;
content += ` <td>`;
content += ` ` + buildOneColumn(all);
content += ` </td>`;
content += ` <td>`;
if (not_followed.error != null) {
content +=
` Error getting recommended users ` +
`you don't already follow: ${not_followed.error}`;
} else {
content += ` ` + buildOneColumn(not_followed);
}
content += ` </td>`;
content += ` </tr>`;
content += `</table>`;
return content;
}
function buildOneColumn(userList) {
let content = ``;
content += '<ol class="user-submitted-links">';
userList.forEach(({ name, url, profile_thumb_url }) => {
content += `<li>`;
content += `<a
href="${url}"
style="display: flex; align-items: center; border: dotted 1px rgba(255,255,255,0.5); padding: 4px"
>`;
if (profile_thumb_url) {
content += `<img
alt="${name} thumbnail"
title="${name}"
src="${profile_thumb_url}"
style="max-height: 40px; padding-right: 10px"
/>`;
}
content += `<span>${name}</span>`;
content += `</a>`;
content += "</li>";
});
content += "</ol>";
return content;
}
}
(async function () {
if (window.location.hostname == "www.furaffinity.net") {
await fa();
} else {
console.log("unhandled domain ", window.location.hostname);
}
})();