├── .gitignore ├── .prettierrc ├── LICENSE ├── Makefile ├── README.md ├── assets ├── bookmarklet.svg ├── chrome-webstore.svg └── firefox-addons.svg ├── bookmarklet └── README.md ├── src ├── background.js ├── content.js ├── images │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-32.png │ └── icon-64.png ├── manifest--base.json └── options │ ├── options.css │ ├── options.html │ └── options.js └── web-ext-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: true 2 | singleQuote: true 3 | trailingComma: "es5" 4 | useTabs: true 5 | printWidth: 9999 6 | overrides: 7 | - files: "*.json" 8 | options: 9 | useTabs: false 10 | - files: "*.css" 11 | options: 12 | singleQuote: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Bramus Van Damme - https://www.bram.us/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: 2 | 3 | SRC_DIR?="src" 4 | BUILD_DIR?="build" 5 | DIST_DIR?="dist" 6 | CHROME_BINARY?="/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" 7 | PWD=${shell pwd} 8 | 9 | VERSION?=${shell cat src/manifest--base.json | jq '.version'} 10 | 11 | build-firefox: 12 | @printf "\e[1m\e[94m♺ Building version ${VERSION} for Firefox\e[0m" 13 | @mkdir -p ${BUILD_DIR}/firefox 14 | @yes | rm -rf "${BUILD_DIR}/firefox/*" 15 | @cp -r ${SRC_DIR}/* ${BUILD_DIR}/firefox 16 | @# Firefox needs an empty background.service_worker 17 | @cat ${BUILD_DIR}/firefox/manifest--base.json | jq '.background.service_worker = ""' > ${BUILD_DIR}/firefox/manifest.json 18 | @rm ${BUILD_DIR}/firefox/manifest--base.json 19 | @printf "\n\e[1m\e[32m✔ Done\e[0m\n\n" 20 | 21 | build-chromium: 22 | @printf "\e[1m\e[94m♺ Building version ${VERSION} for Chromium\e[0m" 23 | @mkdir -p ${BUILD_DIR}/chromium 24 | @yes | rm -rf "${BUILD_DIR}/chromium/*" 25 | @cp -r ${SRC_DIR}/* ${BUILD_DIR}/chromium 26 | @# Chrome does not need .background.scripts 27 | @cat ${BUILD_DIR}/chromium/manifest--base.json | jq 'del(.background.scripts)' | jq 'del(.browser_specific_settings)' > ${BUILD_DIR}/chromium/manifest.json 28 | @rm ${BUILD_DIR}/chromium/manifest--base.json 29 | @printf "\n\e[1m\e[32m✔ Done\e[0m\n\n" 30 | 31 | build-all: 32 | @$(MAKE) build-firefox 33 | @$(MAKE) build-chromium 34 | 35 | watch: 36 | @watchman --version > /dev/null 2>&1 || (printf "\e[1m\e[31mERROR: watchman is required. Please install it first.\e[0m\n"; exit 1) 37 | @printf "\e[1m\e[94m♺ Starting watchman… \e[0m\n" 38 | @watchman-make -p 'src/*' 'src/**/*' 'Makefile*' -t build-all 39 | 40 | package-firefox: 41 | @printf "\e[1m\e[94m♺ Packaging version ${VERSION} for Firefox\e[0m\n" 42 | @mkdir -p ${DIST_DIR} 43 | @cd ${BUILD_DIR}/firefox/ && zip -r -FS ${PWD}/${DIST_DIR}/${VERSION}--firefox.zip ./* -x "**/.*" 44 | @printf "\e[1m\e[32m✔ Done\e[0m\n\n" 45 | 46 | package-chromium: 47 | @printf "\e[1m\e[94m♺ Packaging version ${VERSION} for Chromium\e[0m\n" 48 | @mkdir -p ${DIST_DIR} 49 | @cd ${BUILD_DIR}/chromium/ && zip -r -FS ${PWD}/${DIST_DIR}/${VERSION}--chromium.zip ./* -x "**/.*" 50 | @printf "\e[1m\e[32m✔ Done\e[0m\n\n" 51 | 52 | package-all: 53 | @$(MAKE) build-firefox 54 | @$(MAKE) package-firefox 55 | @$(MAKE) build-chromium 56 | @$(MAKE) package-chromium 57 | 58 | run-chrome: 59 | @$(MAKE) build-chromium 60 | @printf "\e[1m\e[94m♺ Launching Chrome …\e[0m\n" 61 | @-${CHROME_BINARY} --user-data-dir=/tmp/mastodon-profile-redirect-dev --load-extension=${BUILD_DIR}/chromium --no-first-run &>/dev/null & 62 | @printf "\e[1m\e[32m✔ Done\e[0m\n\n" 63 | 64 | run-firefox: 65 | @$(MAKE) build-firefox 66 | @printf "\e[1m\e[94m♺ Launching Firefox …\e[0m\n" 67 | @web-ext run &>/dev/null & 68 | @printf "\e[1m\e[32m✔ Done\e[0m\n\n" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastodon Redirector 2 | 3 | | [![Available on the Chrome Web Store](./assets/chrome-webstore.svg)](https://chrome.google.com/webstore/detail/mastodon-redirector/limifnkopacddgpihodacjeckfkpbfoe) | [![Available on the Firefox Add-ons Website](./assets/firefox-addons.svg)](https://addons.mozilla.org/en-US/firefox/addon/mastodon-profile-redirect/) | [![Available as Bookmarklets](./assets/bookmarklet.svg)](#other-browsers) | 4 | |-|-|-| 5 | 6 | One thing that’s a bit of a hassle with Mastodon is that you can’t immediately follow people on other instances: you have to copy the username and search for it on the instance that hosts your account. Same story with favoriting and boosting their posts, when viewed on their instance’s URL. 7 | 8 | To make this easier, this extension exists. 9 | 10 | ## Demo 11 | 12 | https://github.com/bramus/mastodon-redirector/assets/213073/b0554662-a7c2-4261-aae0-957f38f47ffb 13 | 14 | _This demo shows Chrome with the extension running. My Mastodon host is `front-end.social`. After configuring the extension, I can quickly jump from other Mastodon instances to my own instance where I can follow/favorite/boost._ 15 | 16 | ## Installation 17 | 18 | ### Quick Links 19 | 20 | Use one of the buttons below to be taken to the extension stores 21 | 22 | | [![Available on the Chrome Web Store](./assets/chrome-webstore.svg)](https://chrome.google.com/webstore/detail/mastodon-redirector/limifnkopacddgpihodacjeckfkpbfoe) | [![Available on the Firefox Add-ons Website](./assets/firefox-addons.svg)](https://addons.mozilla.org/en-US/firefox/addon/mastodon-profile-redirect/) | [![Available as Bookmarklets](./assets/bookmarklet.svg)](#other-browsers) | 23 | |-|-|-| 24 | 25 | ### Chromium-based browsers 26 | 27 | _This applies to browsers that are based on Chromium. This includes Google Chrome, Microsoft Edge, …_ 28 | 29 | The easiest way is to install it from [the Chrome Web Store](https://chrome.google.com/webstore/detail/mastodon-view-profile-on-my-mastodon-instance/limifnkopacddgpihodacjeckfkpbfoe) 30 | 31 | Alternatively you can clone this repository and [load the extension unpacked](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). Before you can do this, you will need to [build the project](#development) though. 32 | 33 | Don’t forget to [configure the extension](#configuration). 34 | 35 | ### Firefox 36 | 37 | You can install the extension from [the Firefox Add-Ons Website](https://addons.mozilla.org/en-US/firefox/addon/mastodon-profile-redirect/). Because this extension uses Manifest v3, support is limited to Firefox 109 and up. 38 | 39 | To run the extension in older versions of Firefox: 40 | 41 | - Go to `about:config` and set `extensions.manifestV3.enabled` to `true` 42 | - Hit the “download file” link on the [Add-Ons listing](https://addons.mozilla.org/en-US/firefox/addon/mastodon-profile-redirect/), upon which Firefox will prompt you to install it 43 | 44 | Don’t forget to [configure the extension](#configuration). 45 | 46 | ### Other Browsers 47 | 48 | If you can’t or won’t run the extension, you can use [the bookmarklets](./bookmarklet/) which also do the job. 49 | 50 | ## Configuration 51 | 52 | - Chrome: 53 | - Right click the extension icon and choose “Options” 54 | - Enter the details and hit “Save Settings” 55 | - Firefox: 56 | - Right click the extension icon and choose “Manage Extension” 57 | - Click the three dot menu next to the extension name and click “preferences” 58 | - Enter the details and hit “Save Settings” 59 | 60 | TIP: To get easy access, pin the extension icon 61 | 62 | ## Usage 63 | 64 | When viewing a Mastodon page on a different instance than yours, hit the extension icon to get redirected to the profile/post on your instance. You can also use the `Alt`+`Shift`+`M` / `Option`+`Shift`+`M` shortcut to invoke the redirect. 65 | 66 | Furthermore, this extension modifies the modals Mastodon shows on other instances. When wanting to follow/favorite/boost, a button with a direct link to your Mastodon instance will be injected into the markup. 67 | 68 | ## Development 69 | 70 | ### Building 71 | 72 | Due to minor differences in support for extensions that use Manifest v3 in Chromium and Firefox, the code of this repository cannot directly be loaded. The code needs to be built. 73 | 74 | ```bash 75 | make build-all 76 | ``` 77 | 78 | The built, yet still unpacked, extensions can be found in `./build/firefox` and `./build/chromium`. 79 | 80 | Building depends on [`jq`](https://stedolan.github.io/jq/) which you must install first. 81 | 82 | While actively developing, make use of the `watch` task. It will auto-rebuild whenever you save something in `src/`. 83 | 84 | ```bash 85 | make watch 86 | ``` 87 | 88 | ### Loading 89 | 90 | For Chromium based browsers, [load the extension unpacked](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked) from the `./build/chromium` folder. Alternatively you can use the `run-chrome` Make script: 91 | 92 | ```bash 93 | make run-chrome 94 | ``` 95 | 96 | For Firefox, you’ll need [`web-ext`](https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/) to load the extension during development. A `web-ext-config.js` is included in the repository, so you can easily run `web-ext run`. Alternatively you can use the `run-firefox` Make script: 97 | 98 | ```bash 99 | make run-firefox 100 | ``` 101 | 102 | ## Support and bugs 103 | 104 | This project is offered as is. However, I am open to receiving bug reports and accepting PRs to improve this extension. 105 | 106 | ## License 107 | 108 | This project is released under the MIT public license. See the enclosed `LICENSE` for details. 109 | 110 | ## Acknowledgements 111 | 112 | Icon by [Flatart](https://www.iconfinder.com/icons/4373112/logo_logos_mastodon_icon) 113 | -------------------------------------------------------------------------------- /assets/bookmarklet.svg: -------------------------------------------------------------------------------- 1 | Available as aBookmarklet -------------------------------------------------------------------------------- /assets/chrome-webstore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/firefox-addons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bookmarklet/README.md: -------------------------------------------------------------------------------- 1 | # Mastodon Redirect Bookmarklets 2 | 3 | If you can’t or won’t run [the Mastodon Profile Redirect browser extension](https://github.com/bramus/mastodon-profile-redirect/), you can use these bookmarklets instead which also do the job 4 | 5 | ## Installation Instructions 6 | 7 | For each of the bookmarklets: 8 | 9 | - Create a new bookmark 10 | - Set the Name 11 | - Set the URL to the contents of the code block 12 | - Be sure to include the `javascript:` prefix from the code. This is required. 13 | - Be sure to change the value of `LOCAL_DOMAIN` _(line 2)_ so that it reflects your Mastodon instance. 14 | 15 | ## The Code 16 | 17 | ### View Mastodon Profile 18 | 19 | ```js 20 | javascript: (function () { 21 | const LOCAL_DOMAIN = 'example.org'; /* 👈 Change this value to your Mastodon domain, e.g. 'mastodon.social' */ 22 | const WEB_DOMAIN = LOCAL_DOMAIN; /* 👈 Only change this value if your Masto host is hosted an different domain than the LOCAL_DOMAIN */ 23 | 24 | /* Show warning in case user has not configured the bookmarklet */ 25 | if (LOCAL_DOMAIN === 'example.org') { 26 | alert('This bookmarklet is not configured properly. Please follow the installation instructions and change the value for LOCAL_DOMAIN before you use it.'); 27 | return; 28 | } 29 | 30 | function tryAndGetUserName() { 31 | /* Profile with a moved banner (e.g. https://mastodon.social/@bramus): follow that link */ 32 | const userNewProfile = document.querySelector('.moved-account-banner .button')?.getAttribute('href'); 33 | if (userNewProfile) { 34 | return userNewProfile.substring(2); 35 | } 36 | 37 | /* Profile page, e.g. https://fediverse.zachleat.com/@zachleat and https://front-end.social/@mia */ 38 | /* First try the username meta tag. However, sometimes Mastodon forgets to inject it, so we fall back to the username shown in the profile header */ 39 | const userFromProfilePage = document.querySelector('meta[property="profile:username"]')?.getAttribute('content') || document.querySelector('.account__header .account__header__tabs__name small')?.innerText.substring(1); 40 | if (userFromProfilePage) { 41 | /* Don’t return if already watching on own LOCAL_DOMAIN instance */ 42 | if (window.location.host === LOCAL_DOMAIN) return null; 43 | return userFromProfilePage; 44 | } 45 | 46 | /* Message detail, e.g. https://front-end.social/@mia/109348973362020954 and https://bell.bz/@andy/109392510558650993 and https://bell.bz/@andy/109392510558650993 */ 47 | const userFromDetailPage = document.querySelector('.detailed-status .display-name__account')?.innerText; 48 | if (userFromDetailPage) return userFromDetailPage.substring(1); 49 | 50 | return null; 51 | } 52 | 53 | let user = tryAndGetUserName(); 54 | if (!user) return; 55 | 56 | /* Trim off @domain suffix in case it matches with LOCAL_DOMAIN. This due to https://github.com/mastodon/mastodon/issues/21469 */ 57 | if (user.endsWith(`@${LOCAL_DOMAIN}`)) { 58 | user = user.substring(0, user.length - `@${LOCAL_DOMAIN}`.length); 59 | } 60 | 61 | window.location.href = `https://${WEB_DOMAIN}/@${user}`; 62 | })(); 63 | ``` 64 | 65 | ### View Toot 66 | 67 | ```js 68 | javascript: (function () { 69 | const LOCAL_DOMAIN = 'example.org'; /* 👈 Change this value to your Mastodon domain, e.g. 'mastodon.social' */ 70 | const WEB_DOMAIN = LOCAL_DOMAIN; /* 👈 Only change this value if your Masto host is hosted an different domain than the LOCAL_DOMAIN */ 71 | 72 | /* Show warning in case user has not configured the bookmarklet */ 73 | if (LOCAL_DOMAIN === 'example.org') { 74 | alert('This bookmarklet is not configured properly. Please follow the installation instructions and change the value for LOCAL_DOMAIN before you use it.'); 75 | return; 76 | } 77 | 78 | window.location.href = `https://${WEB_DOMAIN}/authorize_interaction?uri=${encodeURIComponent(window.location.href)}`; 79 | })(); 80 | ``` -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | const go = () => { 2 | let MY_MASTO_LOCAL_DOMAIN = null; 3 | let MY_MASTO_WEB_DOMAIN = null; 4 | 5 | const tryAndGetUserNameFromProfilePage = () => { 6 | /* Profile with a moved banner (e.g. https://mastodon.social/@bramus): follow that link */ 7 | const userNewProfile = document.querySelector('.moved-account-banner .button')?.getAttribute('href'); 8 | if (userNewProfile) { 9 | return userNewProfile.substring(2); 10 | } 11 | 12 | /* Profile page, e.g. https://fediverse.zachleat.com/@zachleat and https://front-end.social/@mia */ 13 | const userFromProfilePage = document.querySelector('.account__header .account__header__tabs__name small')?.innerText.split('\n'); 14 | if (userFromProfilePage) { 15 | /* For profile pages on Mastodon v4.2.x */ 16 | if (userFromProfilePage.length == 1) { 17 | return userFromProfilePage[0].substring(1); 18 | } 19 | /* For profile pages on Mastodon v4.3.x and v4.4.x */ 20 | else if (userFromProfilePage.length == 3) { 21 | return userFromProfilePage[0].substring(1) + userFromProfilePage[1]; 22 | } 23 | } 24 | 25 | // Not a profile page or some markup that is preventing things from happening 26 | return null; 27 | }; 28 | 29 | const getProfileRedirectUrl = () => { 30 | let user = tryAndGetUserNameFromProfilePage(); 31 | 32 | // Found user ~> Redirect to profile on own host 33 | if (user) { 34 | /* Trim off @domain suffix in case it matches with MY_MASTO_LOCAL_DOMAIN. This due to https://github.com/mastodon/mastodon/issues/21469 */ 35 | if (user.endsWith(`@${MY_MASTO_LOCAL_DOMAIN}`)) { 36 | user = user.substring(0, user.length - `@${MY_MASTO_LOCAL_DOMAIN}`.length); 37 | } 38 | 39 | return `https://${MY_MASTO_WEB_DOMAIN}/@${user}`; 40 | } 41 | 42 | return null; 43 | }; 44 | 45 | const getPostRedirectUrl = () => { 46 | // We resort to URL sniffing here … sorry 47 | const urlPathParts = window.location.pathname.substr('1').split('/'); 48 | 49 | // The path must be something like /@user/12345 50 | if (urlPathParts.length != 2) return null; 51 | if (!Number.isInteger(parseInt(urlPathParts[1]))) return null; 52 | 53 | // It is quite safe to assume this was a Mastodon post detail 54 | return `https://${MY_MASTO_WEB_DOMAIN}/authorize_interaction?uri=${encodeURIComponent(window.location.href)}`; 55 | }; 56 | 57 | chrome.storage.sync.get( 58 | { 59 | local_domain: '', 60 | web_domain: '', 61 | }, 62 | function (items) { 63 | MY_MASTO_LOCAL_DOMAIN = items.local_domain; 64 | MY_MASTO_WEB_DOMAIN = items.web_domain || MY_MASTO_LOCAL_DOMAIN; 65 | 66 | if (!MY_MASTO_LOCAL_DOMAIN) { 67 | alert('Please go to options and set your MY_MASTO_LOCAL_DOMAIN first'); 68 | return; 69 | } 70 | 71 | // Don’t do anything if already on the mastodon domain 72 | if (window.location.host === MY_MASTO_LOCAL_DOMAIN) return null; 73 | 74 | const extractors = [getProfileRedirectUrl, getPostRedirectUrl]; 75 | 76 | let redirectUrl = null; 77 | do { 78 | redirectUrl = extractors.shift()(); 79 | } while (redirectUrl == null && extractors.length); 80 | 81 | if (redirectUrl) { 82 | window.location.href = redirectUrl; 83 | } else { 84 | alert('No Mastodon profile or Mastodon post detected. If this is incorrect, please file a bug.'); 85 | } 86 | } 87 | ); 88 | }; 89 | 90 | chrome.action.onClicked.addListener((tab) => { 91 | if (!tab.url.includes('chrome://')) { 92 | chrome.scripting.executeScript({ 93 | target: { tabId: tab.id }, 94 | func: go, 95 | }); 96 | } 97 | }); 98 | 99 | chrome.runtime.onInstalled.addListener(({ reason }) => { 100 | if (reason === chrome.runtime.OnInstalledReason.INSTALL) { 101 | chrome.runtime.openOptionsPage(); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | const isMostLikelyMastodon = document.querySelector('#mastodon'); 2 | 3 | let LOCAL_DOMAIN = null; 4 | let WEB_DOMAIN = null; 5 | 6 | // This method gets the profile redirect URL from the DOM 7 | // This because the it must be in the form `@username@otherhost` 8 | const tryAndGetUserNameFromProfilePage = () => { 9 | /* Profile with a moved banner (e.g. https://mastodon.social/@bramus): follow that link */ 10 | const userNewProfile = document.querySelector('.moved-account-banner .button')?.getAttribute('href'); 11 | if (userNewProfile) { 12 | return userNewProfile.substring(2); 13 | } 14 | 15 | /* Profile page, e.g. https://fediverse.zachleat.com/@zachleat and https://front-end.social/@mia */ 16 | const userFromProfilePage = document.querySelector('.account__header .account__header__tabs__name small')?.innerText.split('\n'); 17 | if (userFromProfilePage) { 18 | /* For profile pages on Mastodon v4.2.x */ 19 | if (userFromProfilePage.length == 1) { 20 | return userFromProfilePage[0].substring(1); 21 | } 22 | /* For profile pages on Mastodon v4.3.x and v4.4.x */ 23 | else if (userFromProfilePage.length == 3) { 24 | return userFromProfilePage[0].substring(1) + userFromProfilePage[1]; 25 | } 26 | } 27 | 28 | // Not a profile page or some markup that is preventing things from happening 29 | return null; 30 | }; 31 | 32 | const getProfileRedirectUrl = () => { 33 | let user = tryAndGetUserNameFromProfilePage(); 34 | 35 | // Found user ~> Redirect to profile on own host 36 | if (user) { 37 | /* Trim off @domain suffix in case it matches with LOCAL_DOMAIN. This due to https://github.com/mastodon/mastodon/issues/21469 */ 38 | if (user.endsWith(`@${LOCAL_DOMAIN}`)) { 39 | user = user.substring(0, user.length - `@${LOCAL_DOMAIN}`.length); 40 | } 41 | 42 | return `https://${WEB_DOMAIN}/@${user}`; 43 | } 44 | 45 | return null; 46 | }; 47 | 48 | const getRedirectUrlForUrl = (url) => { 49 | return `https://${WEB_DOMAIN}/authorize_interaction?uri=${encodeURIComponent(url)}`; 50 | }; 51 | 52 | let haltMutationObserver = false; 53 | const go = () => { 54 | const $modalRoot = document.querySelector('.modal-root'); 55 | 56 | if ($modalRoot) { 57 | const observer = new MutationObserver(function (mutations_list) { 58 | // Don’t double run when already busy 59 | if (haltMutationObserver) { 60 | return; 61 | } 62 | haltMutationObserver = true; 63 | 64 | mutations_list.forEach(function (mutation) { 65 | if (!mutation.addedNodes.length) { 66 | haltMutationObserver = false; 67 | return; 68 | } 69 | 70 | const $redirectInput = document.querySelector('.modal-root .copypaste input[type="text"]'); 71 | if (!$redirectInput) { 72 | haltMutationObserver = false; 73 | return; 74 | } 75 | 76 | $choiceBox = $redirectInput.closest('.interaction-modal__choices__choice'); 77 | if (!$choiceBox) { 78 | haltMutationObserver = false; 79 | return; 80 | } 81 | 82 | chrome.storage.sync.get( 83 | { 84 | local_domain: '', 85 | web_domain: '', 86 | }, 87 | function (items) { 88 | LOCAL_DOMAIN = items.local_domain; 89 | WEB_DOMAIN = items.web_domain || LOCAL_DOMAIN; 90 | 91 | // Not configured? Show a notification. 92 | if (!WEB_DOMAIN) { 93 | $choiceBox.querySelector('p').innerText = 'Please configure the mastodon-profile-redirect browser extension to more easily follow this account, directly on your Mastodon instance.'; 94 | haltMutationObserver = false; 95 | return; 96 | } 97 | 98 | // Change title to reflect user’s Masto instance 99 | $choiceBox.querySelector('h3 span').innerText = `On ${LOCAL_DOMAIN}`; 100 | 101 | let redirectUrl = $redirectInput.value; 102 | let label = null; 103 | 104 | // We resort to URL sniffing here … sorry 105 | const urlPathParts = new URL(redirectUrl).pathname.substr('1').split('/'); 106 | 107 | // Only 1 part that starts with an @? 108 | // ~> Build and use profile URL 109 | if (urlPathParts.length == 1 && urlPathParts[0].startsWith('@')) { 110 | redirectUrl = getProfileRedirectUrl(); 111 | label = 'View Profile'; 112 | } 113 | // Everything else 114 | // ~> Trust the input value and use that 115 | else { 116 | redirectUrl = getRedirectUrlForUrl($redirectInput.value); 117 | label = 'View Post'; 118 | } 119 | 120 | if (redirectUrl) { 121 | // Create view button 122 | const $viewButton = document.createElement('a'); 123 | $viewButton.classList.add('button', 'button--block'); 124 | $viewButton.href = redirectUrl; 125 | $viewButton.innerText = label; 126 | 127 | // Replace the orig paragraph with the show profile button 128 | $choiceBox.querySelector('p').insertAdjacentElement('beforebegin', $viewButton); 129 | $choiceBox.removeChild($choiceBox.querySelector('p')); 130 | } 131 | } 132 | ); 133 | }); 134 | 135 | // Unlock MutationObserver after having processed the mutations list 136 | setTimeout(() => { 137 | haltMutationObserver = false; 138 | }, 10); 139 | }); 140 | 141 | observer.observe($modalRoot, { subtree: true, childList: true }); 142 | } 143 | }; 144 | 145 | if (isMostLikelyMastodon) go(); 146 | -------------------------------------------------------------------------------- /src/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramus/mastodon-redirector/ac076fdecef0d8c5e2af8fc0b594d37a2235ea6d/src/images/icon-128.png -------------------------------------------------------------------------------- /src/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramus/mastodon-redirector/ac076fdecef0d8c5e2af8fc0b594d37a2235ea6d/src/images/icon-16.png -------------------------------------------------------------------------------- /src/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramus/mastodon-redirector/ac076fdecef0d8c5e2af8fc0b594d37a2235ea6d/src/images/icon-32.png -------------------------------------------------------------------------------- /src/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bramus/mastodon-redirector/ac076fdecef0d8c5e2af8fc0b594d37a2235ea6d/src/images/icon-64.png -------------------------------------------------------------------------------- /src/manifest--base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mastodon Redirector", 3 | "action": {}, 4 | "manifest_version": 3, 5 | "version": "0.4.1", 6 | "description": "Redirect Mastodon URLs from other instances to your own instance.", 7 | "permissions": ["activeTab", "scripting", "storage"], 8 | "icons": { 9 | "16": "images/icon-16.png", 10 | "32": "images/icon-32.png", 11 | "64": "images/icon-64.png", 12 | "128": "images/icon-128.png" 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": ["*://*/*"], 17 | "js": ["content.js"] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background.js", 22 | "scripts": ["background.js"] 23 | }, 24 | "commands": { 25 | "_execute_action": { 26 | "suggested_key": { 27 | "windows": "Alt+Shift+M", 28 | "mac": "Alt+Shift+M", 29 | "chromeos": "Alt+Shift+M", 30 | "linux": "Alt+Shift+M" 31 | } 32 | } 33 | }, 34 | "options_ui": { 35 | "page": "options/options.html", 36 | "browser_style": false, 37 | "open_in_tab": true 38 | }, 39 | "browser_specific_settings": { 40 | "gecko": { 41 | "id": "mastodon-profile-redirect@bram.us" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | background: aliceblue; 8 | } 9 | 10 | body { 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 12 | font-size: 100%; 13 | } 14 | 15 | @layer layout { 16 | html { 17 | max-width: 84ch; 18 | padding: 3rem 2rem; 19 | margin: auto; 20 | line-height: 1.5; 21 | font-size: 1.25rem; 22 | } 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | a, 28 | a:visited { 29 | color: blue; 30 | } 31 | 32 | h1 { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | gap: 0.5rem; 37 | } 38 | 39 | h2 { 40 | margin-top: 2em; 41 | } 42 | } 43 | 44 | @layer forms { 45 | dl { 46 | display: grid; 47 | grid-template: auto / max(auto, 10em) 1fr; 48 | gap: 0.5em; 49 | } 50 | 51 | dt { 52 | grid-column: 1; 53 | } 54 | 55 | dd { 56 | grid-column: 2; 57 | } 58 | 59 | dt, 60 | dd { 61 | margin: 0; 62 | padding: 0.3em 0; 63 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 64 | } 65 | 66 | fieldset { 67 | border: 0; 68 | margin: 0; 69 | padding: 0.5rem; 70 | } 71 | 72 | legend { 73 | padding: 0; 74 | margin-left: -0.5rem; 75 | font-size: 1.5em; 76 | font-weight: bold; 77 | } 78 | 79 | input { 80 | width: 100%; 81 | background: #fff; 82 | } 83 | 84 | input, 85 | button { 86 | font-size: inherit; 87 | font-family: inherit; 88 | } 89 | 90 | .info { 91 | font-style: italic; 92 | font-size: 80%; 93 | } 94 | 95 | dt:has(+ dd input[required]) label::after { 96 | content: "*"; 97 | color: red; 98 | } 99 | 100 | button { 101 | font-size: 80%; 102 | float: right; 103 | padding: 0.3em 0.7em; 104 | border: 2px solid #333; 105 | background-color: #ddd; 106 | border-radius: 0.5em; 107 | font-weight: bold; 108 | cursor: pointer; 109 | } 110 | 111 | button:hover { 112 | background-color: #eee; 113 | } 114 | 115 | button:active { 116 | background-color: #ccc; 117 | } 118 | 119 | form::after { 120 | content: ""; 121 | display: table; 122 | clear: both; 123 | } 124 | } 125 | 126 | @keyframes okfade { 127 | 0% { 128 | background: rgb(0 255 0 / 0.5); 129 | } 130 | 100% { 131 | background: rgb(0 255 0 / 0); 132 | } 133 | } 134 | 135 | .ok { 136 | animation: 0.7s okfade ease-in; 137 | } 138 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Options 7 | 8 | 9 | 10 | 11 | 12 |

