diff --git a/background.js b/background.js index fed57c4..4d5eeaf 100644 --- a/background.js +++ b/background.js @@ -1,30 +1,165 @@ +// Message listener for various actions browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'searchMastodon') { - // Load saved settings from localStorage - const mastodonDomain = localStorage.getItem('domain'); - const apiKey = localStorage.getItem('apiKey'); - const numPosts = localStorage.getItem('numPosts') || 5; - const searchTerm = message.searchTerm; + // Load saved settings from browser.storage.local + browser.storage.local.get(['client_id', 'client_secret', 'access_token', 'domain', 'numPosts']) + .then(({ client_id, client_secret, access_token, domain, numPosts = 5 }) => { + console.log(access_token); + console.log(domain); + if (!access_token || !domain) { + sendResponse({ success: false, error: 'Missing access token or domain.' }); + return; + } + const searchTerm = message.searchTerm; + fetch(`https://${domain}/api/v2/search?q=${encodeURIComponent(searchTerm)}&resolve=true&limit=${numPosts}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + } + }) + .then(response => response.json()) + .then(data => { + console.log(data.statuses); //TODO: For some reason this sendResponse does not work unless I log data.statuses. I have no idea why. Best guess is there's some sort of race condition going on? Though I'm not familiar enough with javascript to know if that's true + sendResponse({ success: true, results: data.statuses }); + }) + .catch(error => { + sendResponse({ success: false, error: error.message }); + }); - fetch(`https://${mastodonDomain}/api/v2/search?q=${encodeURIComponent(searchTerm)}&resolve=true&limit=${numPosts}`, { - headers: { - 'Authorization': `Bearer ${apiKey}` - } - }) - .then(response => response.json()) - .then(data => { - sendResponse({ success: true, results: data.statuses }); - }) - .catch(error => { + return true; + }) + .catch(error => { + sendResponse({ success: false, error: 'Failed to retrieve saved settings.' }); + }); + return true; + } else if (message.action === 'getSettings') { + browser.storage.local.get(['domain', 'numPosts']) + .then(({ domain, numPosts = 5 }) => { + sendResponse({ domain, numPosts }); + }) + .catch(error => { + sendResponse({ success: false, error: 'Failed to retrieve settings.' }); + }); + + return true; + } else if (message.action === 'authorize') { + const domain = message.domain; + let appRegistrate = null; + + // Register the app and start the OAuth flow + registerApp(domain).then(appRegistration => { + console.log('App registration successful:', appRegistration); + appRegistrate = appRegistration; + return launchOAuthFlow(appRegistration, domain); + }).then(redirectUrl => { + return validate(redirectUrl, domain, appRegistrate); // Pass appRegistration here + }).then(() => { + sendResponse({ success: true }); + }).catch(error => { + console.error('Error during OAuth process:', error); sendResponse({ success: false, error: error.message }); }); return true; } - else if (message.action === 'getSettings') { - const domain = localStorage.getItem('domain'); - const numPosts = parseInt(localStorage.getItem('numPosts')); - sendResponse({ domain, numPosts }); - } }); + +// Registers the app with a mastodon server +async function registerApp(domain) { + try { + const response = await fetch(`https://${domain}/api/v1/apps`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + client_name: 'DuckDuckSocial', + redirect_uris: browser.identity.getRedirectURL(), // Use the add-on's redirect URL + scopes: 'read write', + website: 'https://tomcasavant.com' + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error registering app:', errorText); + throw new Error('Failed to register app'); + } + + const appData = await response.json(); + await browser.storage.local.set({ + client_id: appData.client_id, + client_secret: appData.client_secret, + redirect_uri: appData.redirect_uri + }); + + return appData; + } catch (error) { + console.error('Error during app registration:', error); + throw error; + } +} + +// Launches OAuth flow +function launchOAuthFlow(appRegistration, domain) { + const authorizationUrl = `https://${domain}/oauth/authorize?client_id=${appRegistration.client_id}&redirect_uri=${encodeURIComponent(appRegistration.redirect_uri)}&response_type=code&scope=read`; + + return browser.identity.launchWebAuthFlow({ + interactive: true, + url: authorizationUrl + }); +} + +// Get access token from Authorization +async function exchangeCodeForToken(code, domain, appRegistration) { + try { + const response = await fetch(`https://${domain}/oauth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + client_id: appRegistration.client_id, + client_secret: appRegistration.client_secret, + redirect_uri: appRegistration.redirect_uri, + grant_type: 'authorization_code', + code: code + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error exchanging code for token:', errorText); + throw new Error('Failed to exchange authorization code for access token'); + } + + const tokenData = await response.json(); + await browser.storage.local.set({ access_token: tokenData.access_token }); + } catch (error) { + console.error('Error during token exchange:', error); + throw error; + } +} + +// Validates the redirect URL +async function validate(redirectUrl, domain, appRegistration) { + try { + console.log('Redirect URL:', redirectUrl); + + if (redirectUrl) { + const code = new URL(redirectUrl).searchParams.get('code'); + + if (code) { + await exchangeCodeForToken(code, domain, appRegistration); + console.log('Access token saved successfully.'); + } else { + throw new Error('Authorization code not found in redirect URL.'); + } + } else { + throw new Error('No redirect URL returned.'); + } + } catch (error) { + console.error('Validation error:', error); + throw error; + } +} diff --git a/content.js b/content.js index bd9ca42..3b35232 100644 --- a/content.js +++ b/content.js @@ -12,7 +12,9 @@ if (searchTerm) { browser.runtime.sendMessage({ action: 'searchMastodon', searchTerm: searchTerm }) .then(response => { + console.log(response); if (response.success) { + console.log(response); const results = response.results; var div = document.createElement('div'); diff --git a/manifest.json b/manifest.json index 37b60a5..0d94028 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "DuckDuckSocial", - "version": "0.0.2", + "version": "0.0.3", "description": "Adds a scrollable list of posts from the social web that match your DuckDuckGo search. Requires API key from mastodon compatible server", "icons": { "512": "icons/border-512.png" @@ -24,7 +24,8 @@ "permissions": [ "webRequest", "webRequestBlocking", - "storage" + "storage", + "identity" ], "browser_specific_settings": { "gecko": { diff --git a/options.js b/options.js index 3107d55..747ebaa 100644 --- a/options.js +++ b/options.js @@ -1,37 +1,68 @@ document.addEventListener('DOMContentLoaded', function() { const form = document.getElementById('settingsForm'); const domainInput = document.getElementById('domain'); - const apiKeyInput = document.getElementById('apiKey'); const numPostsInput = document.getElementById('numPosts'); + const messageElement = document.querySelector('.message'); // Load saved settings loadSettings(); - // Handle form submission form.addEventListener('submit', function(event) { event.preventDefault(); saveSettings(); }); + document.getElementById('connectMastodon').addEventListener('click', function(event) { + const domain = domainInput.value; + + if (!domain) { + updateMessage('Mastodon server domain is required!'); + return; + } + + browser.runtime.sendMessage({ + action: 'authorize', + domain: domain + }).then(response => { + if (response.success) { + updateMessage('Connected to Mastodon successfully!'); + } else { + updateMessage('Failed to connect to Mastodon: ' + response.error); + } + }).catch(error => { + console.error('Error during OAuth process:', error); + updateMessage('Failed to connect to Mastodon. Please try again.'); + }); + }); + function saveSettings() { const domain = domainInput.value; - const apiKey = apiKeyInput.value; const numPosts = numPostsInput.value; - localStorage.setItem('domain', domain); - localStorage.setItem('apiKey', apiKey); - localStorage.setItem('numPosts', numPosts); - - alert('Settings saved successfully!'); + browser.storage.local.set({ + domain: domain, + numPosts: numPosts + }).then(() => { + updateMessage('Settings saved successfully!'); + }).catch(error => { + console.error('Failed to save settings:', error); + updateMessage('Failed to save settings.'); + }); } function loadSettings() { - const savedDomain = localStorage.getItem('domain'); - const savedApiKey = localStorage.getItem('apiKey'); - const savedNumPosts = localStorage.getItem('numPosts'); - - if (savedDomain) domainInput.value = savedDomain; - if (savedApiKey) apiKeyInput.value = savedApiKey; - if (savedNumPosts) numPostsInput.value = savedNumPosts; + browser.storage.local.get(['domain', 'numPosts']) + .then(({ domain, numPosts = 5 }) => { + if (domain) domainInput.value = domain; + if (numPosts) numPostsInput.value = numPosts; + }) + .catch(error => { + console.error('Failed to load settings:', error); + }); } + + function updateMessage(message) { + messageElement.textContent = message; + messageElement.style.color = 'red'; + } }); diff --git a/settings.html b/settings.html index 3f64384..085483f 100644 --- a/settings.html +++ b/settings.html @@ -1,7 +1,7 @@ - Fedi Search + DuckDuckSocial -

Fedi Search

+

DuckDuckSocial

- - - - + +
+