FediAct/src/inject.js

1043 wiersze
38 KiB
JavaScript
Czysty Zwykły widok Historia

// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
// =-=-=-=-=-= CONSTANTS =-==-=-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
const followButtonPaths = ["div.account__header button.logo-button","div.public-account-header a.logo-button","div.account-card a.logo-button","div.directory-card a.icon-button", "div.detailed-status a.logo-button"]
const profileNamePaths = ["div.account__header__tabs__name small", "div.public-account-header__tabs__name small", "div.detailed-status span.display-name__account", "div.display-name > span"]
const domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/
const handleExtractUrlRegex = /^(?<domain>https?:\/\/(?:\.?[a-z0-9-]+)+(?:\.[a-z]+){1})?\/?@(?<handle>\w+)(?:@(?<handledomain>(?:[\w-]+\.)+?\w+))?(?:\/(?<tootid>\d+))?\/?$/
const handleExtractUriRegex = /^(?<domain>https?:\/\/(?:\.?[a-z0-9-]+)+(?:\.[a-z]+){1})(?:\/users\/)(?<handle>\w+)(?:(?:\/statuses\/)(?<tootid>\d+))?\/?$/
const enableConsoleLog = true
const logPrepend = "[FediAct]"
const maxElementWaitFactor = 200 // x 100ms for total time
const instanceApi = "/api/v1/instance"
const statusApi = "/api/v1/statuses"
const searchApi = "/api/v2/search"
const accountsApi = "/api/v1/accounts"
const apiDelay = 500
const maxTootCache = 200
// settings keys with defauls
var settings = {}
const settingsDefaults = {
fediact_homeinstance: null,
fediact_alert: false,
fediact_mode: "blacklist",
fediact_whitelist: null,
fediact_blacklist: null,
fediact_target: "_self",
fediact_autoaction: true,
fediact_token: null,
fediact_showfollows: true,
fediact_redirects: true,
fediact_enabledelay: true
}
2022-12-03 01:09:23 +00:00
// fix for cross-browser storage api compatibility and other global vars
var browser, chrome, lasthomerequest, fedireply
// currently, the only reliable way to detect all toots etc. has the drawback that the same element could be processed multiple times
// this will store already processed elements to compare prior to processing and will reset as soon as the site context changes
var processed = []
// =-=-=-=-==-=-=-=-==-=-=-=-=-
// =-=-=-=-=-= UTILS =-==-=-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-=-
// wrappers to prepend to log messages
function log(text) {
if (enableConsoleLog) {
console.log(logPrepend + ' ' + text)
}
}
2022-12-03 01:09:23 +00:00
// Custom solution for detecting inserted nodes
// Works in combination with nodeinserted.css (fixes Firefox blocking addon-inserted <style> elements for sites with CSP)
// Is more reliable/practicable in certain situations than mutationobserver, who will ignore any node that was inserted with its parent node at once
(function($) {
$.fn.DOMNodeAppear = function(callback, selector) {
if (!selector) {
return false
}
// catch all animationstart events
$(document).on('animationstart webkitAnimationStart oanimationstart MSAnimationStart', function(e){
// check if the animatonname equals our animation and if the element is one of our selectors
if (e.originalEvent.animationName == 'nodeInserted' && $(e.target).is(selector)) {
if (typeof callback == 'function') {
// return the complete object in the callback
callback(e)
}
}
})
}
jQuery.fn.onAppear = jQuery.fn.DOMNodeAppear
})(jQuery)
2022-12-04 16:56:42 +00:00
// extract given url parameter value
var getUrlParameter = function getUrlParameter(sParam) {
var sPageURL = window.location.search.substring(1),
sURLVariables = sPageURL.split('&'), sParameterName, i
2022-12-04 16:56:42 +00:00
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=')
2022-12-04 16:56:42 +00:00
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : decodeURIComponent(sParameterName[1])
2022-12-04 16:56:42 +00:00
}
}
return false
}
2022-12-04 16:56:42 +00:00
2022-11-21 22:47:32 +00:00
// promisified xhr for api calls
async function makeRequest(method, url, extraheaders) {
// try to prevent error 429 too many request by delaying home instance requests
if (~url.indexOf(settings.fediact_homeinstance) && settings.fediact_enabledelay) {
// get current time
var currenttime = Date.now()
// get difference of current time and time of last request
var difference = currenttime - lasthomerequest
// if difference is smaller than our set api delay value...
if (difference < apiDelay) {
// ... then wait the time required to reach the api delay value...
await new Promise(resolve => {
setTimeout(function() {
resolve()
}, apiDelay-difference)
})
}
// TODO: move this to the top? or get new Date.now() here?
lasthomerequest = currenttime
}
// return a new promise...
return new Promise(function (resolve) {
// create xhr
let xhr = new XMLHttpRequest()
// open it with the method and url specified
xhr.open(method, url)
// set timeout
xhr.timeout = 3000
// set extra headers if any were given
if (extraheaders) {
for (var key in extraheaders) {
xhr.setRequestHeader(key, extraheaders[key])
2022-11-21 22:47:32 +00:00
}
}
// on load, check if status is OK...
2022-11-21 22:47:32 +00:00
xhr.onload = function () {
if (this.status >= 200 && this.status < 300) {
// is ok, resolve promise with response
resolve(xhr.responseText)
2022-11-21 22:47:32 +00:00
} else {
// nope, resolve false
resolve(false)
2022-11-21 22:47:32 +00:00
}
}
// on any error, resolve false
xhr.onerror = function() {
log("Request to " + url + " failed.")
resolve(false)
}
// send the request
xhr.send()
})
2022-11-21 22:47:32 +00:00
}
2022-12-03 01:09:23 +00:00
// Escape characters used for regex
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
2022-12-03 01:09:23 +00:00
// Replace all occurrences of a substring
function replaceAll(str, find, replace) {
return str.replace(new RegExp(escapeRegExp(find), 'g'), replace)
}
2022-12-03 01:09:23 +00:00
// handles redirects to home instance
function redirectTo(url) {
// check if redirects are enabled at all
if (settings.fediact_redirects) {
// check if alert before redirect is enabled and alert if so
if (settings.fediact_alert) {
alert("Redirecting...")
}
// open the url in same/new tab
var win = window.open(url, settings.fediact_target)
log("Redirected to " + url)
// 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.')
}
} else {
log("Redirects disabled.")
}
}
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
// =-=-=-=-= INTERACTIONS =-=-=-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
async function executeAction(id, action) {
if (settings.fediact_autoaction) {
var requestUrl, condition
switch (action) {
case 'follow':
requestUrl = 'https://' + settings.fediact_homeinstance + accountsApi + "/" + id + "/follow"
condition = function(response) {return response.following || response.requested}
break
case 'boost':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/reblog"
condition = function(response) {return response.reblogged}
break
case 'favourite':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/favourite"
condition = function(response) {return response.favourited}
break
case 'bookmark':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/bookmark"
condition = function(response) {return response.bookmarked}
break
case 'unfollow':
requestUrl = 'https://' + settings.fediact_homeinstance + accountsApi + "/" + id + "/unfollow"
condition = function(response) {return !response.following && !response.requested}
break
case 'unboost':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/unreblog"
condition = function(response) {return !response.reblogged}
break
case 'unfavourite':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/unfavourite"
condition = function(response) {return !response.favourited}
break
case 'unbookmark':
requestUrl = 'https://' + settings.fediact_homeinstance + statusApi + "/" + id + "/unbookmark"
condition = function(response) {return !response.bookmarked}
break
default:
log("No valid action specified."); break
}
if (requestUrl) {
var response = await makeRequest("POST",requestUrl,settings.tokenheader)
if (response) {
// convert to json object
response = JSON.parse(response)
if (condition(response)) {
return true
} else {
log(action + " action failed.")
}
} else {
log("API call failed.")
}
}
} else {
log("Auto-action is disabled.")
}
}
// allows to check if user is following one or multiple user IDs on the home instance
async function isFollowingHomeInstance(ids) {
var requestUrl = 'https://' + settings.fediact_homeinstance + accountsApi + "/relationships?"
// build the request url with one or multiple IDs
for (const id of ids) {
// trailing & is no issue
requestUrl += "id[]=" + id.toString() + "&"
}
// make the request
var responseFollowing = await makeRequest("GET",requestUrl,settings.tokenheader)
// fill response array according to id amount with false
const follows = Array(ids.length).fill(false)
// parse the response
if (responseFollowing) {
responseFollowing = JSON.parse(responseFollowing)
// iterate over ids and accounts in response
for (var i = 0; i < ids.length; i++) {
for (account of responseFollowing) {
// check if the current account matches the id at the current index
if (account.id == ids[i]) {
if (account.following) {
// update the response array at the given index with true if the account is already followed
follows[i] = true
}
}
}
}
}
return follows
}
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
// =-=-=-=-=-= RESOLVING =-=-==-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=
// Return the user id on the users home instance
async function resolveHandleToHome(handle) {
var requestUrl = 'https://' + settings.fediact_homeinstance + accountsApi + "/search?q=" + handle + "&resolve=true&limit=1"
var searchResponse = await makeRequest("GET",requestUrl,settings.tokenheader)
if (searchResponse) {
searchResponse = JSON.parse(searchResponse)
if (searchResponse[0].id) {
// return the first account result
return [searchResponse[0].id, searchResponse[0].acct]
}
}
return false
}
// resolve a toot to the users home instance
async function resolveTootToHome(searchstring) {
var requestUrl = 'https://' + settings.fediact_homeinstance + searchApi + "/?q=" + searchstring + "&resolve=true&limit=1"
var response = await makeRequest("GET", requestUrl, settings.tokenheader)
if (response) {
response = JSON.parse(response)
// do we have a status as result?
if (!response.accounts.length && response.statuses.length) {
var status = response.statuses[0]
// return the required status data
return [status.account.acct, status.id, status.reblogged, status.favourited, status.bookmarked]
} else {
return false
}
} else {
return false
}
}
// Get a toot's (external) home instance url by using the 302 redirect feature of mastodon
// we send a message with the toot url to the background script, which will perform the HEAD request
// since XMLHttpRequest/fetch do not allow access to the location header
// TODO: FALLBACK IF 302 IS NOT SUPPORTED
function resolveTootToExternalHome(tooturl) {
// TODO: check if a delay is necessary here too
if (tooturl) {
return new Promise(async function(resolve) {
try {
await chrome.runtime.sendMessage({url: tooturl}, function(response) {
if(response) {
resolve(response)
} else {
resolve(false)
}
})
} catch (e) {
// if we encounter an error here, it is likely since the extension context got invalidated, so reload the page
2022-12-08 15:07:37 +00:00
log(e)
log("Reloading page, extension likely got updated or reloaded.")
2022-12-08 15:07:37 +00:00
location.reload()
}
})
} else {
return false
}
}
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=
// =-=-=-=-= SITE PROCESSING =-==-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=
// custom implementation for allowing to toggle inline css
function toggleInlineCss(el, styles, toggleclass) {
// toggle the active class (specified) on the element (specified), then check the current state
var active = $(el).toggleClass(toggleclass).hasClass(toggleclass)
// we can have multiple styles as input, so loop through them
for (var style of styles) {
// if the element now has the active class...
if (active) {
// set the third value as style (first value / 0 is style itself)
$(el).css(style[0], style[2])
} else {
// otherwise, if the second value is "!remove"...
if (style[1] == "!remove") {
// remove the inline css by regex replacing
var newinline = replaceAll($(el).attr('style'), style[0]+": "+style[2]+";", "")
$(el).attr('style', newinline)
} else {
// otherwise set the second value as style
$(el).css(style[0], style[1])
}
}
}
}
// extract handle from elements
function extractHandle(selectors) {
// check all of the selectors
for (const selector of selectors) {
// if found
if ($(selector).length) {
// return trimmed since there can be whitespace
return $(selector).text().trim()
}
}
return false
}
// check if an toot identifier is already in the "processed" array
function isInProcessedToots(id) {
// iterate array
for (var i = 0; i < processed.length; i++) {
// if the the first value of the nested array at the current index matches the id we look for...
if (processed[i][0] == id) {
// return the index
return i
}
}
// if none was found...
return false
}
// add a toot to the "processed" array
function addToProcessedToots(toot) {
// push the array first
processed.push(toot)
// check the difference of the max elements to cache and the current length of the processed array
var diff = processed.length - maxTootCache
// if diff is greater than 0...
if (diff > 0) {
// remove the first diff items from it
processed = processed.splice(0,diff)
}
}
// trigger the reply button click - will only run when we are on a home instance url with fedireply parameter
2022-12-04 16:56:42 +00:00
async function processReply() {
// wait for the detailed status action bar to appear
2022-12-04 16:56:42 +00:00
$(document).DOMNodeAppear(function(e) {
// find the reply button and click it
$(e.target).find("button:has(i.fa-reply), button:has(i.fa-reply-all)").click()
2022-12-04 16:56:42 +00:00
}, "div.detailed-status__action-bar")
}
2022-11-21 22:47:32 +00:00
// process any toots found on supported sites
async function processToots() {
// determine action when a button is clicked (except reply, which will always redirect)
function getTootAction(e) {
2022-12-09 17:07:13 +00:00
// set to false initially
var action = false
2022-12-09 17:07:13 +00:00
// check if the clicked element has a retweet icon
if ($(e.currentTarget).children("i.fa-retweet").length) {
2022-12-09 17:07:13 +00:00
// if so, check if the retweet icon has the fediactive class (for all other buttons it will be the button element itself)
if ($(e.currentTarget).children("i.fa-retweet").hasClass("fediactive")) {
2022-12-09 17:07:13 +00:00
// yep, has the class - so this action should be unboost
action = "unboost"
} else {
2022-12-09 17:07:13 +00:00
// nope, so it will be a boost
action = "boost"
}
2022-12-09 17:07:13 +00:00
// repeat for favourite and bookmark
} else if ($(e.currentTarget).children("i.fa-star").length) {
if ($(e.currentTarget).hasClass("fediactive")) {
action = "unfavourite"
} else {
action = "favourite"
}
} else if ($(e.currentTarget).children("i.fa-bookmark").length) {
if ($(e.currentTarget).hasClass("fediactive")) {
action = "unbookmark"
} else {
action = "bookmark"
}
// should rarely reach this point, but some v3 instances include the action in the href only, so we have this as a fallback
// (v3 public view does NOT have a bookmark button)
} else if ($(e.currentTarget).attr("href")) {
// does the href include "type=reblog"?
if (~$(e.currentTarget).attr("href").indexOf("type=reblog")) {
// if so, do as above...
if ($(e.currentTarget).hasClass("fediactive")) {
action = "unboost"
} else {
action = "boost"
}
// repeat for favourite
} else if (~$(e.currentTarget).attr("href").indexOf("type=favourite")) {
if ($(e.currentTarget).hasClass("fediactive")) {
action = "unfavourite"
} else {
action = "favourite"
}
}
}
return action
}
// some toots contain an href which can be an already resolved external link or an internal reference
function tootHrefCheck(temp) {
2022-12-09 17:07:13 +00:00
// is it a full url?
if (temp.startsWith("http")) {
2022-12-09 17:07:13 +00:00
// yes, create a new URL() to access its parts
var tempUrl = new URL(temp)
2022-12-09 17:07:13 +00:00
// is the hostname of the URL the same as the current instance hostname?
if (location.hostname == tempUrl.hostname) {
2022-12-09 17:07:13 +00:00
// yes, so its a local toot id
temp = temp.split("/")
2022-12-09 17:07:13 +00:00
// handle trailing / case
var tempLast = temp.pop() || temp.pop()
return [false, tempLast]
} else {
// return full URL, since this is already a resolved link to the toot's home instance
return [true, temp]
}
} else {
2022-12-09 17:07:13 +00:00
// no, so it must be a local toot id as well
temp = temp.split("/")
var tempLast = temp.pop() || temp.pop()
return [false, tempLast]
}
}
// get only the toot author handle
function getTootAuthor(el) {
2022-12-09 17:07:13 +00:00
// find the element containing the display name and return text if found
if ($(el).find("span.display-name__account").length) {
return $(el).find("span.display-name__account").first().text().trim()
}
}
// check elements that can contain the local toot id and return it if found
function getTootInternalId(el) {
2022-12-09 17:07:13 +00:00
// detailed status wrapper - get id from current document url
if ($(el).is(".detailed-status__wrapper")) {
// we will use the last part of the URL path - this should be more universal than always selecting the fifth "/" slice
var temp = window.location.href.split("?")[0].split("/")
return (temp.pop() || temp.pop())
2022-12-09 17:07:13 +00:00
// otherwise check if current element has data-id attribute
} else if ($(el).attr("data-id")) {
// split by "-" to respect some ids startin with "f-"
return $(el).attr("data-id").split("-").slice(-1)[0]
2022-12-09 17:07:13 +00:00
// otherwise do the same for any closest article or div with the data-id attribute
} else if ($(el).closest("article[data-id], div[data-id]").length) {
return $(el).closest("article[data-id], div[data-id]").first().attr("data-id").split("-").slice(-1)[0]
}
}
// check elements that can contain an href (either resolved external link or internal reference)
function getTootExtIntHref(el) {
2022-12-09 17:07:13 +00:00
// for each element possibly containing an href, check if its and external fully resolved href or an internal reference and return the first found
if ($(el).find("a.status__relative-time").length) {
return tootHrefCheck($(el).find("a.status__relative-time").first().attr("href").split("?")[0])
} else if ($(el).find("a.detailed-status__datetime").length) {
return tootHrefCheck($(el).find("a.detailed-status__datetime").first().attr("href").split("?")[0])
} else if ($(el).find("a.modal-button").length) {
return tootHrefCheck($(el).find("a.modal-button").first().attr("href").split("?")[0])
}
}
// main function to process each detected toot element
async function process(el) {
// extra step for detailed status elements to select the correct parent
if ($(el).is("div.detailed-status")) {
el = $(el).closest("div.focusable")
}
2022-12-09 17:07:13 +00:00
// get toot data
var tootAuthor = getTootAuthor($(el))
var tootInternalId = getTootInternalId($(el))
var [tootHrefIsExt, tootHrefOrId] = getTootExtIntHref($(el))
2022-12-09 17:07:13 +00:00
// we will always need an internal reference to the toot, be it an actual internal toot id or the href of a toot already resolved to its home
// tootInternalId will be preferred if both are set
var internalIdentifier = tootInternalId || tootHrefOrId
2022-12-09 17:07:13 +00:00
// do we have one of those?
if (internalIdentifier) {
var homeResolveStrings = []
// check if id is already cached
var cacheIndex = isInProcessedToots(internalIdentifier)
// get all button elements of this toot
var favButton = $(el).find("button:has(i.fa-star), a.icon-button:has(i.fa-star)").first()
var boostButton = $(el).find("button:has(i.fa-retweet), a.icon-button:has(i.fa-retweet)").first()
var bookmarkButton = $(el).find("button:has(i.fa-bookmark)").first()
var replyButton = $(el).find("button:has(i.fa-reply), button:has(i.fa-reply-all), a.icon-button:has(i.fa-reply), a.icon-button:has(i.fa-reply-all)").first()
2022-12-09 17:07:13 +00:00
// handles process when a button is clicked
async function clickAction(id, e) {
2022-12-09 17:07:13 +00:00
// determine the action to perform
var action = getTootAction(e)
if (action) {
// resolve url on home instance to get local toot/author identifiers and toot status
var actionExecuted = await executeAction(id, action)
if (actionExecuted) {
2022-12-09 17:07:13 +00:00
// if the action was successfully executed, update the element styles
if (action == "boost" || action == "unboost") {
2022-12-09 17:07:13 +00:00
// toggle inline css styles
toggleInlineCss($(e.currentTarget).find("i"),[["color","!remove","rgb(140, 141, 255)"],["transition-duration", "!remove", "0.9s"],["background-position", "!remove", "0px 100%"]], "fediactive")
2022-12-09 17:07:13 +00:00
// update element in cache if exists
if (cacheIndex) {
processed[cacheIndex][3] = !processed[cacheIndex][3]
}
2022-12-09 17:07:13 +00:00
// same for favourite, bookmarked....
} else if (action == "favourite" || action == "unfavourite") {
toggleInlineCss($(e.currentTarget),[["color","!remove","rgb(202, 143, 4)"]], "fediactive")
if (cacheIndex) {
processed[cacheIndex][4] = !processed[cacheIndex][4]
}
} else {
toggleInlineCss($(e.currentTarget),[["color","!remove","rgb(255, 80, 80)"]], "fediactive")
if (cacheIndex) {
processed[cacheIndex][5] = !processed[cacheIndex][5]
}
2022-12-09 17:07:13 +00:00
}
return true
} else {
log("Could not execute action on home instance.")
return false
}
} else {
log("Could not determine action.")
return false
}
}
2022-12-09 17:07:13 +00:00
// handles initialization of element styles
function initStyles(tootdata) {
2022-12-09 17:07:13 +00:00
// always remove any existing "Unresolved" indicator from the element first
$(el).find(".feditriggered").remove()
2022-12-09 17:07:13 +00:00
// is the toot unresolved?
2022-12-08 15:07:37 +00:00
if (!tootdata[1]) {
2022-12-09 17:07:13 +00:00
// yes, then add the Unresolved indicator
$("<span class='feditriggered' style='color: orange; padding-right: 10px; padding-left: 10px'>Unresolved</span>").insertAfter($(favButton))
2022-12-08 15:07:37 +00:00
} else {
2022-12-09 17:07:13 +00:00
// otherwise start processing button styles
// first enable the bookmark button (is disabled on external instances)
2022-12-08 15:07:37 +00:00
$(bookmarkButton).removeClass("disabled").removeAttr("disabled")
2022-12-09 17:07:13 +00:00
// set the toot buttons to active, depending on the state of the resolved toot and if the element already has the active class
2022-12-08 15:07:37 +00:00
if (tootdata[4]) {
if (!$(favButton).hasClass("fediactive")) {
toggleInlineCss($(favButton),[["color","!remove","rgb(202, 143, 4)"]], "fediactive")
}
}
2022-12-09 17:07:13 +00:00
// repeat for other buttons
2022-12-08 15:07:37 +00:00
if (tootdata[3]) {
if (!$(boostButton).find("i.fediactive").length) {
toggleInlineCss($(boostButton).find("i"),[["color","!remove","rgb(140, 141, 255)"],["transition-duration", "!remove", "0.9s"],["background-position", "!remove", "0px 100%"]], "fediactive")
}
2022-11-21 22:47:32 +00:00
}
2022-12-08 15:07:37 +00:00
if (tootdata[5]) {
if (!$(bookmarkButton).hasClass("fediactive")) {
toggleInlineCss($(bookmarkButton),[["color","!remove","rgb(255, 80, 80)"]], "fediactive")
}
}
}
}
2022-12-09 17:07:13 +00:00
// handles binding of clicks events for all buttons of a toot
function clickBinder(tootdata) {
2022-12-09 17:07:13 +00:00
// reply button is simple, it will always redirect to the homeinstance with the fedireply parameter set
$(replyButton).on("click", function(e){
2022-12-09 17:07:13 +00:00
// prevent default and immediate propagation
e.preventDefault()
e.stopImmediatePropagation()
2022-12-09 17:07:13 +00:00
// redirect to the resolved URL + fedireply parameter (so the extension can handle it after redirect)
redirectTo(tootdata[6]+"?fedireply")
})
2022-12-09 17:07:13 +00:00
// for all other buttons...
$([favButton, boostButton, bookmarkButton]).each(function() {
2022-12-09 17:07:13 +00:00
// these behave differently with single / double click
// we use a custom solution for handling dblclick since the default event does not work here
// init function global vars required for single/double click handling
var clicks = 0
var timer
$(this).on("click", async function(e) {
// prevent default and immediate propagation
e.preventDefault()
e.stopImmediatePropagation()
2022-12-09 17:07:13 +00:00
// increase click counter
clicks++
2022-12-09 17:07:13 +00:00
// this will always run, but see below for double click handling
if (clicks == 1) {
timer = setTimeout(async function() {
// execute action on click and get result (fail/success)
var actionExecuted = await clickAction(tootdata[2], e)
if (!actionExecuted) {
log("Action failed.")
}
2022-12-09 17:07:13 +00:00
// reset clicks
clicks = 0
}, 350)
} else {
2022-12-09 17:07:13 +00:00
// if we get here, the element was clicked twice before the above timeout was over, so this is a double click
// reset the above timeout so it wont execute
clearTimeout(timer)
// same as above, but we redirect if the result is successful
var actionExecuted = await clickAction(tootdata[2], e)
if (!actionExecuted) {
log("Action failed.")
} else {
2022-12-09 17:07:13 +00:00
// redirect to home instance with the resolved toot url
redirectTo(tootdata[6])
}
2022-12-09 17:07:13 +00:00
// reset clicks
clicks = 0
}
}).on("dblclick", function(e) {
// default dblclick event must be prevented
e.preventDefault()
e.stopImmediatePropagation()
})
})
}
// if element is not in cache, resolve it
if (!cacheIndex) {
2022-12-09 17:07:13 +00:00
// always add the already resolved external toot href first, if it was set
if (tootHrefIsExt) {
homeResolveStrings.push(tootHrefOrId)
}
2022-12-09 17:07:13 +00:00
// we can only process internalTootIds if we also have a user handle
if (tootAuthor) {
// get handle/handledomain without @
var matches = tootAuthor.match(handleExtractUrlRegex)
var [isExternalHandle, extHomeResolved] = [false, false]
// if we have a handledomain...
if (matches.groups.handledomain) {
// check if the current hostname includes that handle domain...
if (!(~location.hostname.indexOf(matches.groups.handledomain))) {
isExternalHandle = true
}
}
// add ids
var internalTootIds = [tootInternalId]
if (!tootHrefIsExt) {
internalTootIds.push(tootHrefOrId)
}
// filter duplicates and undefined values (shorter than checking with if clauses when adding...)
internalTootIds = internalTootIds.filter((element, index) => {
return (element !== undefined && internalTootIds.indexOf(element) == index)
})
// loop through internal ids (will be only 1 normally, but we want to maximize our chances for resolving later on)
for (var internalTootId of internalTootIds) {
// if its not an external handle...
if (!isExternalHandle) {
// add resolve strings for both formats on the current external instance
homeResolveStrings.push(location.protocol + "//" + location.hostname + "/users/" + matches.groups.handle + "/statuses/" + internalTootId)
homeResolveStrings.push(location.protocol + "//" + location.hostname + "/@" + matches.groups.handle + "/" + internalTootId)
// otherwise, start external resolve process if not done already for one of the internalTootIds
} else if (!extHomeResolved) {
var extResolveString = location.protocol + '//' + location.hostname + "/" + tootAuthor + "/" + internalTootId
var resolveTootHome = await resolveTootToExternalHome(extResolveString)
if (resolveTootHome) {
// update var so next tootid will not be resolved externally, if any more
extHomeResolved = true
// always push the originally returned url
homeResolveStrings.push(resolveTootHome)
// if it matches the URI format, also add the @ format
if (handleExtractUriRegex.test(resolveTootHome)) {
var tmpmatches = resolveTootHome.match(handleExtractUriRegex)
if (tmpmatches.groups.handle && tmpmatches.groups.tootid && tmpmatches.groups.domain) {
homeResolveStrings.push(tmpmatches.groups.domain + "/@" + tmpmatches.groups.handle + "/" + tmpmatches.groups.tootid)
}
// otherwise, if it matches the @ format, also add the URI format
} else if (handleExtractUrlRegex.test(resolveTootHome)) {
var tmpmatches = resolveTootHome.match(handleExtractUrlRegex)
if (tmpmatches.groups.handle && tmpmatches.groups.tootid && tmpmatches.groups.domain) {
homeResolveStrings.push(tmpmatches.groups.domain + "/users/" + tmpmatches.groups.handle + "/statuses/" + tmpmatches.groups.tootid)
}
}
}
2022-12-09 15:34:38 +00:00
// always add fallback to current external instance URL (for external handles, there is no /users/... format)
homeResolveStrings.push(location.protocol + "//" + location.hostname + "/" + tootAuthor + "/" + internalTootId)
}
}
}
2022-12-09 17:14:48 +00:00
// if we have any resolve strings to resolve on our home instance...
if (homeResolveStrings.length) {
2022-12-09 17:14:48 +00:00
// filter duplicates
homeResolveStrings = homeResolveStrings.filter((element, index) => {
return (homeResolveStrings.indexOf(element) == index)
})
2022-12-09 17:14:48 +00:00
// initialize with false
var resolvedToHomeInstance = false
2022-12-09 17:14:48 +00:00
// for each resolve string...
for (var homeResolveString of homeResolveStrings) {
2022-12-09 17:14:48 +00:00
// run only if not already resolved
if (!resolvedToHomeInstance) {
// resolve toot on actual home instance
var resolvedToot = await resolveTootToHome(homeResolveString) // [status.account.acct, status.id, status.reblogged, status.favourited, status.bookmarked]
if (resolvedToot) {
2022-12-09 17:14:48 +00:00
// if successful, set condition to true (so it will not be resolved twice)
resolvedToHomeInstance = true
// set the redirect to home instance URL in @ format
var redirectUrl = 'https://' + settings.fediact_homeinstance + "/@" + resolvedToot[0] + "/" + resolvedToot[1]
2022-12-09 17:14:48 +00:00
// prepare the cache entry / toot data entry
fullEntry = [internalIdentifier, ...resolvedToot, redirectUrl, true]
}
}
}
2022-12-09 17:14:48 +00:00
// was any resolve successful?
if (resolvedToHomeInstance) {
2022-12-09 17:14:48 +00:00
// yes, so add to processed toots with the full toot data entry
addToProcessedToots(fullEntry)
// continue with click handling...
clickBinder(fullEntry)
2022-12-09 17:14:48 +00:00
// ... and init styles
initStyles(fullEntry)
} else {
2022-12-09 17:14:48 +00:00
// no, but we will still add the toot to cache as unresolved
log("Failed to resolve: "+homeResolveStrings)
addToProcessedToots([internalIdentifier, false])
initStyles([internalIdentifier, false])
}
} else {
2022-12-09 17:14:48 +00:00
// no resolve possible without any resolve strings, but we will still add the toot to cache as unresolved
log("Could not identify a post URI for home resolving.")
addToProcessedToots([internalIdentifier, false])
initStyles([internalIdentifier, false])
}
2022-11-21 22:47:32 +00:00
} else {
2022-12-09 17:14:48 +00:00
// the toot is already in cache, so grab it
var toot = processed[cacheIndex]
2022-12-09 17:14:48 +00:00
// init stylings
initStyles(toot)
2022-12-09 17:14:48 +00:00
// if it is NOT unresolved, bind click handlers again
2022-12-08 15:07:37 +00:00
if (toot[1]) {
clickBinder(toot)
}
2022-11-21 22:47:32 +00:00
}
} else {
log("Could not get toot data.")
}
}
// One DOMNodeAppear to rule them all
$(document).DOMNodeAppear(async function(e) {
process($(e.target))
}, "div.status, div.detailed-status")
2022-11-21 22:47:32 +00:00
}
2022-11-21 22:47:32 +00:00
// main function to listen for the follow button pressed and open a new tab with the home instance
async function processFollow() {
2022-12-08 15:07:37 +00:00
var fullHandle
var action = "follow"
// for mastodon v3 - v4 does not show follow buttons / account cards on /explore
async function process(el) {
// wrapper for follow/unfollow action
2022-12-08 15:07:37 +00:00
async function execFollow(id) {
// execute action and save result
var response = await executeAction(id, action)
// if action was successful, update button text and action value according to performed action
if (action == "follow" && response) {
$(el).text("Unfollow")
action = "unfollow"
return true
2022-12-09 17:14:48 +00:00
// repeat for unfollow action
} else if (action == "unfollow" && response) {
$(el).text("Follow")
action = "follow"
return true
}
}
// for mastodon v3 explore page
if ($(el).closest("div.account-card").length) {
fullHandle = $(el).closest("div.account-card").find("div.display-name > span").text().trim()
} else {
// for all other pages, where only one of the selection elements is present
for (const selector of profileNamePaths) {
if ($(selector).length) {
fullHandle = $(selector).text().trim()
break
}
}
}
2022-12-09 17:14:48 +00:00
// do we have a full handle?
if (fullHandle) {
2022-12-09 17:14:48 +00:00
// yes, so resolve it to a user id on our homeinstance
var resolvedHandle = await resolveHandleToHome(fullHandle)
if (resolvedHandle) {
2022-12-09 17:14:48 +00:00
// successfully resolved
// if showfollows is enabled...
if (settings.fediact_showfollows) {
2022-12-09 17:14:48 +00:00
// ... then check if user is already following
var isFollowing = await isFollowingHomeInstance([resolvedHandle[0]])
2022-12-09 17:14:48 +00:00
// update button text and action if already following
if (isFollowing[0]) {
$(el).text("Unfollow")
action = "unfollow"
}
}
2022-12-09 17:14:48 +00:00
// single and double click handling (see toot processing for explanation, is the same basically)
var clicks = 0
var timer
$(el).on("click", async function(e) {
// prevent default and immediate propagation
e.preventDefault()
e.stopImmediatePropagation()
clicks++
if (clicks == 1) {
timer = setTimeout(async function() {
2022-12-08 15:07:37 +00:00
execFollow(resolvedHandle[0])
clicks = 0
}, 350)
} else {
clearTimeout(timer)
2022-12-08 15:07:37 +00:00
var done = await execFollow(resolvedHandle[0])
if (done) {
var saveText = $(el).text()
var redirectUrl = 'https://' + settings.fediact_homeinstance + '/@' + resolvedHandle[1]
$(el).text("Redirecting...")
setTimeout(function() {
redirectTo(redirectUrl)
$(el).text(saveText)
}, 1000)
} else {
log("Action failed.")
}
clicks = 0
}
}).on("dblclick", function(e) {
e.preventDefault()
e.stopImmediatePropagation()
})
2022-11-21 22:47:32 +00:00
} else {
log("Could not resolve user home ID.")
2022-11-21 22:47:32 +00:00
}
}
}
// create css selector from selector array
var allFollowPaths = followButtonPaths.join(",")
// one domnodeappear to rule them all
$(document).DOMNodeAppear(async function(e) {
process($(e.target))
}, allFollowPaths)
2022-11-21 22:47:32 +00:00
}
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=
// =-=-=-=-=-= SETUP / RUN =-==-=-=-=
// =-=-=-=-==-=-=-=-==-=-=-=-==-=-=-=
// process white/blacklist from ext settings
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 (domain.length) {
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)];
}
2022-11-21 22:47:32 +00:00
function checkSettings() {
// if the home instance is undefined/null/empty
if (settings.fediact_homeinstance == null || !settings.fediact_homeinstance) {
log("Mastodon home instance is not set.")
return false
2022-11-21 22:47:32 +00:00
}
// no token for api available (see background.js)
if (!settings.fediact_token) {
log("No API token available. Are you logged in to your home instance? If yes, wait for 1-2 minutes and reload page.")
return false
} else {
settings.tokenheader = {"Authorization":"Bearer " + settings.fediact_token,}
}
2022-11-21 22:47:32 +00:00
// if the value looks like a domain...
if (!(domainRegex.test(settings.fediact_homeinstance))) {
log("Instance setting is not a valid domain name.")
return false
2022-11-21 22:47:32 +00:00
}
if (settings.fediact_mode == "whitelist") {
2022-11-21 22:47:32 +00:00
// if in whitelist mode and the cleaned whitelist is empty, return false
settings.fediact_whitelist = processDomainList(settings.fediact_whitelist)
if (settings.fediact_whitelist.length < 1) {
2022-11-21 22:47:32 +00:00
log("Whitelist is empty or invalid.")
return false
}
2022-11-21 22:47:32 +00:00
} else {
// also process the blacklist if in blacklist mode, but an empty blacklist is OK so we do not return false
settings.fediact_blacklist = processDomainList(settings.fediact_blacklist)
2022-11-21 22:47:32 +00:00
}
return true
2022-11-21 22:47:32 +00:00
}
// test if the current site should be processed or not
// this will also be the function for whitelist/blacklist feature
async function checkSite() {
2022-11-21 22:47:32 +00:00
// is this site on our home instance?
if (location.hostname == settings.fediact_homeinstance) {
2022-12-04 16:56:42 +00:00
fedireply = getUrlParameter("fedireply")
if (!fedireply) {
log("Current site is your home instance.")
return false
2022-12-04 16:56:42 +00:00
}
2022-11-21 22:47:32 +00:00
}
// are we in whitelist mode?
if (settings.fediact_mode == "whitelist") {
2022-11-21 22:47:32 +00:00
// if so, check if site is NOT in whitelist
if ($.inArray(location.hostname, settings.fediact_whitelist) < 0) {
log("Current site is not in whitelist.")
return false
2022-11-21 22:47:32 +00:00
}
} else {
// otherwise we are in blacklist mode, so check if site is on blacklist
if ($.inArray(location.hostname, settings.fediact_blacklist) > -1) {
log("Current site is in blacklist.")
return false
2022-11-21 22:47:32 +00:00
}
}
// last check - and probably the most accurate to determine if it actually is mastadon
var requestUrl = location.protocol + '//' + location.hostname + instanceApi
2022-11-21 22:47:32 +00:00
// call instance api to confirm its mastodon and get normalized handle uri
var response = await makeRequest("GET", requestUrl, null)
2022-11-21 22:47:32 +00:00
if (response) {
var uri = JSON.parse(response).uri
2022-11-21 22:47:32 +00:00
if (uri) {
// run external mode
return true
}
}
log("Does not look like a Mastodon instance.")
return false
2022-11-21 22:47:32 +00:00
}
async function backgroundProcessor() {
// wait for any url change messages from background script
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.urlchanged) {
// reset already processed elements
processed = []
}
// if the settings were update, we do a page reload
if (request.updatedfedisettings) {
location.reload()
}
})
// send message to initialize onUpdated listener in background script (this way it gets the tabid and we do not need to bind the listener for ALL sites)
try {
await chrome.runtime.sendMessage({running: true})
return true
} catch(e) {
log(e)
}
return false
}
2022-11-21 22:47:32 +00:00
// run wrapper
async function run() {
// get setting
try {
settings = await (browser || chrome).storage.local.get(settingsDefaults)
} catch(e) {
log(e)
return false
}
2022-11-21 22:47:32 +00:00
if (settings) {
// validate settings
if (checkSettings()) {
// check site (if and which scripts should run)
if (await checkSite()) {
2022-12-04 16:56:42 +00:00
if (fedireply) {
processReply()
} else {
if (backgroundProcessor()) {
processFollow()
processToots()
} else {
log("Failed to initialize background script.")
}
2022-12-04 16:56:42 +00:00
}
2022-11-21 22:47:32 +00:00
} else {
log("Will not process this site.")
}
}
} else {
log("Could not load settings.")
}
}
run()