From a61f1d7d71563bb9b2a44408e225e6d3c85bbda7 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 9 Mar 2023 18:43:40 -0800 Subject: [PATCH] Change to a page action instead of a browser action --- .tool-versions | 1 + src/Identity Icon Hover.svg | 4 + src/Info.svg | 4 + src/Information.svg | 4 + src/follow-user.js | 1 - src/manifest.json | 9 ++ src/mastodon-api.js | 56 ++++++----- src/page-action.css | 14 +++ src/page-action.html | 75 ++++++++++++++ src/page-action.js | 196 ++++++++++++++++++++++++++++++++++++ 10 files changed, 339 insertions(+), 25 deletions(-) create mode 100644 .tool-versions create mode 100644 src/Identity Icon Hover.svg create mode 100644 src/Info.svg create mode 100644 src/Information.svg create mode 100644 src/page-action.css create mode 100644 src/page-action.html create mode 100644 src/page-action.js diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..27552eb --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 18.15.0 diff --git a/src/Identity Icon Hover.svg b/src/Identity Icon Hover.svg new file mode 100644 index 0000000..e39e8f4 --- /dev/null +++ b/src/Identity Icon Hover.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/Info.svg b/src/Info.svg new file mode 100644 index 0000000..bb8d266 --- /dev/null +++ b/src/Info.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/Information.svg b/src/Information.svg new file mode 100644 index 0000000..f1c4f5e --- /dev/null +++ b/src/Information.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/follow-user.js b/src/follow-user.js index c59531d..f45cba3 100644 --- a/src/follow-user.js +++ b/src/follow-user.js @@ -1,5 +1,4 @@ (() => { - const defaultServerUrl = 'https://mastodon.social'; browser.storage.sync .get('serverUrl') .then((results) => results.serverUrl || defaultServerUrl) diff --git a/src/manifest.json b/src/manifest.json index da8b9d7..34b156b 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -16,7 +16,9 @@ } }, "permissions": [ + "", "activeTab", + "tabs", "storage", "identity" ], @@ -29,5 +31,12 @@ "page": "options.html", "browser_style": true, "chrome_style": true + }, + "page_action": { + "browser_style": true, + "default_icon": "mastodon-logo-purple.svg", + "default_popup": "page-action.html", + "default_title": "Follow Mastodon User", + "show_matches": [ "https://*/@*" ] } } diff --git a/src/mastodon-api.js b/src/mastodon-api.js index e4daa85..423b73e 100644 --- a/src/mastodon-api.js +++ b/src/mastodon-api.js @@ -13,7 +13,7 @@ async function registerApp(serverUrl, redirectURL) { clientSecret: response.client_secret, appId: response.vapid_key }; - console.log('Registered app', app); + console.log('Registered app', JSON.stringify(app)); return app; } @@ -51,35 +51,43 @@ async function getUserAccessToken(serverUrl, redirectURL, authCode, clientId, cl return token; } -async function lookupAccount(serverUrl, acct) { - console.log('Looking up account', serverUrl, acct); - let lookupUrl = `${serverUrl}/api/v1/accounts/lookup`; - lookupUrl += `?acct=${encodeURIComponent(acct)}`; - lookupUrl += "&skip_webfinger=false"; - - let response = await getJson(lookupUrl, acct); +async function verifyCredentials(serverUrl, token) { + console.log('Verifying credentials', serverUrl, token); + let verifyUrl = `${serverUrl}/api/v1/accounts/verify_credentials`; + let response = await getJson(verifyUrl, null, token); console.log(JSON.stringify(response)); return response; } -async function searchAccount(serverUrl, acct, token) { - console.log('Searching account', serverUrl, acct); - let lookupUrl = `${serverUrl}/api/v1/accounts/search`; - lookupUrl += `?q=${encodeURIComponent(acct)}`; - lookupUrl += "&resolve=true"; +// async function lookupAccount(serverUrl, acct) { +// console.log('Looking up account', serverUrl, acct); +// let lookupUrl = `${serverUrl}/api/v1/accounts/lookup`; +// lookupUrl += `?acct=${encodeURIComponent(acct)}`; +// lookupUrl += "&skip_webfinger=false"; - let response = await getJson(lookupUrl, acct, token); - console.log(JSON.stringify(response)); - return response; -} +// let response = await getJson(lookupUrl, acct); +// console.log(JSON.stringify(response)); +// return response; +// } -async function followAcccount(serverUrl, id, token) { - console.log('Following account', serverUrl, id, token); - let followUrl = `${serverUrl}/api/v1/accounts/${id}/follow`; - let response = await postJson(followUrl, id, token); - console.log(JSON.stringify(response)); - return response; -} +// async function searchAccount(serverUrl, acct, token) { +// console.log('Searching account', serverUrl, acct); +// let lookupUrl = `${serverUrl}/api/v1/accounts/search`; +// lookupUrl += `?q=${encodeURIComponent(acct)}`; +// lookupUrl += "&resolve=true"; + +// let response = await getJson(lookupUrl, acct, token); +// console.log(JSON.stringify(response)); +// return response; +// } + +// async function followAcccount(serverUrl, id, token) { +// console.log('Following account', serverUrl, id, token); +// let followUrl = `${serverUrl}/api/v1/accounts/${id}/follow`; +// let response = await postJson(followUrl, id, token); +// console.log(JSON.stringify(response)); +// return response; +// } function postJson(url, data, token) { return fetchJson('POST', url, data, token); diff --git a/src/page-action.css b/src/page-action.css new file mode 100644 index 0000000..804724b --- /dev/null +++ b/src/page-action.css @@ -0,0 +1,14 @@ +.hidden { + display: none; +} + +.panel-section-message { + flex-direction: column; + padding: 4px 0; +} + +.panel-message-item { + padding: 4px 16px; + display: flex; + flex-direction:row; +} \ No newline at end of file diff --git a/src/page-action.html b/src/page-action.html new file mode 100644 index 0000000..8dc083a --- /dev/null +++ b/src/page-action.html @@ -0,0 +1,75 @@ + + + + + + + + Follow Mastodon User + + +
+
+ Follow Mastodon User +
+
+ +
+
+ Not logged in, visit extension options to login. +
+
+ +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/src/page-action.js b/src/page-action.js new file mode 100644 index 0000000..1b2c183 --- /dev/null +++ b/src/page-action.js @@ -0,0 +1,196 @@ +async function checkLoggedIn() { + try { + let settings = await browser.storage.sync.get(['accessToken', 'serverUrl']); + + if (!!settings && !!settings.accessToken) { + document.querySelector("#debug-serverUrl").textContent = settings.serverUrl; + document.querySelector("#debug-accessToken").textContent = settings.accessToken; + + document.querySelector("#not-logged-in").classList.add("hidden"); + document.querySelector("#logged-in").classList.remove("hidden"); + document.querySelector("#actions").classList.remove("hidden"); + + let myAccount = await verify(settings); + document.querySelector("#myUsername").textContent = myAccount.display_name + "(@" + myAccount.username + ")"; + + let username = await getUsername(); + document.querySelector("#debug-username").textContent = username; + + let account = await lookupAccount(settings.serverUrl, settings.accessToken, username); + document.querySelector("#debug-account").textContent = account.id; + + let relationship = await getRelationship(settings.serverUrl, settings.accessToken, account.id); + console.log(relationship); + document.querySelector('#debug-relationship').textContent = JSON.stringify(relationship); + + if (relationship[0].following) { + document.querySelector("#unfollow").classList.remove("hidden"); + } else { + document.querySelector("#follow").classList.remove("hidden"); + } + + if (relationship[0].blocking) { + document.querySelector("#unblock").classList.remove("hidden"); + } else { + document.querySelector("#block").classList.remove("hidden"); + } + + if (relationship[0].muting) { + document.querySelector("#unmute").classList.remove("hidden"); + } else + { + document.querySelector("#mute").classList.remove("hidden"); + } + } + } catch (err) { + document.querySelector("#debug-section").classList.remove("hidden"); + document.querySelector("#debug-message").value = "Error: " + JSON.stringify(err); + } +} + +async function getUsername() { + try { + let results = await browser.tabs.executeScript({ + code: "(function() { let username=document.querySelector('meta[property=\"profile:username\"]'); return username && username.content; })();" + }); + return results && results[0]; + } catch (err) { + throw new Error("Error getting username: " + err); + } +} + +async function verify(settings) { + try { + const response = await fetch(settings.serverUrl + '/api/v1/accounts/verify_credentials', { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + settings.accessToken + }, + method: 'GET' + }); + return await response.json(); + } catch (err) { + throw new Error("Error verifying credentials: " + err); + } +} + +async function lookupAccount(serverUrl, accessToken, username) { + try { + const url = serverUrl + + '/api/v1/accounts/lookup' + + '?acct=' + encodeURIComponent(username) + + '&skip_webfinger=false' + const response = await fetch(url, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + accessToken + }, + method: 'GET' + }); + return await response.json(); + } catch (err) { + throw new Error("Error looking up account: " + err); + } +} + +async function getRelationship(serverUrl, accessToken, accountId) { + try { + const url = serverUrl + + '/api/v1/accounts/relationships' + + '?id[]=' + encodeURIComponent(accountId) + const response = await fetch(url, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + accessToken + }, + method: 'GET' + }); + return await response.json(); + } catch (err) { + throw new Error("Error getting relationship: " + err); + } +} + +async function accountAction(action) { + try { + let settings = await getLoggedInUser(); + let account = document.querySelector("#debug-account").textContent; + console.log("Attempting to " + action + " user " + account); + const response = await fetch(settings.serverUrl + '/api/v1/accounts/' + account + '/' + action, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + settings.accessToken + }, + method: 'POST' + }); + if (response.status != 200) { + throw new Error("Error following user: " + response.status + " " + response.statusText + + " " + await response.text()); + } + } catch (err) { + throw new Error("Error attempting to " + action + " user: " + err); + } +} + +async function follow() { + await accountAction('follow'); + document.querySelector("#follow").classList.add("hidden"); + document.querySelector("#unfollow").classList.remove("hidden"); +} + +async function unfollow() { + await accountAction('unfollow'); + document.querySelector("#unfollow").classList.add("hidden"); + document.querySelector("#follow").classList.remove("hidden"); +} + +async function block() { + await accountAction('block'); + document.querySelector("#block").classList.add("hidden"); + document.querySelector("#unblock").classList.remove("hidden"); +} + +async function unblock() { + await accountAction('unblock'); + document.querySelector("#unblock").classList.add("hidden"); + document.querySelector("#block").classList.remove("hidden"); +} + +async function mute() { + await accountAction('mute'); + document.querySelector("#mute").classList.add("hidden"); + document.querySelector("#unmute").classList.remove("hidden"); +} + +async function unmute() { + await accountAction('unmute'); + document.querySelector("#unmute").classList.add("hidden"); + document.querySelector("#mute").classList.remove("hidden"); +} + +async function getLoggedInUser() { + let settings = await browser.storage.sync.get(['accessToken', 'serverUrl']); + if (!settings || !settings.accessToken) { + throw "No access token found. Please log in."; + } + return settings; +} + +async function openOptionsPage() { + await browser.runtime.openOptionsPage(); +} + +document.querySelector('#openOptionsPage').addEventListener('click', openOptionsPage); +document.querySelector('#follow').addEventListener('click', follow); +document.querySelector('#unfollow').addEventListener('click', unfollow); +document.querySelector('#block').addEventListener('click', block); +document.querySelector('#unblock').addEventListener('click', unblock); +document.querySelector('#mute').addEventListener('click', mute); +document.querySelector('#unmute').addEventListener('click', unmute); + +document.addEventListener('DOMContentLoaded', checkLoggedIn); +