···7788## [Unreleased]
991010+## [1.2.0] - 2023-05-03
1111+1212+- Adds support for Firefox
1313+- Adds privacy consent dialog for Google DNS (required by Mozilla)
1414+1015## [1.1.0] - 2023-05-03
11161217- use DID for profile url instead of domain name
+5-1
README.md
···2233[](https://chrome.google.com/webstore/detail/skylink-bluesky-did-detector/aflpfginfpjhanhkmdpohpggpolfopmb)
4455-A simple Chrome extension that detects if the current website is connected to a Bluesky user.
55+A simple web extension that detects if the current website is connected to a Bluesky user.
6677Remember 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.
88···1717As of April 30, 2023, Bluesky is still invite-only and the web app is at https://staging.bsky.app.
18181919You can find me there at [@adhdjesse.com](https://staging.bsky.app/profile/adhdjesse.com)
2020+2121+**Contributors:**
2222+2323+[@danielhuckmann.com](https://staging.bsky.app/profile/danielhuckmann.com) - Firefox Support & Privacy Consent
+56-19
background.js
···11-const tabsWithDID = new Map()
11+// Set up cross-browser compatibility
22+const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime;
33+const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs;
44+const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;
55+const action = typeof browser !== 'undefined' ? browser.browserAction : chrome.action;
2633-const bskyAppUrl = "https://staging.bsky.app"
77+// On extension installation, check if privacy consent was already accepted and show it if not
88+runtime.onInstalled.addListener(() => {
99+ storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
1010+ if (typeof privacyConsentAccepted === "undefined" || !privacyConsentAccepted) {
1111+ tabs.create({ url: "privacy_consent.html" });
1212+ }
1313+ });
1414+});
4151616+// If the message 'SHOW_CONSENT' is received, open the privacy consent tab
1717+runtime.onMessage.addListener((message, sender, sendResponse) => {
1818+ if (message.type === 'SHOW_CONSENT') {
1919+ tabs.create({ url: "privacy_consent.html" });
2020+ }
2121+});
2222+2323+// Map to store tabs with DIDs
2424+const tabsWithDID = new Map();
2525+2626+// URL of the Bluesky Web Applications
2727+const bskyAppUrl = 'https://staging.bsky.app';
2828+2929+// Function to set the extension icon
530function setIcon(tabId, iconName) {
66- chrome.action.setIcon({ path: iconName, tabId })
3131+ action.setIcon({ path: iconName, tabId });
732}
83399-chrome.runtime.onInstalled.addListener(() => {
1010- chrome.tabs.query({}, (tabs) => {
1111- tabs.forEach((tab) => setIcon(tab.id, "logo48_gray.png"))
1212- })
1313-})
3434+// On extension installation, set the icon to gray for all tabs
3535+runtime.onInstalled.addListener(() => {
3636+ tabs.query({}, (tabs) => {
3737+ tabs.forEach((tab) => setIcon(tab.id, 'logo48_gray.png'));
3838+ });
3939+});
14401515-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
4141+// When a message is received from the DNS check, set the icon color to blue.
4242+runtime.onMessage.addListener((message, sender, sendResponse) => {
1643 if (message.type === "DID_FOUND") {
1717- setIcon(sender.tab.id, "logo48.png")
1818- tabsWithDID.set(sender.tab.id, message.did)
4444+ setIcon(sender.tab.id, "logo48.png");
4545+ tabsWithDID.set(sender.tab.id, message.did);
1946 } else {
2020- setIcon(sender.tab.id, "logo48_gray.png")
2121- tabsWithDID.delete(sender.tab.id)
4747+ setIcon(sender.tab.id, "logo48_gray.png");
4848+ tabsWithDID.delete(sender.tab.id);
2249 }
2323-})
5050+});
5151+5252+// Open the consent page if it hasn't been accepted and the user clicks on the extension icon
5353+action.onClicked.addListener((tab) => {
5454+ storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
5555+ if (typeof privacyConsentAccepted === "undefined" || !privacyConsentAccepted) {
5656+ tabs.create({ url: "privacy_consent.html" });
5757+ }
5858+ });
5959+});
24602525-chrome.action.onClicked.addListener((tab) => {
2626- const did = tabsWithDID.get(tab.id)
6161+// When the extension icon is clicked, open the profile page if there's a DID
6262+action.onClicked.addListener((tab) => {
6363+ const did = tabsWithDID.get(tab.id);
2764 if (did) {
2828- const newUrl = `${bskyAppUrl}/profile/${did}`
2929- chrome.tabs.create({ url: newUrl })
6565+ const newUrl = `${bskyAppUrl}/profile/${did}`;
6666+ tabs.create({ url: newUrl });
3067 }
3131-})
6868+});
+86
build_extension.sh
···11+#!/bin/sh
22+33+# This was tested on macOS 13.3.1 using the builtin zip command at /usr/bin/zip
44+# It should also work on Linux, but I haven't tested it.
55+66+check_zip() {
77+ if ! command -v zip >/dev/null 2>&1; then
88+ printf "zip command not found. Attempting to install...\n"
99+ if command -v apt-get >/dev/null 2>&1; then
1010+ sudo apt-get install zip
1111+ elif command -v dnf >/dev/null 2>&1; then
1212+ sudo dnf install zip
1313+ else
1414+ printf "Cannot determine package manager. Please install 'zip' manually.\n"
1515+ exit 1
1616+ fi
1717+ fi
1818+}
1919+2020+check_zip
2121+2222+clean() {
2323+ rm -rf ./extension_packages/ff_build
2424+ rm -rf ./extension_packages/chrome_build
2525+}
2626+2727+build_firefox() {
2828+ printf "Building Firefox Extension\n\n"
2929+ mkdir -p ./extension_packages/ff_build
3030+ cp manifest-firefox.json ./extension_packages/ff_build/manifest.json
3131+ cp ./*.png ./*.js ./*.html ./*.md LICENSE ./extension_packages/ff_build/
3232+ zip -j ./extension_packages/skylink-firefox.zip ./extension_packages/ff_build/*
3333+ printf "\nFirefox extension available at ./extension_packages/skylink-firefox.zip\n"
3434+}
3535+3636+build_chrome() {
3737+ printf "Building Chrome Extension\n\n"
3838+ mkdir -p ./extension_packages/chrome_build
3939+ cp manifest.json ./extension_packages/chrome_build/manifest.json
4040+ cp ./*.png ./*.js ./*.html ./*.md LICENSE ./extension_packages/chrome_build/
4141+ zip -j ./extension_packages/skylink-chrome.zip ./extension_packages/chrome_build/*
4242+ printf "\nChrome extension available at ./extension_packages/skylink-chrome.zip\n"
4343+}
4444+4545+display_help() {
4646+ echo "Usage: $0 [--firefox] [--chrome] [--all] [--help]"
4747+ echo "Options:"
4848+ echo " --firefox Build Firefox extension"
4949+ echo " --chrome Build Chrome extension"
5050+ echo " --all Build both Firefox and Chrome extensions"
5151+ echo " --help Display this help message"
5252+}
5353+5454+if [ $# -eq 0 ]; then
5555+ display_help
5656+ exit 0
5757+fi
5858+5959+while [ $# -gt 0 ]; do
6060+ case "$1" in
6161+ --firefox)
6262+ clean
6363+ build_firefox
6464+ ;;
6565+ --chrome)
6666+ clean
6767+ build_chrome
6868+ ;;
6969+ --all)
7070+ clean
7171+ build_firefox
7272+ printf "\n\n"
7373+ build_chrome
7474+ ;;
7575+ --help)
7676+ display_help
7777+ exit 0
7878+ ;;
7979+ *)
8080+ echo "Invalid option: $1"
8181+ display_help
8282+ exit 1
8383+ ;;
8484+ esac
8585+ shift
8686+done
+57-36
content.js
···11-function getDomainName() {
22- const hostname = window.location.hostname
33- return hostname.replace(/^www\./, "")
44-}
11+// Set up cross-browser compatibility
22+const runtime = typeof browser !== 'undefined' ? browser.runtime : chrome.runtime;
33+const tabs = typeof browser !== 'undefined' ? browser.tabs : chrome.tabs;
44+const storage = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;
55+66+// Main function to perform actions, but only if the privacy consent has been accepted
77+function performAction(privacyConsentAccepted) {
88+ // If the user has accepted the privacy consent
99+ if (privacyConsentAccepted) {
1010+ // Function to get the domain name from the current hostname
1111+ function getDomainName() {
1212+ const hostname = window.location.hostname;
1313+ return hostname.replace(/^www\./, '');
1414+ }
1515+1616+ // Function to check for a DID in the domain's TXT records
1717+ async function checkForDID(domain) {
1818+ // We use Google's DNS over HTTPS API to resolve the TXT record
1919+ const response = await fetch(
2020+ `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
2121+ );
2222+ const data = await response.json();
52366-async function checkForDID(domain) {
77- // We use Google's DNS over HTTPS API to resolve the TXT record
88- const response = await fetch(
99- `https://dns.google/resolve?name=_atproto.${domain}&type=TXT`
1010- )
1111- const data = await response.json()
2424+ // We use the TXT record type to avoid CORS issues
2525+ const records = data?.Answer?.filter((record) => record.type === 16) || [];
12261313- // We use the TXT record type to avoid CORS issues
1414- const records = data?.Answer?.filter((record) => record.type === 16) || []
2727+ // We filter out all records that are not TXT records
2828+ const didRecord = records.find((record) =>
2929+ record.data.includes("did=did:plc:")
3030+ );
15311616- // We filter out all records that are not TXT records
1717- const didRecord = records.find((record) =>
1818- record.data.includes("did=did:plc:")
1919- )
3232+ // We return the DID if we found one
3333+ return didRecord ? didRecord.data.replace("did=", "") : null;
3434+ }
20352121- // We return the DID if we found one
2222- return didRecord ? didRecord.data.replace("did=", "") : null
2323-}
3636+ // We check for a DID on the current domain
3737+ ;(async function () {
3838+ const domain = getDomainName()
3939+ const did = await checkForDID(domain)
24402525-// We check for a DID on the current domain
2626-;(async function () {
2727- const domain = getDomainName()
2828- const did = await checkForDID(domain)
4141+ if (did) {
4242+ chrome.runtime.sendMessage({ type: "DID_FOUND", did })
4343+ } else {
4444+ chrome.runtime.sendMessage({ type: "DID_NOT_FOUND" })
4545+ }
4646+ })();
29473030- if (did) {
3131- chrome.runtime.sendMessage({ type: "DID_FOUND", did })
4848+ // We listen for messages from the background script
4949+ runtime.onMessage.addListener((message, sender, sendResponse) => {
5050+ if (message.type === "GET_DID") {
5151+ checkForDID(getDomainName())
5252+ .then((did) => sendResponse({ did }))
5353+ .catch(() => sendResponse({ did: null }))
5454+ return true // Indicate that the response will be sent asynchronously.
5555+ }
5656+ });
3257 } else {
3333- chrome.runtime.sendMessage({ type: "DID_NOT_FOUND" })
5858+ // Do nothing since the consent form has not been accepted.
5959+ return;
3460 }
3535-})()
6161+}
36623737-// We listen for messages from the background script
3838-chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
3939- if (message.type === "GET_DID") {
4040- checkForDID(getDomainName())
4141- .then((did) => sendResponse({ did }))
4242- .catch(() => sendResponse({ did: null }))
4343- return true // Indicate that the response will be sent asynchronously.
4444- }
4545-})
6363+// Get the user's privacy consent from the storage and perform actions accordingly
6464+storage.get("privacyConsentAccepted", ({ privacyConsentAccepted }) => {
6565+ performAction(privacyConsentAccepted);
6666+});