diff --git a/src/libs/fetchPronouns.js b/src/libs/fetchPronouns.js index 7a9ccab..003816d 100644 --- a/src/libs/fetchPronouns.js +++ b/src/libs/fetchPronouns.js @@ -60,7 +60,7 @@ export async function fetchPronouns(dataID, accountName, type) { status = await fetchStatus(dataID); } - let pronouns = extractFromStatus(status); + let pronouns = await extractFromStatus(status); if (!pronouns) { pronouns = "null"; //TODO: if no field check bio diff --git a/src/libs/pronouns.js b/src/libs/pronouns.js index 7f2b099..dad45f2 100644 --- a/src/libs/pronouns.js +++ b/src/libs/pronouns.js @@ -2,7 +2,7 @@ import sanitizeHtml from "sanitize-html"; const fieldMatchers = [/pro.*nouns?/i, "pronomen"]; const knownPronounUrls = [ - /pronouns\.page\/([\w/]+)/, + /pronouns\.page\/:?([\w/@]+)/, /pronouns\.within\.lgbt\/([\w/]+)/, /pronouns\.cc\/pronouns\/([\w/]+)/, ]; @@ -14,9 +14,9 @@ const knownPronounUrls = [ * If found, it sanitizes and returns the value of said field. * * @param {any} status - * @returns {string|null} Author pronouns if found. Otherwise returns null. + * @returns {Promise} Author pronouns if found. Otherwise returns null. */ -export function extractFromStatus(status) { +export async function extractFromStatus(status) { // get account from status and pull out fields const account = status.account; const fields = account.fields; @@ -42,6 +42,62 @@ export function extractFromStatus(status) { text = pronounsRaw.match(knownUrlRe)[1]; } + // Right now, only the pronoun.page regex matches the @usernames. + if (text.charAt(0) === "@") { + text = await queryPronounsFromPronounsPage(text.substring(1)); + } + if (!text) return null; return text; } + +/** + * Queries the pronouns from the pronouns.page API. + * @param {string} username The username of the person. + * @returns {Promise} The pronouns that have set the "yes" opinion. + */ +async function queryPronounsFromPronounsPage(username) { + // Example page: https://en.pronouns.page/api/profile/get/andrea?version=2 + const resp = await fetch(`https://en.pronouns.page/api/profile/get/${username}?version=2`); + if (resp.status >= 400) { + return null; + } + + const { profiles } = await resp.json(); + if (!profiles) return null; + + // Unfortunately, pronouns.page does not return a 404 if a profile does not exist, but an empty profiles object. :clown_face: + if (!Object.keys(profiles).length) return null; + + let pronouns; + // Query the pronouns in the following language order: + // 1. The mastodon interface language + // 2. The spoken languages according to the user + // 3. The english language. + const languages = [document.documentElement.lang, ...window.navigator.languages, "en"]; + for (const lang of languages) { + if (lang in profiles) { + pronouns = profiles[lang].pronouns; + break; + } + } + + // If we don't have a value yet, just take the first profile. + if (!pronouns) pronouns = profiles[0].pronouns; + + let val = pronouns.find((x) => x.opinion === "yes").value; + val = sanitizePronounPageValue(val); + return val; +} + +/** + * @param {string} val + */ +function sanitizePronounPageValue(val) { + if (!val.startsWith("https://")) return val; + + val = val.replace(/https?:\/\/.+\.pronouns\.page\/:?/, ""); + + if (val === "no-pronouns") val = "no pronouns"; + return val; +} diff --git a/src/manifest.json b/src/manifest.json index e9e04d8..171e378 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -10,7 +10,7 @@ "description": "puts pronouns next to usernames on mastodon", "homepage_url": "https://github.com/ItsVipra/ProToots", - "permissions": ["storage"], + "permissions": ["storage", "https://en.pronouns.page/api/profile/get/*"], "browser_action": { "default_icon": "icons/icon small_size/icon small_size.png", diff --git a/tests/extractPronouns.spec.js b/tests/extractPronouns.spec.js index fbb21ff..0930064 100644 --- a/tests/extractPronouns.spec.js +++ b/tests/extractPronouns.spec.js @@ -13,8 +13,8 @@ const validFields = [ ]; for (const field of validFields) { - extract(`${field} is extracted`, () => { - const result = pronouns.extractFromStatus({ + extract(`${field} is extracted`, async () => { + const result = await pronouns.extractFromStatus({ account: { fields: [{ name: field, value: "pro/nouns" }], }, @@ -30,12 +30,19 @@ valueExtractionSuite.before(() => { global.window = { // @ts-ignore navigator: { - language: "en", + languages: ["en"], + }, + }; + global.document = { + // @ts-ignore + documentElement: { + lang: "de", }, }; }); valueExtractionSuite.after(() => { global.window = undefined; + global.document = undefined; }); const valueExtractionTests = [ ["she/her", "she/her"], // exact match @@ -44,7 +51,13 @@ const valueExtractionTests = [ ["https://en.pronouns.page/they/them", "they/them"], // plain-text "URLs" ["pronouns.page/they/them", "they/them"], // plain-text "URLs" without scheme [``, "they/them"], // HTML-formatted URLs - [``, null], // pronoun pages with usernames + [``, "she/her"], // pronoun pages with usernames + [ + ``, + null, + ], // 404 errors + [``, "Katze"], // custom pronouns + [``, "Katze"], // custom pronouns in profile ]; for (const [input, expects] of valueExtractionTests) { valueExtractionSuite(input, async () => {