230 lines
6.6 KiB
JavaScript
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);
|
|
}
|
|
})();
|