├── .firefox-profile └── .gitkeep ├── .node-version ├── src ├── libs │ ├── platforms │ │ └── mastodon.js │ ├── logging.js │ ├── settings.js │ ├── caching.js │ ├── protootshelpers.js │ ├── domhelpers.js │ ├── fetchPronouns.js │ └── pronouns.js ├── icons │ ├── icon full_size │ │ ├── icon full_size.png │ │ ├── icon full_size.html │ │ └── icon full_size.css │ └── icon small_size │ │ ├── icon small_size.png │ │ ├── icon small_size.html │ │ ├── icon small_size.css │ │ └── icon small_size.svg ├── manifest.json ├── options │ ├── options.js │ ├── options.html │ └── options.css ├── styles │ └── proplate.css └── content_scripts │ └── protoots.js ├── .vscode ├── extensions.json └── settings.json ├── documentation ├── example_screenshot.png └── get-the-addon-178x60px.png ├── scripts ├── build.mjs ├── watch.mjs └── shared.mjs ├── jsconfig.json ├── .eslintrc.yaml ├── .github └── workflows │ ├── publish-extension.yml │ ├── codequality.yml │ └── prepare-release.yml ├── package.json ├── .gitignore ├── README.md ├── tests └── extractPronouns.spec.js └── LICENSE.md /.firefox-profile/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v18.16.0 2 | -------------------------------------------------------------------------------- /src/libs/platforms/mastodon.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /documentation/example_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsVipra/ProToots/HEAD/documentation/example_screenshot.png -------------------------------------------------------------------------------- /documentation/get-the-addon-178x60px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsVipra/ProToots/HEAD/documentation/get-the-addon-178x60px.png -------------------------------------------------------------------------------- /src/icons/icon full_size/icon full_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsVipra/ProToots/HEAD/src/icons/icon full_size/icon full_size.png -------------------------------------------------------------------------------- /src/icons/icon small_size/icon small_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsVipra/ProToots/HEAD/src/icons/icon small_size/icon small_size.png -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild"; 2 | import { defaultBuildOptions } from "./shared.mjs"; 3 | 4 | await esbuild.build({ 5 | ...defaultBuildOptions, 6 | }); 7 | -------------------------------------------------------------------------------- /scripts/watch.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild"; 2 | import { defaultBuildOptions } from "./shared.mjs"; 3 | let ctx = await esbuild.context(defaultBuildOptions); 4 | await ctx.watch(); 5 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "checkJs": true, 6 | "moduleResolution": "nodenext" 7 | }, 8 | "include": ["src/**/*.js", "tests/**/*.js"] 9 | } 10 | -------------------------------------------------------------------------------- /src/icons/icon full_size/icon full_size.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |
](https://addons.mozilla.org/en-US/firefox/addon/protoots/)
14 |
15 | Alternatively you can download an unsigned version from the [releases page](https://github.com/ItsVipra/Protoots/releases).
16 |
17 | ---
18 |
19 | ## Known issues
20 |
21 | - None! It's perfect!
22 | - no but seriously, please submit any bugs you find as an issue :3
23 |
24 | Please also take a look at the FAQ below and the [issue list](https://github.com/ItsVipra/ProToots/issues).
25 |
26 | ---
27 |
28 | ## FAQ
29 |
30 | ### Why does ProToots need permission for all websites?
31 |
32 | > The addon needs to determine whether or not the site you are currently browsing is a Mastodon server. For that to work, it requires access to all sites. Otherwise, each existing Mastodon server would have to be explicitly added.
33 |
34 | ### Why can't I see any ProPlates?
35 |
36 | > It is likely your instance is not supported. This is because forks of Mastodon all work slightly differently and we cannot account for every version out there.
37 | > If ProToots isn't working on your instance please tell your admins to contact us here on Github.
38 |
39 | ### ProPlates don't have a background/low contrast on my instance.
40 |
41 | > Mastodon does not provide set variables for element colors, so we have to adjust the plate styling to each theme. If they're not displaying correctly please tell your admins to [follow these steps to style ProPlates](#how-do-i-style-proplates-to-correctly-display-on-my-themes).
42 |
43 | ### Somebody has added/changed pronouns, why is ProToots still showing no/their old pronouns?
44 |
45 | > In order to strain your instance less pronouns are cached for 24h, so it might take up to a day to see this change reflected.
46 | > Alternatively you can simply hit the "Reset cache" button in the addon settings.
47 |
48 | ### Why does the ProPlate just show a link?
49 |
50 | > When an author only provides their pronouns as a pronouns.page link we display that instead. In the future we'll be able to extract pronouns from the given link. (See [#7](https://github.com/ItsVipra/ProToots/issues/7))
51 |
52 | ---
53 |
54 | ## Instance admin info
55 |
56 | ### Protoots aren't working on my instance
57 |
58 | > Currently ProToots only looks for specific classes and IDs. If your instance has changed the name of those, ProToots will not find them.
59 | > Especially the **parent div with id 'Mastodon'** is important, since without that no other code will run.
60 | > Please open an issue with your server name and info on which names you've changed, so we can add support for your instance.
61 | > We're also working on a way to more easily support many different types of fedi software, such as Misskey or Akkoma. See [#12](https://github.com/ItsVipra/ProToots/issues/12)
62 |
63 | ### How do I style ProPlates to correctly display on my themes?
64 |
65 | > You can set their background-color and color attribute for each theme.
66 | > To do this simply add some CSS to your server. [Here's how.](https://fedi.tips/customising-your-mastodon-servers-appearance/)
67 | > See [our default styles](/src/styles/proplate.css) for reference.
68 |
69 | ---
70 |
71 | ## Developer setup
72 |
73 | - Clone the repository
74 | - Install the required dependencies using `npm install`
75 | - Start the development workflow with `npm start`
76 | - Build with `npm run package`
77 | - Mess around with with [protoots.js](/src/content_scripts/protoots.js)
78 | - Trans rights!
79 |
--------------------------------------------------------------------------------
/tests/extractPronouns.spec.js:
--------------------------------------------------------------------------------
1 | import { suite } from "uvu";
2 | import * as assert from "uvu/assert";
3 | import * as pronouns from "../src/libs/pronouns.js";
4 |
5 | const extract = suite("field extraction");
6 | const validFields = [
7 | "pronoun",
8 | "pronouns",
9 | "PRONOUNS",
10 | "professional nouns",
11 | "pronomen",
12 | "Pronouns / Pronomen",
13 | "Pronomen (DE)",
14 | "Pronouns (EN)",
15 | "i go by",
16 | "go by",
17 | ];
18 | const invalidFields = ["pronounciation", "pronomenverwaltung"];
19 |
20 | for (const field of validFields) {
21 | extract(`${field} is extracted`, async () => {
22 | const result = await pronouns.extractFromStatus({
23 | account: {
24 | fields: [{ name: field, value: "pro/nouns" }],
25 | },
26 | });
27 | assert.equal("pro/nouns", result);
28 | });
29 | }
30 |
31 | for (const field of invalidFields) {
32 | extract(`${field} is not extracted`, async () => {
33 | const result = await pronouns.extractFromStatus({
34 | account: {
35 | fields: [{ name: field, value: "pro/nouns" }],
36 | },
37 | });
38 | assert.equal(result, null);
39 | });
40 | }
41 |
42 | extract.run();
43 |
44 | const valueExtractionSuite = suite("value extraction");
45 | valueExtractionSuite.before(() => {
46 | global.window = {
47 | // @ts-ignore
48 | navigator: {
49 | languages: ["en"],
50 | },
51 | };
52 | global.document = {
53 | // @ts-ignore
54 | documentElement: {
55 | lang: "de",
56 | },
57 | };
58 | });
59 | valueExtractionSuite.after(() => {
60 | global.window = undefined;
61 | global.document = undefined;
62 | });
63 | const valueExtractionTests = [
64 | ["she/her", "she/her"], // exact match
65 | ["they and them", "they and them"], // exact match with multiple words
66 | ["they/them (https://pronouns.page/they/them)", "they/them"], // plain-text "URL" with additional text
67 | ["https://en.pronouns.page/they/them", "they/them"], // plain-text "URLs"
68 | ["pronouns.page/they/them", "they/them"], // plain-text "URLs" without scheme
69 | [``, "they/them"], // HTML-formatted URLs
70 | [``, "she/her"], // pronoun pages with usernames
71 | [
72 | ``,
73 | null,
74 | ], // 404 errors
75 | [``, "Katze"], // custom pronouns
76 | [``, "Katze/Katze's"], // custom pronouns in profile
77 | [`:theythem:`, null], // emojis shortcodes used for pronouns
78 | [
79 | // This is an actual example from a Mastodon field, with example.com redirecting to pronouns.page.
80 | `dey/denen, es/ihm - https://example.com`,
81 | "dey/denen, es/ihm",
82 | ],
83 | ["https://en.pronouns.page/it", "it/its"], // single-word pronoun pages
84 | ];
85 | for (const [input, expects] of valueExtractionTests) {
86 | valueExtractionSuite(input, async () => {
87 | const result = await pronouns.extractFromStatus({
88 | account: {
89 | fields: [{ name: "pronouns", value: input }],
90 | },
91 | });
92 | assert.equal(result, expects);
93 | });
94 | }
95 |
96 | valueExtractionSuite.run();
97 |
98 | const bioExtractSuite = suite("bio extraction");
99 | const bioExtractTests = [
100 | ["I'm cute and my pronouns are she/her", "she/her"], // exact match
101 | ["my pronouns are helicopter/joke", null], // not on allowlist
102 | ["pronouns: uwu/owo", "uwu/owo"], // followed by pronoun pattern
103 | ["pronouns: any", "any"], // followed by pronoun pattern
104 | ["I'm cute af (she / they)", "she/they"], // with whitespace between pronouns
105 | ["pronouns: any/all", "any/all"], // any pronouns
106 | ["any pronouns", "any pronouns"], // any pronouns
107 | ["He/Him", "He/Him"], //capitalised pronouns
108 | ];
109 | for (const [input, expects] of bioExtractTests) {
110 | bioExtractSuite(input, async () => {
111 | const result = await pronouns.extractFromStatus({
112 | account: { note: input },
113 | });
114 | assert.equal(result, expects);
115 | });
116 | }
117 |
118 | bioExtractSuite.run();
119 |
--------------------------------------------------------------------------------
/src/libs/fetchPronouns.js:
--------------------------------------------------------------------------------
1 | import { debug, error, info, log, warn } from "./logging";
2 | import { cachePronouns, getPronouns } from "./caching";
3 | import { normaliseAccountName } from "./protootshelpers";
4 | import { extractFromStatus } from "./pronouns";
5 |
6 | let conversationsCache;
7 |
8 | /**
9 | * Fetches pronouns associated with account name.
10 | * If cache misses object is fetched from the instance.
11 | *
12 | * @param {string | undefined} dataID ID of the object being requested, in case cache misses.
13 | * @param {string} accountName The account name, used for caching. Should have the "@" prefix.
14 | * @param {string} type Type of data-id
15 | * @returns {string} The pronouns if we have any, otherwise "null".
16 | */
17 | export async function fetchPronouns(dataID, accountName, type) {
18 | // log(`searching for ${account_name}`);
19 | const cacheResult = await getPronouns(accountName);
20 | debug(cacheResult);
21 | if (cacheResult) return cacheResult;
22 |
23 | if (!dataID) {
24 | warn(`Could not fetch pronouns for user ${accountName}, because no status ID was passed.`);
25 | return null;
26 | }
27 |
28 | let status;
29 | if (type === "notification") {
30 | status = await fetchNotification(dataID);
31 | } else if (type === "account") {
32 | status = await fetchAccount(dataID);
33 | } else if (type === "conversation") {
34 | const conversations = await fetchConversations();
35 | for (const conversation of conversations) {
36 | for (const account of conversation.accounts) {
37 | //conversations can have multiple participants, check that we're passing along the right account
38 | if (normaliseAccountName(account.acct) == accountName) {
39 | //package the account object in an empty object for compatibility with getPronounField()
40 | status = { account: account };
41 | }
42 | }
43 | }
44 | } else {
45 | status = await fetchStatus(dataID);
46 | }
47 |
48 | if (!status) {
49 | log(`Fetching ${type} failed, trying notification instead.`);
50 | status = await fetchNotification(dataID);
51 | } //fallback for glitch-soc notifications
52 |
53 | let pronouns = await extractFromStatus(status);
54 | if (!pronouns) {
55 | pronouns = "null";
56 | info(`no pronouns found for ${accountName}, cached null`);
57 | }
58 | await cachePronouns(accountName, pronouns);
59 | return pronouns;
60 | }
61 |
62 | /**
63 | * Fetches status by statusID from host_name with user's access token.
64 | *
65 | * @param {string} statusID ID of status being requested.
66 | * @returns {Promise