restructure folders, use promises for storage, restructure code

tests
lartsch 2022-11-18 08:20:22 -05:00
rodzic 427c4d0083
commit d2543eb499
9 zmienionych plików z 339 dodań i 348 usunięć

Wyświetl plik

@ -1,5 +1,5 @@
# FediFollow-Chrome
A Chrome extension that simplifies following Fediverse/Mastodon users on other instances than your own by automatically redirecting you to your instance when pressing a follow button. The redirection is shortly indicated in the follow button itself and additional modals are blocked. Should work for other Chromium browsers too, as well as Kiwi browser on Android. It was tested with about 20 instances and supports different instance layouts/flavours.
A Chrome extension that simplifies following Mastodon users on other instances than your own by automatically redirecting you to your instance when pressing a follow button. The redirection is shortly indicated in the follow button itself and additional modals are blocked. Should work for other Chromium browsers too, as well as Kiwi browser on Android. It was tested with about 20 instances and supports different instance layouts/flavours.
I made this since I could only find a working extension for Firefox, that does the same (Simplified Federation).
@ -22,11 +22,14 @@ Right now, you need to install it using developer mode. The extension is already
```
- Required: Hit "Submit" to update your settings
> **Note**
> 1. The whitelist mode can be useful if you do not want the extension to run basic checks on every site (since it needs to determine if it is a Mastodon site). Not sure if the blacklist feature is good for anything but I still included it.
> 2. It can have several reasons why a particular instance might not work:
> **Additional usage notes**
> 1. Currently supports different flavours of Mastodon 4
> 2. If the redirect is not working, you most likely are not logged in on your home instance
> 3. The whitelist mode can be useful if you do not want the extension to run basic checks on every site (since it needs to determine if it is a Mastodon site). Not sure if the blacklist feature is good for anything but I still included it.
> 4. It can have several reasons why a particular instance might not work:
> - There are instances that use custom layouts/flavours (additional identifiers need to be added to extension)
> - Instance chose to hide the follow button when not logged in (not supported yet)
> - It's not a Mastodon instance (not supported yet)
> - Element identifiers might change over time (extension needs to be updated)
>
> So please be aware, that this extension can fail in some cases. Feel free to submit pull requests / issues.
@ -36,7 +39,9 @@ Right now, you need to install it using developer mode. The extension is already
![Redirect Indication](https://github.com/lartsch/FediFollow-Chrome/blob/main/img/screenshot2.PNG?raw=true)
## Todos / Planned features
- Add support for Firefox
- Add support for post interactions
- Add support for other implementations (Plemora, GNU Social, ...)
- Publish to Chrome Webstore (in progress, currently in review by Google)
- Find additional layouts/flavours to add identifiers for
- Support for profiles views with follow button disabled

254
inject.js
Wyświetl plik

@ -1,254 +0,0 @@
// prep
const buttonPaths = ["div.account__header button.logo-button","div.public-account-header a.logo-button"];
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
const handleRegex = /^(?:https?:\/\/(www\.)?.*\..*?\/)(?<handle>@\w+(?:@\w+\.\w+)?)(?:\/?.*|\z)$/;
const enableConsoleLog = true;
const logPrepend = "[FediFollow]";
const maxElementWaitFactor = 200; // x 100ms for total time
var lastUrl = window.location.href;
var instance;
var showAlert;
var mode;
var whitelist;
var blacklist;
var target;
// wrapper to prepend to log messages
function log(text) {
if (enableConsoleLog) {
console.log(logPrepend + ' ' + text)
}
}
// function to wait for given elements to appear - first found element gets returned (but as of now the selectors are for different layouts anyways)
var waitForEl = function(counter, selectors, callback) {
var match = false;
// check all of the selectors
for (const selector of selectors) {
// if found
if ($(selector).length) {
// set match = true to prevent repetition hand over the found element
match = true;
callback(selector);
}
}
// repeat if no match was found and we did not exceed the wait factor yet
if (!match && counter < maxElementWaitFactor) {
setTimeout(function() {
// increase counter
waitForEl(counter + 1, selectors, callback);
}, 100);
}
};
// extract handle from any mastodon url
var extractHandle = function(url, callback) {
// regex with named match group
var match = url.match(handleRegex);
match = match.groups.handle
// check if match is valid
ats = (match.match(/@/g) || []).length;
if (!(match == null) && ats >= 1 && ats <= 2) {
// return the named match group
callback(match);
}
}
// test if the current site should be processed or not
// this will also be the function for whitelist/blacklist feature
function checkSite() {
// is this site on our home instance?
if (document.domain == instance) {
log("Current site is your home instance.");
return false;
}
if (mode == "whitelist") {
if ($.inArray(document.domain, whitelist) < 0) {
log("Current site is not in whitelist.");
return false;
}
} else {
if ($.inArray(document.domain, blacklist) > -1) {
log("Current site is in blacklist.");
return false;
}
}
// check if the current site looks like Mastodon
$(document).ready(function() {
if (!($("head").text().includes("mastodon") || $("head").text().includes("Mastodon") || $("div#mastodon").length)) {
log("Could not find a reference that this is a Mastodon site.")
return false;
}
});
return true;
}
// main function to listen for the follow button pressed and open a new tab with the home instance
function processSite() {
// check if we have a handle in the url
if (window.location.href.includes("@")) {
// grab the user handle
extractHandle(window.location.href, function(handle) {
// if we got one...
if (handle) {
// wait until follow button appears (document is already ready, but most content is loaded afterwards)
waitForEl(0, buttonPaths, function(found) {
if (found) {
// setup the button click listener
$(found).click(function(e) {
// prevent default action and other handlers
e.preventDefault();
e.stopImmediatePropagation();
// check the alert setting and show it if set
if (showAlert) {
alert("Redirecting to "+instance);
}
// backup the button text
var originaltext = $(found).text();
// replace the button text to indicate redirection
$(found).text("Redirecting...");
// timeout 1000ms to make it possible to notice the redirection indication
setTimeout(function() {
// if more than 1 @, we have a domain in the handle
if ((handle.match(/@/g) || []).length > 1) {
// but if its our own...
if (handle.includes(instance)) {
// ...then we need to remove it
handle = "@"+ handle.split("@")[1];
}
// request string
var request = 'https://'+instance+'/'+handle;
} else {
// with only 1 @, we have a local handle and need to append the domain
var request = 'https://'+instance+'/'+handle+'@'+document.domain;
}
// open the window
var win = window.open(request, target);
log("Redirected to " + request)
// focus the new tab if open was successfull
if (win) {
win.focus();
} else {
// otherwise notify user...
log('Could not open new window. Please allow popups for this website.');
}
// restore original button text
$(found).text(originaltext);
}, 1000);
});
} else {
log("Could not find any follow button.");
}
});
} else {
log("Could not find a handle.");
}
});
} else {
log("No handle in this URL.");
}
}
// for some reason, locationchange event did not work for me so lets use this ugly thing...
function urlChangeLoop() {
// run every 100ms, can probably be reduced
setTimeout(function() {
// compare last to current url
if (!(lastUrl == window.location.href)) {
// update lastUrl and run main script
lastUrl = window.location.href;
processSite();
}
// repeat
urlChangeLoop();
}, 300);
}
function processDomainList(newLineList) {
// split by new line
var arrayFromList = newLineList.split(/\r?\n/);
// array to put checked domains into
var cleanedArray = [];
for (var domain of arrayFromList) {
// remove whitespace
domain = domain.trim();
if (domainRegex.test(domain)) {
cleanedArray.push(domain)
} else {
log("Removed invalid domain " + domain + " from blacklist/whitelist.")
}
}
// return newly created set (remvoes duplicates)
return [...new Set(cleanedArray)];;
}
function checkSettings() {
// if the home instance is undefined/null/empty
if (instance == null || !instance) {
log("Mastodon home instance is not set.");
return false;
}
// if the value looks like a domain...
if (!(domainRegex.test(instance))) {
log("Instance setting is not a valid domain name.");
return false;
}
// set default if no value
if ($.inArray(mode, ["blacklist","whitelist"]) < 0) {
mode = "blacklist";
}
if ($.inArray(target, ["_blank","_self"]) < 0) {
target = "_blank";
}
if (mode == "whitelist") {
// if in whitelist mode and the cleaned whitelist is empty, return false
whitelist = processDomainList(whitelist);
if (whitelist.length < 1) {
log("Whitelist is empty or invalid.")
return false;
}
} else {
// also process the blacklist if in blacklist mode, but an empty blacklist is OK so we do not return false
blacklist = processDomainList(blacklist);
}
return true;
}
function run() {
// get the extension setting for the users' Mastadon home instance
chrome.storage.local.get(['fedifollow_homeinstance'], function(fetchedData) {
instance = fetchedData.fedifollow_homeinstance.trim();
// and alert setting
chrome.storage.local.get(['fedifollow_alert'], function(fetchedData) {
showAlert = fetchedData.fedifollow_alert;
// mode
chrome.storage.local.get(['fedifollow_mode'], function(fetchedData) {
mode = fetchedData.fedifollow_mode;
// whitelist
chrome.storage.local.get(['fedifollow_whitelist'], function(fetchedData) {
whitelist = fetchedData.fedifollow_whitelist;
// blacklist
chrome.storage.local.get(['fedifollow_blacklist'], function(fetchedData) {
blacklist = fetchedData.fedifollow_blacklist;
chrome.storage.local.get(['fedifollow_target'], function(fetchedData) {
target = fetchedData.fedifollow_target;
if (checkSettings()) {
// check if the current URL should be processed
if (checkSite()) {
// ... run the actual script (once for the start and then in a loop depending on url changes)
processSite();
urlChangeLoop();
} else {
log("Will not process this URL.")
}
}
});
});
});
});
});
});
}
run();

Wyświetl plik

@ -1,89 +0,0 @@
// prep
var currentVal_instance;
var currentVal_alert;
var currentVal_mode;
var currentVal_whitelist;
var currentVal_blacklist;
var currentVal_target;
// this performs loading the settings into the popup, reacting to changes and saving changes
function popupTasks() {
$(document).ready(function() {
// set all default/configured values and show fields accordingly
$("input#homeinstance").val(currentVal_instance);
$("textarea#blacklist_content").val(currentVal_blacklist);
$("textarea#whitelist_content").val(currentVal_whitelist);
if (currentVal_alert){
$("input#alert").prop('checked', true);
} else {
$("input#alert").prop('checked', false);
}
// we only set the mode if we have an actual value. otherwise the default should be left.
if (currentVal_mode) {
$("select#mode").val(currentVal_mode);
}
if (currentVal_target) {
$("select#target").val(currentVal_target);
}
// both containers are hidden by default
if ($("select#mode").val() == "whitelist") {
$("div#whitelist_input").show();
} else {
$("div#blacklist_input").show();
}
// check changes of the select to update whitelist/blacklist input
$("select#mode").change(function() {
if ($("select#mode").val() == "whitelist") {
$("div#blacklist_input").hide();
$("div#whitelist_input").show();
} else {
$("div#whitelist_input").hide();
$("div#blacklist_input").show();
}
});
// perform storage actions on form submit
$("form#fedifollow-settings").on('submit', function(e){
e.preventDefault();
var newVal_instance = $("input#homeinstance").val();
var newVal_alert = $("input#alert").is(':checked')
var newVal_mode = $("select#mode").val();
var newVal_whitelist = $("textarea#whitelist_content").val();
var newVal_blacklist = $("textarea#blacklist_content").val();
var newVal_target = $("select#target").val();
chrome.storage.local.set({fedifollow_homeinstance: newVal_instance}, function() {
chrome.storage.local.set({fedifollow_alert: newVal_alert}, function() {
chrome.storage.local.set({fedifollow_mode: newVal_mode}, function() {
chrome.storage.local.set({fedifollow_whitelist: newVal_whitelist}, function() {
chrome.storage.local.set({fedifollow_blacklist: newVal_blacklist}, function() {
chrome.storage.local.set({fedifollow_target: newVal_target}, function() {
$("span#indicator").show();
});
});
});
});
});
});
});
});
}
// read all settings, then run popupTasks()
chrome.storage.local.get(['fedifollow_homeinstance'], function(fetchedData) {
currentVal_instance = fetchedData.fedifollow_homeinstance;
chrome.storage.local.get(['fedifollow_alert'], function(fetchedData) {
currentVal_alert = fetchedData.fedifollow_alert;
chrome.storage.local.get(['fedifollow_mode'], function(fetchedData) {
currentVal_mode = fetchedData.fedifollow_mode;
chrome.storage.local.get(['fedifollow_whitelist'], function(fetchedData) {
currentVal_whitelist = fetchedData.fedifollow_whitelist;
chrome.storage.local.get(['fedifollow_blacklist'], function(fetchedData) {
currentVal_blacklist = fetchedData.fedifollow_blacklist;
chrome.storage.local.get(['fedifollow_target'], function(fetchedData) {
currentVal_target = fetchedData.fedifollow_target;
popupTasks()
});
});
});
});
});
});

Wyświetl plik

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.0 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.0 KiB

248
src/inject.js 100644
Wyświetl plik

@ -0,0 +1,248 @@
// prep
const buttonPaths = ["div.account__header button.logo-button","div.public-account-header a.logo-button"];
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/;
const handleRegex = /^(?:https?:\/\/(www\.)?.*\..*?\/)(?<handle>@\w+(?:@\w+\.\w+)?)(?:\/?.*|\z)$/;
const enableConsoleLog = true;
const logPrepend = "[FediFollow]";
const maxElementWaitFactor = 200; // x 100ms for total time
var lastUrl = window.location.href;
// settings keys with defauls
var settings = {
fedifollow_homeinstance: null,
fedifollow_alert: false,
fedifollow_mode: "blacklist",
fedifollow_whitelist: null,
fedifollow_blacklist: null,
fedifollow_target: "_blank"
}
// wrappers to prepend to log messages
function log(text) {
if (enableConsoleLog) {
console.log(logPrepend + ' ' + text)
}
}
function logerr(error) {
if (enableConsoleLog) {
console.error(logPrepend + ' Error: ' + error)
}
}
// function to wait for given elements to appear - first found element gets returned (but as of now the selectors are for different layouts anyways)
var waitForEl = function(counter, selectors, callback) {
var match = false;
// check all of the selectors
for (const selector of selectors) {
// if found
if ($(selector).length) {
// set match = true to prevent repetition hand over the found element
match = true;
callback(selector);
}
}
// repeat if no match was found and we did not exceed the wait factor yet
if (!match && counter < maxElementWaitFactor) {
setTimeout(function() {
// increase counter
waitForEl(counter + 1, selectors, callback);
}, 100);
}
};
// extract handle from any mastodon url
var extractHandle = function(url, callback) {
// regex with named match group
var match = url.match(handleRegex);
match = match.groups.handle
// check if match is valid
ats = (match.match(/@/g) || []).length;
if (!(match == null) && ats >= 1 && ats <= 2) {
// return the named match group
callback(match);
}
}
function processDomainList(newLineList) {
// split by new line
var arrayFromList = newLineList.split(/\r?\n/);
// array to put checked domains into
var cleanedArray = [];
for (var domain of arrayFromList) {
// remove whitespace
domain = domain.trim();
if (domainRegex.test(domain)) {
cleanedArray.push(domain)
} else {
log("Removed invalid domain " + domain + " from blacklist/whitelist.")
}
}
// return newly created set (remvoes duplicates)
return [...new Set(cleanedArray)];;
}
function runWithSettings(settings) {
function checkSettings() {
// if the home instance is undefined/null/empty
if (settings.fedifollow_homeinstance == null || !settings.fedifollow_homeinstance) {
log("Mastodon home instance is not set.");
return false;
}
// if the value looks like a domain...
if (!(domainRegex.test(settings.fedifollow_homeinstance))) {
log("Instance setting is not a valid domain name.");
return false;
}
// set default if wrong value
if ($.inArray(settings.fedifollow_mode, ["blacklist","whitelist"]) < 0) {
settings.fedifollow_mode = "blacklist";
}
if ($.inArray(settings.fedifollow_target, ["_blank","_self"]) < 0) {
settings.fedifollow_target = "_blank";
}
if (settings.fedifollow_mode == "whitelist") {
// if in whitelist mode and the cleaned whitelist is empty, return false
settings.fedifollow_whitelist = processDomainList(settings.fedifollow_whitelist);
if (settings.fedifollow_whitelist.length < 1) {
log("Whitelist is empty or invalid.")
return false;
}
} else {
// also process the blacklist if in blacklist mode, but an empty blacklist is OK so we do not return false
settings.fedifollow_blacklist = processDomainList(settings.fedifollow_blacklist);
}
return true;
}
// main function to listen for the follow button pressed and open a new tab with the home instance
function processSite() {
// check if we have a handle in the url
if (window.location.href.includes("@")) {
// grab the user handle
extractHandle(window.location.href, function(handle) {
// if we got one...
if (handle) {
// wait until follow button appears (document is already ready, but most content is loaded afterwards)
waitForEl(0, buttonPaths, function(found) {
if (found) {
// setup the button click listener
$(found).click(function(e) {
// prevent default action and other handlers
e.preventDefault();
e.stopImmediatePropagation();
// check the alert setting and show it if set
if (settings.fedifollow_alert) {
alert("Redirecting to "+settings.fedifollow_homeinstance);
}
// backup the button text
var originaltext = $(found).text();
// replace the button text to indicate redirection
$(found).text("Redirecting...");
// timeout 1000ms to make it possible to notice the redirection indication
setTimeout(function() {
// if more than 1 @, we have a domain in the handle
if ((handle.match(/@/g) || []).length > 1) {
// but if its our own...
if (handle.includes(settings.fedifollow_homeinstance)) {
// ...then we need to remove it
handle = "@"+ handle.split("@")[1];
}
// request string
var request = 'https://'+settings.fedifollow_homeinstance+'/'+handle;
} else {
// with only 1 @, we have a local handle and need to append the domain
var request = 'https://'+settings.fedifollow_homeinstance+'/'+handle+'@'+document.domain;
}
// open the window
var win = window.open(request, settings.fedifollow_target);
log("Redirected to " + request)
// focus the new tab if open was successfull
if (win) {
win.focus();
} else {
// otherwise notify user...
log('Could not open new window. Please allow popups for this website.');
}
// restore original button text
$(found).text(originaltext);
}, 1000);
});
} else {
log("Could not find any follow button.");
}
});
} else {
log("Could not find a handle.");
}
});
} else {
log("No handle in this URL.");
}
}
// test if the current site should be processed or not
// this will also be the function for whitelist/blacklist feature
function checkSite() {
// is this site on our home instance?
if (document.domain == settings.fedifollow_homeinstance) {
log("Current site is your home instance.");
return false;
}
if (settings.fedifollow_mode == "whitelist") {
if ($.inArray(document.domain, settings.fedifollow_whitelist) < 0) {
log("Current site is not in whitelist.");
return false;
}
} else {
if ($.inArray(document.domain, settings.fedifollow_blacklist) > -1) {
log("Current site is in blacklist.");
return false;
}
}
// check if the current site looks like Mastodon
$(document).ready(function() {
if (!($("head").text().includes("mastodon") || $("head").text().includes("Mastodon") || $("div#mastodon").length)) {
log("Could not find a reference that this is a Mastodon site.")
return false;
}
});
return true;
}
// for some reason, locationchange event did not work for me so lets use this ugly thing... since it calls processSite, it needs to be in runWithSettings as well
function urlChangeLoop() {
// run every 100ms, can probably be reduced
setTimeout(function() {
// compare last to current url
if (!(lastUrl == window.location.href)) {
// update lastUrl and run main script
lastUrl = window.location.href;
processSite();
}
// repeat
urlChangeLoop();
}, 300);
}
// check and process settings
if (checkSettings()) {
// check if the current URL should be processed
if (checkSite()) {
// ... run the actual script (once for the start and then in a loop depending on url changes)
processSite();
urlChangeLoop();
} else {
log("Will not process this URL.")
}
}
}
function loadSettings() {
const waitForSettings = (chrome || browser).storage.local.get(settings);
waitForSettings.then(runWithSettings, logerr);
}
loadSettings();

Wyświetl plik

@ -11,7 +11,7 @@
<div id="mhi-wrapper">
<div id="mhi-containers">
<form id="fedifollow-settings">
<label for="homeinstance">Mastodon home instance:</label><br>
<label for="homeinstance">Home instance (make sure you are logged in):</label><br>
<input type="text" id="homeinstance" name="homeinstance" placeholder="mastodon.social"><br>
<label for="alert">Alert on redirect:</label><br>
<input type="checkbox" id="alert" name="alert"><br>

81
src/popup.js 100644
Wyświetl plik

@ -0,0 +1,81 @@
// settings keys with defauls
var settings = {
fedifollow_homeinstance: null,
fedifollow_alert: false,
fedifollow_mode: "blacklist",
fedifollow_whitelist: null,
fedifollow_blacklist: null,
fedifollow_target: "_blank"
}
function onError(error){
console.error(`[FediFollow] Error: ${error}`)
}
// this performs loading the settings into the popup, reacting to changes and saving changes
function popupTasks(settings) {
function showConfirmation() {
$("span#indicator").show();
setTimeout(function() {
$("span#indicator").hide();
}, 1500);
}
function updateSettings(){
// update settings values
settings.fedifollow_homeinstance = $("input#homeinstance").val().trim();
settings.fedifollow_alert = $("input#alert").is(':checked');
settings.fedifollow_mode = $("select#mode").val();
settings.fedifollow_whitelist = $("textarea#whitelist_content").val();
settings.fedifollow_blacklist = $("textarea#blacklist_content").val();
settings.fedifollow_target = $("select#target").val();
// write to storage
const waitForSaved = (chrome || browser).storage.local.set(settings);
// show saved indicator after successful save
waitForSaved.then(showConfirmation(), onError);
}
function restoreForm() {
// set all default/configured values and show fields accordingly
$("input#homeinstance").val(settings.fedifollow_homeinstance);
$("textarea#blacklist_content").val(settings.fedifollow_blacklist);
$("textarea#whitelist_content").val(settings.fedifollow_whitelist);
$("select#mode").val(settings.fedifollow_mode);
$("select#target").val(settings.fedifollow_target);
$("input#alert").prop('checked', settings.fedifollow_alert);
// both containers are hidden by default
if ($("select#mode").val() == "whitelist") {
$("div#whitelist_input").show();
} else {
$("div#blacklist_input").show();
}
// check changes of the select to update whitelist/blacklist input
$("select#mode").change(function() {
if ($("select#mode").val() == "whitelist") {
$("div#blacklist_input").hide();
$("div#whitelist_input").show();
} else {
$("div#whitelist_input").hide();
$("div#blacklist_input").show();
}
});
}
$(document).ready(function() {
// restore the form values
restoreForm();
// perform storage actions on form submit
$("form#fedifollow-settings").on('submit', function(e){
// prevent default
e.preventDefault();
// update settings
updateSettings();
});
});
}
const waitForSettings = (chrome || browser).storage.local.get(settings);
waitForSettings.then(popupTasks, onError);