this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #5 from sanitybit/1.2.0

authored by

Jesse J. Anderson and committed by
GitHub
38655484 a0bdcc22

+359 -58
+2
.gitignore
··· 1 1 **/.DS_Store 2 + extension_packages/* 3 + extension_packages
+5
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ## [1.2.0] - 2023-05-03 11 + 12 + - Adds support for Firefox 13 + - Adds privacy consent dialog for Google DNS (required by Mozilla) 14 + 10 15 ## [1.1.0] - 2023-05-03 11 16 12 17 - use DID for profile url instead of domain name
+5 -1
README.md
··· 2 2 3 3 [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/aflpfginfpjhanhkmdpohpggpolfopmb)](https://chrome.google.com/webstore/detail/skylink-bluesky-did-detector/aflpfginfpjhanhkmdpohpggpolfopmb) 4 4 5 - A simple Chrome extension that detects if the current website is connected to a Bluesky user. 5 + A simple web extension that detects if the current website is connected to a Bluesky user. 6 6 7 7 Remember the good 'ol days of visiting someone's blog and being delighted when the "RSS" lit up in your browser? This is meant to capture that same magic. No more hunting on a page for a random bird icon to see if you can find their online profile. 8 8 ··· 17 17 As of April 30, 2023, Bluesky is still invite-only and the web app is at https://staging.bsky.app. 18 18 19 19 You can find me there at [@adhdjesse.com](https://staging.bsky.app/profile/adhdjesse.com) 20 + 21 + **Contributors:** 22 + 23 + [@danielhuckmann.com](https://staging.bsky.app/profile/danielhuckmann.com) - Firefox Support & Privacy Consent
+56 -19
background.js
··· 1 - const tabsWithDID = new Map() 1 + // Set up cross-browser compatibility 2 + const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime; 3 + const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs; 4 + const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local; 5 + const action = typeof browser !== 'undefined' ? browser.browserAction : chrome.action; 2 6 3 - const bskyAppUrl = "https://staging.bsky.app" 7 + // On extension installation, check if privacy consent was already accepted and show it if not 8 + runtime.onInstalled.addListener(() => { 9 + storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => { 10 + if (typeof privacyConsentAccepted === "undefined" || !privacyConsentAccepted) { 11 + tabs.create({ url: "privacy_consent.html" }); 12 + } 13 + }); 14 + }); 4 15 16 + // If the message 'SHOW_CONSENT' is received, open the privacy consent tab 17 + runtime.onMessage.addListener((message, sender, sendResponse) => { 18 + if (message.type === 'SHOW_CONSENT') { 19 + tabs.create({ url: "privacy_consent.html" }); 20 + } 21 + }); 22 + 23 + // Map to store tabs with DIDs 24 + const tabsWithDID = new Map(); 25 + 26 + // URL of the Bluesky Web Applications 27 + const bskyAppUrl = 'https://staging.bsky.app'; 28 + 29 + // Function to set the extension icon 5 30 function setIcon(tabId, iconName) { 6 - chrome.action.setIcon({ path: iconName, tabId }) 31 + action.setIcon({ path: iconName, tabId }); 7 32 } 8 33 9 - chrome.runtime.onInstalled.addListener(() => { 10 - chrome.tabs.query({}, (tabs) => { 11 - tabs.forEach((tab) => setIcon(tab.id, "logo48_gray.png")) 12 - }) 13 - }) 34 + // On extension installation, set the icon to gray for all tabs 35 + runtime.onInstalled.addListener(() => { 36 + tabs.query({}, (tabs) => { 37 + tabs.forEach((tab) => setIcon(tab.id, 'logo48_gray.png')); 38 + }); 39 + }); 14 40 15 - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 41 + // When a message is received from the DNS check, set the icon color to blue. 42 + runtime.onMessage.addListener((message, sender, sendResponse) => { 16 43 if (message.type === "DID_FOUND") { 17 - setIcon(sender.tab.id, "logo48.png") 18 - tabsWithDID.set(sender.tab.id, message.did) 44 + setIcon(sender.tab.id, "logo48.png"); 45 + tabsWithDID.set(sender.tab.id, message.did); 19 46 } else { 20 - setIcon(sender.tab.id, "logo48_gray.png") 21 - tabsWithDID.delete(sender.tab.id) 47 + setIcon(sender.tab.id, "logo48_gray.png"); 48 + tabsWithDID.delete(sender.tab.id); 22 49 } 23 - }) 50 + }); 51 + 52 + // Open the consent page if it hasn't been accepted and the user clicks on the extension icon 53 + action.onClicked.addListener((tab) => { 54 + storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => { 55 + if (typeof privacyConsentAccepted === "undefined" || !privacyConsentAccepted) { 56 + tabs.create({ url: "privacy_consent.html" }); 57 + } 58 + }); 59 + }); 24 60 25 - chrome.action.onClicked.addListener((tab) => { 26 - const did = tabsWithDID.get(tab.id) 61 + // When the extension icon is clicked, open the profile page if there's a DID 62 + action.onClicked.addListener((tab) => { 63 + const did = tabsWithDID.get(tab.id); 27 64 if (did) { 28 - const newUrl = `${bskyAppUrl}/profile/${did}` 29 - chrome.tabs.create({ url: newUrl }) 65 + const newUrl = `${bskyAppUrl}/profile/${did}`; 66 + tabs.create({ url: newUrl }); 30 67 } 31 - }) 68 + });
+86
build_extension.sh
··· 1 + #!/bin/sh 2 + 3 + # This was tested on macOS 13.3.1 using the builtin zip command at /usr/bin/zip 4 + # It should also work on Linux, but I haven't tested it. 5 + 6 + check_zip() { 7 + if ! command -v zip >/dev/null 2>&1; then 8 + printf "zip command not found. Attempting to install...\n" 9 + if command -v apt-get >/dev/null 2>&1; then 10 + sudo apt-get install zip 11 + elif command -v dnf >/dev/null 2>&1; then 12 + sudo dnf install zip 13 + else 14 + printf "Cannot determine package manager. Please install 'zip' manually.\n" 15 + exit 1 16 + fi 17 + fi 18 + } 19 + 20 + check_zip 21 + 22 + clean() { 23 + rm -rf ./extension_packages/ff_build 24 + rm -rf ./extension_packages/chrome_build 25 + } 26 + 27 + build_firefox() { 28 + printf "Building Firefox Extension\n\n" 29 + mkdir -p ./extension_packages/ff_build 30 + cp manifest-firefox.json ./extension_packages/ff_build/manifest.json 31 + cp ./*.png ./*.js ./*.html ./*.md LICENSE ./extension_packages/ff_build/ 32 + zip -j ./extension_packages/skylink-firefox.zip ./extension_packages/ff_build/* 33 + printf "\nFirefox extension available at ./extension_packages/skylink-firefox.zip\n" 34 + } 35 + 36 + build_chrome() { 37 + printf "Building Chrome Extension\n\n" 38 + mkdir -p ./extension_packages/chrome_build 39 + cp manifest.json ./extension_packages/chrome_build/manifest.json 40 + cp ./*.png ./*.js ./*.html ./*.md LICENSE ./extension_packages/chrome_build/ 41 + zip -j ./extension_packages/skylink-chrome.zip ./extension_packages/chrome_build/* 42 + printf "\nChrome extension available at ./extension_packages/skylink-chrome.zip\n" 43 + } 44 + 45 + display_help() { 46 + echo "Usage: $0 [--firefox] [--chrome] [--all] [--help]" 47 + echo "Options:" 48 + echo " --firefox Build Firefox extension" 49 + echo " --chrome Build Chrome extension" 50 + echo " --all Build both Firefox and Chrome extensions" 51 + echo " --help Display this help message" 52 + } 53 + 54 + if [ $# -eq 0 ]; then 55 + display_help 56 + exit 0 57 + fi 58 + 59 + while [ $# -gt 0 ]; do 60 + case "$1" in 61 + --firefox) 62 + clean 63 + build_firefox 64 + ;; 65 + --chrome) 66 + clean 67 + build_chrome 68 + ;; 69 + --all) 70 + clean 71 + build_firefox 72 + printf "\n\n" 73 + build_chrome 74 + ;; 75 + --help) 76 + display_help 77 + exit 0 78 + ;; 79 + *) 80 + echo "Invalid option: $1" 81 + display_help 82 + exit 1 83 + ;; 84 + esac 85 + shift 86 + done
+57 -36
content.js
··· 1 - function getDomainName() { 2 - const hostname = window.location.hostname 3 - return hostname.replace(/^www\./, "") 4 - } 1 + // Set up cross-browser compatibility 2 + const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime; 3 + const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs; 4 + const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local; 5 + 6 + // Main function to perform actions, but only if the privacy consent has been accepted 7 + function performAction(privacyConsentAccepted) { 8 + // If the user has accepted the privacy consent 9 + if (privacyConsentAccepted) { 10 + // Function to get the domain name from the current hostname 11 + function getDomainName() { 12 + const hostname = window.location.hostname; 13 + return hostname.replace(/^www\./, ''); 14 + } 15 + 16 + // Function to check for a DID in the domain's TXT records 17 + async function checkForDID(domain) { 18 + // We use Google's DNS over HTTPS API to resolve the TXT record 19 + const response = await fetch( 20 + `https://dns.google/resolve?name=_atproto.${domain}&type=TXT` 21 + ); 22 + const data = await response.json(); 5 23 6 - async function checkForDID(domain) { 7 - // We use Google's DNS over HTTPS API to resolve the TXT record 8 - const response = await fetch( 9 - `https://dns.google/resolve?name=_atproto.${domain}&type=TXT` 10 - ) 11 - const data = await response.json() 24 + // We use the TXT record type to avoid CORS issues 25 + const records = data?.Answer?.filter((record) => record.type === 16) || []; 12 26 13 - // We use the TXT record type to avoid CORS issues 14 - const records = data?.Answer?.filter((record) => record.type === 16) || [] 27 + // We filter out all records that are not TXT records 28 + const didRecord = records.find((record) => 29 + record.data.includes("did=did:plc:") 30 + ); 15 31 16 - // We filter out all records that are not TXT records 17 - const didRecord = records.find((record) => 18 - record.data.includes("did=did:plc:") 19 - ) 32 + // We return the DID if we found one 33 + return didRecord ? didRecord.data.replace("did=", "") : null; 34 + } 20 35 21 - // We return the DID if we found one 22 - return didRecord ? didRecord.data.replace("did=", "") : null 23 - } 36 + // We check for a DID on the current domain 37 + ;(async function () { 38 + const domain = getDomainName() 39 + const did = await checkForDID(domain) 24 40 25 - // We check for a DID on the current domain 26 - ;(async function () { 27 - const domain = getDomainName() 28 - const did = await checkForDID(domain) 41 + if (did) { 42 + chrome.runtime.sendMessage({ type: "DID_FOUND", did }) 43 + } else { 44 + chrome.runtime.sendMessage({ type: "DID_NOT_FOUND" }) 45 + } 46 + })(); 29 47 30 - if (did) { 31 - chrome.runtime.sendMessage({ type: "DID_FOUND", did }) 48 + // We listen for messages from the background script 49 + runtime.onMessage.addListener((message, sender, sendResponse) => { 50 + if (message.type === "GET_DID") { 51 + checkForDID(getDomainName()) 52 + .then((did) => sendResponse({ did })) 53 + .catch(() => sendResponse({ did: null })) 54 + return true // Indicate that the response will be sent asynchronously. 55 + } 56 + }); 32 57 } else { 33 - chrome.runtime.sendMessage({ type: "DID_NOT_FOUND" }) 58 + // Do nothing since the consent form has not been accepted. 59 + return; 34 60 } 35 - })() 61 + } 36 62 37 - // We listen for messages from the background script 38 - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 39 - if (message.type === "GET_DID") { 40 - checkForDID(getDomainName()) 41 - .then((did) => sendResponse({ did })) 42 - .catch(() => sendResponse({ did: null })) 43 - return true // Indicate that the response will be sent asynchronously. 44 - } 45 - }) 63 + // Get the user's privacy consent from the storage and perform actions accordingly 64 + storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => { 65 + performAction(privacyConsentAccepted); 66 + });
+35
manifest-firefox.json
··· 1 + { 2 + "manifest_version": 2, 3 + "name": "SkyLink - Bluesky DID Detector", 4 + "short_name": "SkyLink", 5 + "version": "1.2.0", 6 + "author": "jesse@adhdjesse.com", 7 + "browser_action": { 8 + "default_icon": { 9 + "48": "logo48_gray.png", 10 + "128": "logo128_gray.png" 11 + } 12 + }, 13 + "icons": { 14 + "48": "logo48.png", 15 + "128": "logo128.png" 16 + }, 17 + "description": "Detects Decentralized Identifiers (DIDs) in a domain's TXT records and links to the associated Bluesky profile.", 18 + "permissions": ["activeTab", "tabs", "management", "storage", "<all_urls>"], 19 + "content_security_policy": "script-src 'self'; object-src 'self'", 20 + "background": { 21 + "scripts": ["background.js"] 22 + }, 23 + "content_scripts": [ 24 + { 25 + "matches": ["<all_urls>"], 26 + "js": ["content.js"] 27 + } 28 + ], 29 + "browser_specific_settings": { 30 + "gecko": { 31 + "id": "jesse@adhdjesse.com", 32 + "strict_min_version": "79.0" 33 + } 34 + } 35 + }
+2 -2
manifest.json
··· 2 2 "manifest_version": 3, 3 3 "name": "SkyLink - Bluesky DID Detector", 4 4 "short_name": "SkyLink", 5 - "version": "1.1.0", 5 + "version": "1.2.0", 6 6 "author": "jesse@adhdjesse.com", 7 7 "action": { 8 8 "default_icon": { "48": "logo48_gray.png", "128": "logo128_gray.png" } 9 9 }, 10 10 "icons": { "48": "logo48.png", "128": "logo128.png" }, 11 11 "description": "Detects Decentralized Identifiers (DIDs) in a domain's TXT records and links to the associated Bluesky profile.", 12 - "permissions": ["activeTab", "tabs"], 12 + "permissions": ["activeTab", "tabs", "storage", "management"], 13 13 "background": { "service_worker": "background.js" }, 14 14 "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.js"] }] 15 15 }
+98
privacy_consent.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Skylink Privacy Consent</title> 7 + <style> 8 + html { 9 + line-height: 1.15; 10 + -webkit-text-size-adjust: 100%; 11 + } 12 + 13 + body { 14 + margin: 0; 15 + } 16 + 17 + h1 { 18 + font-size: 2em; 19 + margin: 0.67em 0; 20 + } 21 + 22 + a { 23 + background-color: transparent; 24 + } 25 + 26 + p { 27 + margin: 1em 0; 28 + } 29 + 30 + ul { 31 + margin: 1em 0; 32 + padding: 0 0 0 40px; 33 + } 34 + 35 + li { 36 + list-style-type: disc; 37 + margin: 0 0 0.25em 0; 38 + } 39 + 40 + button { 41 + font-family: inherit; 42 + font-size: 100%; 43 + line-height: 1.15; 44 + margin: 0; 45 + overflow: visible; 46 + text-transform: none; 47 + } 48 + 49 + button::-moz-focus-inner { 50 + border-style: none; 51 + padding: 0; 52 + } 53 + 54 + button:-moz-focusring { 55 + outline: 1px dotted ButtonText; 56 + } 57 + .centered-container { 58 + position: absolute; 59 + top: 50%; 60 + left: 50%; 61 + transform: translate(-50%, -50%); 62 + } 63 + </style> 64 + </head> 65 + <body> 66 + <div class="centered-container"> 67 + <h1>Privacy Consent for SkyLink - Bluesky DID Detector</h1> 68 + <p>While <b>no information is collected by the extension author</b>, this extension makes use of the <a href="https://developers.google.com/speed/public-dns/">Google DNS service</a> for required functionality.</p> 69 + <p>The Google DNS service is used because browser extensions do not have native access to DNS, which is needed to lookup the AT PROTO TXT record.</p> 70 + <p>A summary of what is collected by Google DNS: 71 + <ul> 72 + <li>the IP address of your device sending the DNS query</li> 73 + <li>Requested domain name</li> 74 + <li>Request type (TXT)</li> 75 + <li>Request Size</li> 76 + <li>Transport protocol (HTTPS)</li> 77 + <li>Client's autonomous system number</li> 78 + <li>User's geolocation: country, region, and city (no more specific than 1 km² and 1000 users)</li> 79 + <li>DNS Response code</li> 80 + <li>Google DNS server information</li> 81 + <li>Timestamp</li> 82 + <li>Processing time</li> 83 + <li>Response DNS flags (including AD, CD, DO, RD, and TC)</li> 84 + <li>Response size</li> 85 + <li>EDNS version</li> 86 + <li>EDNS option</li> 87 + <li>EDNS Client Subnet (ECS) (IP protocol and prefix length -- excluding the client IP address)</li> 88 + <li>Version string corresponding to HTTP path (/resolve)</li> 89 + <li>Response HTTP encoding, such as application/dns-message or json</li> 90 + </ul> 91 + </p> 92 + <p><a href="https://developers.google.com/speed/public-dns/privacy" target="_blank">Please Read and accept the full Google DNS privacy policy before using this addon.</a></p> 93 + <button id="accept">Accept (Continue)</button> 94 + <button id="decline">Decline (Remove Extension)</button> 95 + <script src="privacy_consent.js"></script> 96 + </div> 97 + </body> 98 + </html>
+13
privacy_consent.js
··· 1 + // Set up cross-browser compatibility 2 + const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime; 3 + const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local; 4 + const management = typeof browser !== 'undefined' ? browser.management : chrome.management; 5 + 6 + document.getElementById("accept").addEventListener("click", function () { 7 + storage.set({ privacyConsentAccepted: true }); 8 + window.close(); 9 + }); 10 + 11 + document.getElementById("decline").addEventListener("click", function () { 12 + management.uninstallSelf({ showConfirmDialog: true }); 13 + });