Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions otter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,19 @@
"value": ""
},
{
"name": "otterApiSecret",
"name": "otterOAuthClientId",
"type": "password",
"title": "Otter API secret",
"description": "This is the Supabase `service_role` API key",
"title": "Otter OAuth Client ID",
"description": "Otter OAuth Client ID",
"required": true,
"default": "",
"value": ""
},
{
"name": "otterOAuthClientSecret",
"type": "password",
"title": "Otter OAuth Client Secret",
"description": "Otter OAuth Client Secret",
"required": true,
"default": "",
"value": ""
Expand Down
59 changes: 45 additions & 14 deletions otter/src/utils/fetchItems.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { getPreferenceValues } from '@raycast/api'
import { ApiResponse } from '../bookmark.model'
import urlJoin from 'proper-url-join'
import { useFetch } from '@raycast/utils'
import { useCachedPromise } from '@raycast/utils'
import { authorize, client } from './oauth'
import { useRef } from 'react'
import fetch from 'node-fetch'

export const useOtterFetch = <T = unknown, U = undefined>(url: string) => {
const pref = getPreferenceValues()
const fetchResponse = useFetch<ApiResponse>(url, {
headers: {
Authorization: `Bearer ${pref.otterApiSecret}`,
const pref = getPreferenceValues()
const otterBasePath = pref.otterBasePath
export const useOtterFetch = (url: string) => {
const abortable = useRef<AbortController>(new AbortController())
const fetchResponse = useCachedPromise(
async (url: string): Promise<ApiResponse> => {
console.log(`🚀 ~ url:`, url)
await authorize()
const tokens = await client.getTokens()
console.log(`🚀 ~ useOtterFetch tokens:`, tokens)
const response = await fetch(url, {
signal: abortable.current?.signal,
headers: {
Authorization: `Bearer ${tokens?.accessToken}`,
},
})
const result = await response.json()
console.log(`🚀 ~ result:`, result)
return result
},
keepPreviousData: true,
})
[url],
{
abortable,
keepPreviousData: true,
}
)
console.log(`🚀 ~ useOtterFetch ~ fetchResponse:`, fetchResponse)

return fetchResponse
}

export const useFetchSearchItems = (searchTerm: string = '') => {
const pref = getPreferenceValues()
return useOtterFetch<ApiResponse>(
urlJoin(pref.otterBasePath, 'api', 'search', {
console.log(`🚀 ~ useFetchSearchItems ~ searchTerm:`, searchTerm)
return useOtterFetch(
urlJoin(otterBasePath, 'api/search', {
query: {
q: searchTerm,
status: 'active',
Expand All @@ -27,10 +49,19 @@ export const useFetchSearchItems = (searchTerm: string = '') => {
)
}
export const useFetchRecentItems = () => {
const pref = getPreferenceValues()
return useOtterFetch<ApiResponse>(
urlJoin(pref.otterBasePath, 'api', 'bookmarks', {
return useOtterFetch(
urlJoin(otterBasePath, 'api/bookmarks', {
query: { limit: '60', status: 'active' },
})
)
}
export const useFetchMetaItems = (searchTerm: string = '') => {
return useOtterFetch(
urlJoin(otterBasePath, 'api/meta', {
query: {
q: searchTerm,
status: 'active',
},
})
)
}
101 changes: 101 additions & 0 deletions otter/src/utils/oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { OAuth, getPreferenceValues } from '@raycast/api'
import fetch from 'node-fetch'
import urlJoin from 'proper-url-join'

const prefs = getPreferenceValues()

const clientId = prefs.otterOAuthClientId
const clientSecret = prefs.otterOAuthClientSecret

export const client = new OAuth.PKCEClient({
redirectMethod: OAuth.RedirectMethod.Web,
providerName: 'Otter',
providerIcon: 'command-icon.png',
providerId: 'otter',
description: 'Connect your Otter account…',
})

export async function authorize() {
const tokenSet = await client.getTokens()
console.log(`🚀 ~ authorize ~ getTokens:`, tokenSet)

if (tokenSet?.accessToken) {
if (tokenSet?.refreshToken && tokenSet?.isExpired()) {
const tokens = await refreshTokens(tokenSet.refreshToken)
await client.setTokens(tokens)
return tokens.access_token
}
return tokenSet.accessToken
}

const authRequest = await client.authorizationRequest({
endpoint: urlJoin(prefs.otterBasePath, 'auth', 'connect'),
clientId,
scope: 'Auth:Read Database:Read',
})

const { authorizationCode } = await client.authorize(authRequest)
console.log(`🚀 ~ authorize ~ authorizationCode:`, authorizationCode)

const tokens = await fetchTokens(authRequest, authorizationCode)
console.log(`🚀 ~ authorize ~ tokens:`, tokens)
await client.setTokens(tokens)
return tokens.access_token
}

async function fetchTokens(
authRequest: OAuth.AuthorizationRequest,
authCode: string
) {
console.log(`🚀 ~ fetchTokens authCode:`, authCode)
console.log(`🚀 ~ fetchTokens authRequest:`, authRequest)
const response = await fetch('https://api.supabase.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
code_verifier: authRequest.codeVerifier,
redirect_uri: authRequest.redirectURI,
}),
})

if (!response.ok) {
console.error('Fetch tokens error: ', await response.text())
throw new Error(response.statusText)
}

return (await response.json()) as OAuth.TokenResponse
}

async function refreshTokens(
refreshToken: string
): Promise<OAuth.TokenResponse> {
console.log(`🚀 ~ refreshToken:`, refreshToken)
const response = await fetch('https://api.supabase.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
})

if (!response.ok) {
console.error('refresh tokens error:', await response.text())
throw new Error(response.statusText)
}

const tokenResponse = (await response.json()) as OAuth.TokenResponse
tokenResponse.refresh_token = tokenResponse.refresh_token ?? refreshToken

return tokenResponse
}