Mastodon Redirector

13 |

In order to work properly, this extension needs to know which instance hosts your account. Please enter the required information below and hit Save Settings when done. This information is stored locally and not shared with anyone.

14 | 15 |
16 |

Mastodon Instance Configuration

17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |

ℹ️ LOCAL_DOMAIN of the Mastodon instance where you have your account (e.g. toot.cafe, mastodon.social, …)

26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |

ℹ️ WEB_DOMAIN of the Mastodon instance where you have your account. Typically this is the same as the LOCAL_DOMAIN value, yet some instances run Mastodon on a subdomain (e.g. fediverse.zachleat.com).

36 |

You can leave this value empty if it is the same as the LOCAL_DOMAIN which most likely is the case.

37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | // @ref https://developer.chrome.com/docs/extensions/mv3/options/ 2 | function save_options(e) { 3 | e.preventDefault(); 4 | e.stopPropagation(); 5 | 6 | document.getElementById('settings').classList.remove('ok'); 7 | 8 | const MY_MASTO_LOCAL_DOMAIN = document.getElementById('local_domain').value; 9 | const MY_MASTO_WEB_DOMAIN = document.getElementById('web_domain').value; 10 | 11 | chrome.storage.sync.set( 12 | { 13 | local_domain: MY_MASTO_LOCAL_DOMAIN, 14 | web_domain: MY_MASTO_WEB_DOMAIN, 15 | }, 16 | function () { 17 | document.getElementById('settings').classList.add('ok'); 18 | } 19 | ); 20 | } 21 | 22 | function restore_options() { 23 | chrome.storage.sync.get( 24 | { 25 | local_domain: '', 26 | web_domain: '', 27 | }, 28 | function (items) { 29 | document.getElementById('local_domain').value = items.local_domain; 30 | document.getElementById('web_domain').checked = items.web_domain; 31 | } 32 | ); 33 | } 34 | 35 | document.addEventListener('DOMContentLoaded', restore_options); 36 | document.getElementById('settings').addEventListener('submit', save_options); 37 | -------------------------------------------------------------------------------- /web-ext-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | sourceDir: 'build/firefox', 4 | run: { 5 | firefoxPreview: ['mv3'], 6 | firefox: 'nightly', 7 | }, 8 | lint: { 9 | firefoxPreview: ['mv3'], 10 | }, 11 | }; 12 | --------------------------------------------------------------------------------