From 3e60ed295f3eb13e4329bdd8272a8e7abc98ae43 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Thu, 14 Apr 2022 20:58:47 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=8D=A3=20removed=20scrap=20and=20simp?= =?UTF-8?q?lified=20the=20urlResolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 155 ++++++++++++------------------------ src/common/yt/auth.ts | 32 ++++++++ src/common/yt/urlResolve.ts | 139 +++++++++++++------------------- src/manifest.json | 2 +- src/scripts/background.ts | 6 +- src/tools/YTtoLBRY.tsx | 6 +- 6 files changed, 147 insertions(+), 193 deletions(-) create mode 100644 src/common/yt/auth.ts diff --git a/src/common/settings.ts b/src/common/settings.ts index c6fa00c..5eb0210 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -13,8 +13,8 @@ export function getExtensionSettingsAsync(): Promise { } -export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app' -export interface TargetPlatform { + +const targetPlatform = (o: { domainPrefix: string displayName: string theme: string @@ -27,10 +27,14 @@ export interface TargetPlatform { button?: JSX.CSSProperties } } +}) => o +export type TargetPlatform = ReturnType +export type TargetPlatformName = Extract +export const getTargetPlatfromSettingsEntiries = () => { + return Object.entries(targetPlatformSettings) as any as [Extract, TargetPlatform][] } - -export const targetPlatformSettings: Record = { - 'madiator.com': { +export const targetPlatformSettings = { + 'madiator.com': targetPlatform({ domainPrefix: 'https://madiator.com/', displayName: 'Madiator.com', theme: '#075656', @@ -42,8 +46,8 @@ export const targetPlatformSettings: Record icon: { transform: 'scale(1.2)' } } } - }, - odysee: { + }), + odysee: targetPlatform({ domainPrefix: 'https://odysee.com/', displayName: 'Odysee', theme: '#1e013b', @@ -51,8 +55,8 @@ export const targetPlatformSettings: Record text: 'Watch on Odysee', icon: chrome.runtime.getURL('icons/lbry/odysee-logo.svg') } - }, - app: { + }), + app: targetPlatform({ domainPrefix: 'lbry://', displayName: 'LBRY App', theme: '#075656', @@ -60,118 +64,59 @@ export const targetPlatformSettings: Record text: 'Watch on LBRY', icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg') } - }, -} - -export const getTargetPlatfromSettingsEntiries = () => { - return Object.entries(targetPlatformSettings) as any as [Extract, TargetPlatform][] + }), } -export type SourcePlatformName = 'youtube.com' | 'yewtu.be' -export interface SourcePlatform { + + +const sourcePlatform = (o: { hostnames: string[] htmlQueries: { mountButtonBefore: string, videoPlayer: string } -} - -export const sourcePlatfromSettings: Record = { - "yewtu.be": { - hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'], - htmlQueries: { - mountButtonBefore: '#watch-on-youtube', - videoPlayer: '#player-container video' - } - }, - "youtube.com": { - hostnames: ['www.youtube.com'], - htmlQueries: { - mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button', - videoPlayer: '#ytd-player video' - } - } -} - +}) => o +export type SourcePlatform = ReturnType +export type SourcePlatformName = Extract export function getSourcePlatfromSettingsFromHostname(hostname: string) { const values = Object.values(sourcePlatfromSettings) for (const settings of values) if (settings.hostnames.includes(hostname)) return settings return null } - - -export type YTUrlResolverName = 'lbryInc' | 'madiatorScrap' - -export const Keys = Symbol('keys') -export const Values = Symbol('values') -export const SingleValueAtATime = Symbol() -export type YtUrlResolveResponsePath = (string | number | typeof Keys | typeof Values)[] -export interface YtUrlResolveFunction { - pathname: string - defaultParams: Record - valueParamName: string - paramArraySeperator: string | typeof SingleValueAtATime - responsePath: YtUrlResolveResponsePath +export const sourcePlatfromSettings = { + "yewtu.be": sourcePlatform({ + hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'], + htmlQueries: { + mountButtonBefore: '#watch-on-youtube', + videoPlayer: '#player-container video' + } + }), + "youtube.com": sourcePlatform({ + hostnames: ['www.youtube.com'], + htmlQueries: { + mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button', + videoPlayer: '#ytd-player video' + } + }) } -export interface YTUrlResolver { + +const ytUrlResolver = (o: { name: string - hostname: string - functions: { - getChannelId: YtUrlResolveFunction - getVideoId: YtUrlResolveFunction - } -} - -export const ytUrlResolversSettings: Record = { - lbryInc: { + href: string +}) => o +export type YTUrlResolver = ReturnType +export type YTUrlResolverName = Extract +export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResolversSettings) as any as [Extract, YTUrlResolver][] +export const ytUrlResolversSettings = { + lbryInc: ytUrlResolver({ name: "Odysee", - hostname: "api.odysee.com", - functions: { - getChannelId: { - pathname: "/yt/resolve", - defaultParams: {}, - valueParamName: "channel_ids", - paramArraySeperator: ',', - responsePath: ["data", "channels", Values] - }, - getVideoId: { - pathname: "/yt/resolve", - defaultParams: {}, - valueParamName: "video_ids", - paramArraySeperator: ",", - responsePath: ["data", "videos", Values] - } - } - }, - madiatorScrap: { - name: "Madiator.com", - hostname: "scrap.madiator.com", - functions: { - getChannelId: { - pathname: "/api/get-lbry-channel", - defaultParams: { - v: 2 - }, - valueParamName: "url", - paramArraySeperator: SingleValueAtATime, - responsePath: ["lbrych"] - }, - getVideoId: { - pathname: "/api/get-lbry-video", - defaultParams: { - v: 2 - }, - valueParamName: "url", - paramArraySeperator: SingleValueAtATime, - responsePath: ["lbryurl"] - } - } - } -} - -export const getYtUrlResolversSettingsEntiries = () => { - return Object.entries(ytUrlResolversSettings) as any as [Extract, YTUrlResolver][] + href: "https://api.odysee.com/yt/resolve" + }), + madiatorFinder: ytUrlResolver({ + name: "Madiator Finder", + href: "https://finder.madiator.com/api/v1/resolve" + }) } \ No newline at end of file diff --git a/src/common/yt/auth.ts b/src/common/yt/auth.ts new file mode 100644 index 0000000..32ea04f --- /dev/null +++ b/src/common/yt/auth.ts @@ -0,0 +1,32 @@ +import crypto from 'crypto' + +function generateKeys() { + // The `generateKeyPairSync` method accepts two arguments: + // 1. The type ok keys we want, which in this case is "rsa" + // 2. An object with the properties of the key + const keys = crypto.generateKeyPairSync("rsa", { + // The standard secure default length for RSA keys is 2048 bits + modulusLength: 2048, + }) + + return keys +} + +function encodeKeys(keys: { publicKey: Buffer, privateKey: Buffer }) { + return JSON.stringify({ publicKey: keys.publicKey.toString('base64'), privateKey: keys.privateKey.toString('base64') }) +} + +function decodeKeys(encodedKeys: string) { + const keysBase64 = JSON.parse(encodedKeys) + return { + publicKey: Buffer.from(keysBase64.publicKey), + privateKey: Buffer.from(keysBase64.privateKey) + } +} + +function sign(data: string, privateKey: Buffer) { + return crypto.sign("sha256", Buffer.from(data), { + key: privateKey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + }) +} \ No newline at end of file diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index d02e3d4..559b52c 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -1,105 +1,80 @@ -import { chunk, groupBy } from "lodash" -import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from "../settings" +import { chunk } from "lodash" +import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../settings" import { LbryPathnameCache } from "./urlCache" // const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS const QUERY_CHUNK_SIZE = 100 -export interface YtIdResolverDescriptor { - id: string - type: 'channel' | 'video' + +export type YtUrlResolveItem = { type: 'video' | 'channel', id: string } +type Results = Record +type Paramaters = YtUrlResolveItem[] + +interface ApiResponse { + channels?: Record + videos?: Record } -/** -* @param descriptorsWithIndex YT resource IDs to check -* @returns a promise with the list of channels that were found on lbry -*/ -export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> { - let descriptorsPayload: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) - descriptors = null as any - const results: (string | null)[] = [] +export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise { + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] + async function requestChunk(params: Paramaters) { + const results: Results = {} - descriptorsPayload = (await Promise.all(descriptorsPayload.map(async (descriptor, index) => { - if (!descriptor?.id) return - const cache = await LbryPathnameCache.get(descriptor.id) + // Check for cache first, add them to the results if there are any cache + // And remove them from the params, so we dont request for them + params = (await Promise.all(params.map(async (item) => { + const cachedLbryUrl = await LbryPathnameCache.get(item.id) - // Cache can be null, if there is no lbry url yet - if (cache !== undefined) { - // Null values shouldn't be in the results - if (cache) results[index] = cache - return - } - - return descriptor - }))).filter((descriptor) => descriptor) as any - - const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE) - let progressCount = 0 - await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => { - const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any - - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() - const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] - - const url = new URL(`https://${urlResolverSetting.hostname}`) - - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) { - for (const path of responsePath) { - switch (typeof path) { - case 'string': case 'number': response = response[path]; continue - } - switch (path) { - case Keys: response = Object.keys(response); continue - case Values: response = Object.values(response); continue - } + // Cache can be null, if there is no lbry url yet + if (cachedLbryUrl !== undefined) { + // Null values shouldn't be in the results + if (cachedLbryUrl !== null) results[item.id] = { id: cachedLbryUrl, type: item.type } + return null } - return response as T - } - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) { - url.pathname = urlResolverFunction.pathname - Object.entries(urlResolverFunction.defaultParams).forEach(([name, value]) => url.searchParams.set(name, value.toString())) + // No cache found + return item + }))).filter((o) => o) as Paramaters - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) { - await Promise.all(descriptorsGroup.map(async (descriptor) => { - url.searchParams.set(urlResolverFunction.valueParamName, descriptor.id) + if (params.length === 0) return results - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (apiResponse.ok) { - const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - } - else if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) + const url = new URL(`${urlResolverSetting.href}`) + url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(',')) + url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(',')) - progressCount++ - if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) - })) + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (apiResponse.ok) { + const response: ApiResponse = await apiResponse.json() + for (const [id, lbryUrl] of Object.entries(response.channels ?? {})) { + // we cache it no matter if its null or not + await LbryPathnameCache.put(lbryUrl, id) + + if (lbryUrl) results[id] = { id: lbryUrl, type: 'channel' } } - else { - url.searchParams.set(urlResolverFunction.valueParamName, descriptorsGroup - .map((descriptor) => descriptor.id) - .join(urlResolverFunction.paramArraySeperator)) + for (const [id, lbryUrl] of Object.entries(response.videos ?? {})) { + // we cache it no matter if its null or not + await LbryPathnameCache.put(lbryUrl, id) - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (apiResponse.ok) { - const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - await Promise.all(values.map(async (value, index) => { - const descriptor = descriptorsGroup[index] - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - })) - } - - progressCount += descriptorsGroup.length - if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) + if (lbryUrl) results[id] = { id: lbryUrl, type: 'video' } } } - if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) - if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) - })) + return results + } + + const results: Results = {} + const chunks = chunk(params, QUERY_CHUNK_SIZE) + + let i = 0 + if (progressCallback) progressCallback(0) + for (const chunk of chunks) { + if (progressCallback) progressCallback(++i / (chunks.length + 1)) + Object.assign(results, await requestChunk(chunk)) + } + console.log(results) + if (progressCallback) progressCallback(1) return results } diff --git a/src/manifest.json b/src/manifest.json index 891ccf8..343d3f3 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -11,7 +11,7 @@ "https://lbry.tv/", "https://odysee.com/", "https://madiator.com/", - "https://scrap.madiator.com/", + "https://finder.madiator.com/", "tabs", "storage" ], diff --git a/src/scripts/background.ts b/src/scripts/background.ts index c30af4f..5fe7a4a 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -1,7 +1,7 @@ -import { resolveById, YtIdResolverDescriptor } from '../common/yt/urlResolve' +import { resolveById, YtUrlResolveItem } from '../common/yt/urlResolve' -async function resolveYT(descriptor: YtIdResolverDescriptor) { - const lbryProtocolUrl: string | null = (await resolveById([descriptor]).then(a => a[0])) ?? null +async function resolveYT(item: YtUrlResolveItem) { + const lbryProtocolUrl: string | null = (await resolveById([item]).then((items) => items[item.id]))?.id ?? null if (!lbryProtocolUrl) return null return lbryProtocolUrl.replaceAll('#', ':') /* const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true }) diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index 5faaa29..8709429 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -18,12 +18,14 @@ async function lbryChannelsFromFile(file: File) { ext === 'xml' || ext == 'opml' ? getSubsFromOpml : ext === 'csv' ? getSubsFromCsv : getSubsFromJson)(await getFileContent(file))) - const lbryPathnames = await resolveById( + + const items = await resolveById( Array.from(ids).map(id => ({ id, type: 'channel' } as const)), (progress) => render(, document.getElementById('root')!)) + const { targetPlatform: platform } = await getExtensionSettingsAsync() const urlPrefix = targetPlatformSettings[platform].domainPrefix - return lbryPathnames.map(channel => urlPrefix + channel) + return Object.values(items).map((item) => urlPrefix + item.id) } function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise | void, progress: number }) { From 365173d316d897b0d32eb25a57dfd72707ad6294 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 15 Apr 2022 09:37:39 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=8D=99=20auth=20added?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/crypto.ts | 56 +++++++++++++++++++++++++++++++++++++ src/common/settings.ts | 20 +++++++++---- src/common/useSettings.ts | 21 ++++++++++---- src/common/yt/urlCache.ts | 3 +- src/common/yt/urlResolve.ts | 10 +++++-- src/manifest.json | 3 +- src/popup/popup.tsx | 7 ++--- src/scripts/background.ts | 3 +- src/scripts/ytContent.tsx | 14 ++++------ 9 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 src/common/crypto.ts diff --git a/src/common/crypto.ts b/src/common/crypto.ts new file mode 100644 index 0000000..2b80a45 --- /dev/null +++ b/src/common/crypto.ts @@ -0,0 +1,56 @@ +export async function generateKeys() { + const keys = await window.crypto.subtle.generateKey( + { + name: "RSASSA-PKCS1-v1_5", + // Consider using a 4096-bit key for systems that require long-term security + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["sign", "verify"] + ) + + return { + publicKey: await exportPublicKey(keys.publicKey), + privateKey: await exportPrivateKey(keys.privateKey) + } +} + +async function exportPrivateKey(key: CryptoKey) { + const exported = await window.crypto.subtle.exportKey( + "pkcs8", + key + ) + return Buffer.from(exported).toString('base64') +} + +async function exportPublicKey(key: CryptoKey) { + const exported = await window.crypto.subtle.exportKey( + "spki", + key + ) + return Buffer.from(exported).toString('base64') +} + +function importPrivateKey(base64: string) { + + return window.crypto.subtle.importKey( + "pkcs8", + Buffer.from(base64, 'base64'), + { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + }, + true, + ["sign"] + ) +} + +export async function sign(data: string, privateKey: string) { + return Buffer.from(await window.crypto.subtle.sign( + { name: "RSASSA-PKCS1-v1_5" }, + await importPrivateKey(privateKey), + Buffer.from(data) + )) +} \ No newline at end of file diff --git a/src/common/settings.ts b/src/common/settings.ts index 5eb0210..56cb35e 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -4,16 +4,22 @@ export interface ExtensionSettings { redirect: boolean targetPlatform: TargetPlatformName urlResolver: YTUrlResolverName + publicKey?: string, + privateKey?: string } -export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' } + + +export const DEFAULT_SETTINGS: ExtensionSettings = { + redirect: true, + targetPlatform: 'odysee', + urlResolver: 'odyseeApi' +} export function getExtensionSettingsAsync(): Promise { return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))) } - - const targetPlatform = (o: { domainPrefix: string displayName: string @@ -69,8 +75,6 @@ export const targetPlatformSettings = { - - const sourcePlatform = (o: { hostnames: string[] htmlQueries: { @@ -111,12 +115,16 @@ export type YTUrlResolver = ReturnType export type YTUrlResolverName = Extract export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResolversSettings) as any as [Extract, YTUrlResolver][] export const ytUrlResolversSettings = { - lbryInc: ytUrlResolver({ + odyseeApi: ytUrlResolver({ name: "Odysee", href: "https://api.odysee.com/yt/resolve" }), madiatorFinder: ytUrlResolver({ name: "Madiator Finder", href: "https://finder.madiator.com/api/v1/resolve" + }), + local: ytUrlResolver({ + name: "Local", + href: "http://localhost:3000/api/v1/resolve" }) } \ No newline at end of file diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index 26e519d..8578a08 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -1,13 +1,14 @@ import { useEffect, useReducer } from 'preact/hooks' -import { DEFAULT_SETTINGS } from './settings' +import { generateKeys } from './crypto' +import { DEFAULT_SETTINGS, ExtensionSettings } from './settings' /** * A hook to read the settings from local storage * * @param initial the default value. Must have all relevant keys present and should not change */ -export function useSettings(initial: T) { - const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial) +export function useSettings(initial: ExtensionSettings) { + const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial) // register change listeners, gets current values, and cleans up the listeners on unload useEffect(() => { const changeListener = (changes: Record, areaName: string) => { @@ -19,12 +20,22 @@ export function useSettings(initial: T) { dispatch(Object.fromEntries(changeSet)) } chrome.storage.onChanged.addListener(changeListener) - chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) + chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) + + generateKeys().then((keys) => { + setSetting('publicKey', keys.publicKey) + setSetting('privateKey', keys.privateKey) + }) + return () => chrome.storage.onChanged.removeListener(changeListener) }, []) return state } +/** Utilty to set a setting in the browser */ +export const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }) + + /** A hook to read watch on lbry settings from local storage */ -export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) +export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) \ No newline at end of file diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts index 8adcc63..9d2535d 100644 --- a/src/common/yt/urlCache.ts +++ b/src/common/yt/urlCache.ts @@ -37,8 +37,7 @@ async function clearExpired() { }) } -async function clearAll() -{ +async function clearAll() { return await new Promise((resolve, reject) => { const store = db?.transaction("store", "readwrite").objectStore("store") if (!store) return resolve() diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index 559b52c..b7f7b68 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -1,11 +1,10 @@ import { chunk } from "lodash" +import { sign } from "../crypto" import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../settings" import { LbryPathnameCache } from "./urlCache" -// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS const QUERY_CHUNK_SIZE = 100 - export type YtUrlResolveItem = { type: 'video' | 'channel', id: string } type Results = Record type Paramaters = YtUrlResolveItem[] @@ -16,7 +15,7 @@ interface ApiResponse { } export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise { - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const { urlResolver: urlResolverSettingName, privateKey, publicKey } = await getExtensionSettingsAsync() const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] async function requestChunk(params: Paramaters) { @@ -43,6 +42,11 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre const url = new URL(`${urlResolverSetting.href}`) url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(',')) url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(',')) + if (publicKey && privateKey) + url.searchParams.set('keys', JSON.stringify({ + signature: await sign(url.searchParams.toString(), privateKey), + publicKey + })) const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) if (apiResponse.ok) { diff --git a/src/manifest.json b/src/manifest.json index 343d3f3..34b2ec6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -12,6 +12,7 @@ "https://odysee.com/", "https://madiator.com/", "https://finder.madiator.com/", + "http://localhost:3000", "tabs", "storage" ], @@ -53,4 +54,4 @@ "128": "icons/wol/icon128.png" }, "manifest_version": 2 -} +} \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index b9e506d..e550e46 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,13 +1,11 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio' -import { ExtensionSettings, getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings' -import { useLbrySettings } from '../common/useSettings' +import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings' +import { setSetting, useLbrySettings } from '../common/useSettings' import { LbryPathnameCache } from '../common/yt/urlCache' import './popup.sass' -/** Utilty to set a setting in the browser */ -const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }) /** Gets all the options for redirect destinations as selection options */ const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries() @@ -21,6 +19,7 @@ function WatchOnLbryPopup() { let [clearingCache, updateClearingCache] = useState(() => false) return
+ { }
} chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => { - lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)).catch((err) => - { + lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)).catch((err) => { sendResponse('error') console.error(err) }) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 5a0de20..1d47618 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -117,32 +117,30 @@ async function requestLbryPathname(videoId: string) { async function redirectTo({ lbryPathname, platfrom, time }: Target) { const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`) - + if (time) url.searchParams.set('t', time.toFixed(0)) - + findVideoElement().then((videoElement) => { videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) videoElement.pause() }) - + if (platfrom === targetPlatformSettings.app) { if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) // On redirect with app, people might choose to cancel browser's dialog // So we dont destroy the current window automatically for them // And also we are keeping the same window for less distiraction - if (settings.redirect) - { + if (settings.redirect) { location.replace(url.toString()) } - else - { + else { open(url.toString(), '_blank') if (window.history.length === 1) window.close() else window.history.back() } } - else + else location.replace(url.toString()) } From 2a3771e45ac32a3fe5ac3a984769cf6c2125050b Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 15 Apr 2022 09:50:25 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=8D=99=20signature=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 10 +++++++--- src/common/useSettings.ts | 15 +++++++++------ src/common/yt/urlResolve.ts | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/common/settings.ts b/src/common/settings.ts index 56cb35e..f1ba6e5 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -110,6 +110,7 @@ export const sourcePlatfromSettings = { const ytUrlResolver = (o: { name: string href: string + signRequest: boolean }) => o export type YTUrlResolver = ReturnType export type YTUrlResolverName = Extract @@ -117,14 +118,17 @@ export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResol export const ytUrlResolversSettings = { odyseeApi: ytUrlResolver({ name: "Odysee", - href: "https://api.odysee.com/yt/resolve" + href: "https://api.odysee.com/yt/resolve", + signRequest: false }), madiatorFinder: ytUrlResolver({ name: "Madiator Finder", - href: "https://finder.madiator.com/api/v1/resolve" + href: "https://finder.madiator.com/api/v1/resolve", + signRequest: true }), local: ytUrlResolver({ name: "Local", - href: "http://localhost:3000/api/v1/resolve" + href: "http://localhost:3000/api/v1/resolve", + signRequest: true }) } \ No newline at end of file diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index 8578a08..2fc4ee3 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -22,11 +22,6 @@ export function useSettings(initial: ExtensionSettings) { chrome.storage.onChanged.addListener(changeListener) chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) - generateKeys().then((keys) => { - setSetting('publicKey', keys.publicKey) - setSetting('privateKey', keys.privateKey) - }) - return () => chrome.storage.onChanged.removeListener(changeListener) }, []) @@ -38,4 +33,12 @@ export const setSetting = (setting: K, value: /** A hook to read watch on lbry settings from local storage */ -export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) \ No newline at end of file +export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) + +{ + const settings = useLbrySettings() + if (!settings.publicKey || !settings.privateKey) generateKeys().then((keys) => { + setSetting('publicKey', keys.publicKey) + setSetting('privateKey', keys.privateKey) + }) +} \ No newline at end of file diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index b7f7b68..8c87784 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -42,7 +42,7 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre const url = new URL(`${urlResolverSetting.href}`) url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(',')) url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(',')) - if (publicKey && privateKey) + if (urlResolverSetting.signRequest && publicKey && privateKey) url.searchParams.set('keys', JSON.stringify({ signature: await sign(url.searchParams.toString(), privateKey), publicKey From 651ca5b4dce17a10fc807f849afaf119989d479e Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 15 Apr 2022 10:30:49 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=94=A5=20debug=20code=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 5 ----- src/common/useSettings.ts | 16 +++++++--------- src/manifest.json | 1 - 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/common/settings.ts b/src/common/settings.ts index f1ba6e5..e19c13f 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -125,10 +125,5 @@ export const ytUrlResolversSettings = { name: "Madiator Finder", href: "https://finder.madiator.com/api/v1/resolve", signRequest: true - }), - local: ytUrlResolver({ - name: "Local", - href: "http://localhost:3000/api/v1/resolve", - signRequest: true }) } \ No newline at end of file diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index 2fc4ee3..62339b2 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -19,6 +19,12 @@ export function useSettings(initial: ExtensionSettings) { if (changeSet.length === 0) return // no changes; no use dispatching dispatch(Object.fromEntries(changeSet)) } + + if (!state.privateKey || !state.publicKey) generateKeys().then((keys) => { + setSetting('publicKey', keys.publicKey) + setSetting('privateKey', keys.privateKey) + }) + chrome.storage.onChanged.addListener(changeListener) chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) @@ -33,12 +39,4 @@ export const setSetting = (setting: K, value: /** A hook to read watch on lbry settings from local storage */ -export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) - -{ - const settings = useLbrySettings() - if (!settings.publicKey || !settings.privateKey) generateKeys().then((keys) => { - setSetting('publicKey', keys.publicKey) - setSetting('privateKey', keys.privateKey) - }) -} \ No newline at end of file +export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 34b2ec6..d150e80 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -12,7 +12,6 @@ "https://odysee.com/", "https://madiator.com/", "https://finder.madiator.com/", - "http://localhost:3000", "tabs", "storage" ], From 450ca8cef68e2566bfa16bad2d393d4ff29ddde3 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 15 Apr 2022 13:37:37 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=94=A5=20bug=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/crypto.ts | 2 +- src/common/settings.ts | 10 +++++----- src/common/useSettings.ts | 22 ++++++++-------------- src/common/yt/urlResolve.ts | 15 ++++++--------- src/manifest.json | 2 +- src/popup/popup.tsx | 1 - src/scripts/storageSetup.ts | 10 +++++++++- 7 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/common/crypto.ts b/src/common/crypto.ts index 2b80a45..d210897 100644 --- a/src/common/crypto.ts +++ b/src/common/crypto.ts @@ -52,5 +52,5 @@ export async function sign(data: string, privateKey: string) { { name: "RSASSA-PKCS1-v1_5" }, await importPrivateKey(privateKey), Buffer.from(data) - )) + )).toString('base64') } \ No newline at end of file diff --git a/src/common/settings.ts b/src/common/settings.ts index e19c13f..2e5e991 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -4,16 +4,16 @@ export interface ExtensionSettings { redirect: boolean targetPlatform: TargetPlatformName urlResolver: YTUrlResolverName - publicKey?: string, - privateKey?: string + publicKey: string | null, + privateKey: string | null } - - export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', - urlResolver: 'odyseeApi' + urlResolver: 'odyseeApi', + privateKey: null, + publicKey: null } export function getExtensionSettingsAsync(): Promise { diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index 62339b2..8199f35 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -5,28 +5,22 @@ import { DEFAULT_SETTINGS, ExtensionSettings } from './settings' /** * A hook to read the settings from local storage * - * @param initial the default value. Must have all relevant keys present and should not change + * @param defaultSettings the default value. Must have all relevant keys present and should not change */ -export function useSettings(initial: ExtensionSettings) { - const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial) +export function useSettings(defaultSettings: ExtensionSettings) { + const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), defaultSettings) + const settingsKeys = Object.keys(defaultSettings) // register change listeners, gets current values, and cleans up the listeners on unload useEffect(() => { const changeListener = (changes: Record, areaName: string) => { if (areaName !== 'local') return - const changeSet = Object.keys(changes) - .filter(k => Object.keys(initial).includes(k)) - .map(k => [k, changes[k].newValue]) - if (changeSet.length === 0) return // no changes; no use dispatching - dispatch(Object.fromEntries(changeSet)) + const changeEntries = Object.keys(changes).filter((key) => settingsKeys.includes(key)).map((key) => [key, changes[key].newValue]) + if (changeEntries.length === 0) return // no changes; no use dispatching + dispatch(Object.fromEntries(changeEntries)) } - if (!state.privateKey || !state.publicKey) generateKeys().then((keys) => { - setSetting('publicKey', keys.publicKey) - setSetting('privateKey', keys.privateKey) - }) - chrome.storage.onChanged.addListener(changeListener) - chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) + chrome.storage.local.get(settingsKeys, async (settings) => dispatch(settings)) return () => chrome.storage.onChanged.removeListener(changeListener) }, []) diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index 8c87784..c83835f 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -36,6 +36,7 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre // No cache found return item }))).filter((o) => o) as Paramaters + console.log(params) if (params.length === 0) return results @@ -51,17 +52,13 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) if (apiResponse.ok) { const response: ApiResponse = await apiResponse.json() - for (const [id, lbryUrl] of Object.entries(response.channels ?? {})) { + for (const item of params) + { + const lbryUrl = ((item.type === 'channel' ? response.channels : response.videos) ?? {})[item.id] ?? null // we cache it no matter if its null or not - await LbryPathnameCache.put(lbryUrl, id) + await LbryPathnameCache.put(lbryUrl, item.id) - if (lbryUrl) results[id] = { id: lbryUrl, type: 'channel' } - } - for (const [id, lbryUrl] of Object.entries(response.videos ?? {})) { - // we cache it no matter if its null or not - await LbryPathnameCache.put(lbryUrl, id) - - if (lbryUrl) results[id] = { id: lbryUrl, type: 'video' } + if (lbryUrl) results[item.id] = { id: lbryUrl, type: item.type } } } diff --git a/src/manifest.json b/src/manifest.json index d150e80..e4ca076 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -34,7 +34,7 @@ "scripts/storageSetup.js", "scripts/background.js" ], - "persistent": false + "persistent": true }, "browser_action": { "default_title": "Watch on LBRY", diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index e550e46..9c63644 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -19,7 +19,6 @@ function WatchOnLbryPopup() { let [clearingCache, updateClearingCache] = useState(() => false) return
- { }
) - .filter(([k]) => settings[k] === null || settings[k] === undefined) + .filter(([k]) => settings[k] === undefined || settings[k] === null) // fix our local var and set it in storage for later if (invalidEntries.length > 0) { @@ -15,6 +17,12 @@ async function initSettings() { chrome.storage.local.set(changeSet) } + if (!settings.privateKey || !settings.publicKey) + await generateKeys().then((keys) => { + setSetting('publicKey', keys.publicKey) + setSetting('privateKey', keys.privateKey) + }) + chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' }) } From a0f66bc062d7c89dc7da1eebdd2aaa99df61d9bf Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 22 Apr 2022 19:08:03 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=8D=A3=20implementing=20finder=20logi?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/crypto.ts | 32 ++++++++++++++++++++++++++++++++ src/popup/popup.sass | 11 +++++++++-- src/popup/popup.tsx | 37 ++++++++++++++++++++++++++++++++++++- src/scripts/storageSetup.ts | 6 ------ 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/common/crypto.ts b/src/common/crypto.ts index d210897..c6d27d6 100644 --- a/src/common/crypto.ts +++ b/src/common/crypto.ts @@ -1,3 +1,6 @@ +import { getExtensionSettingsAsync } from "./settings" +import { setSetting } from "./useSettings" + export async function generateKeys() { const keys = await window.crypto.subtle.generateKey( { @@ -53,4 +56,33 @@ export async function sign(data: string, privateKey: string) { await importPrivateKey(privateKey), Buffer.from(data) )).toString('base64') +} + +export async function loginAndSetNickname() { + const settings = await getExtensionSettingsAsync() + + let nickname; + while (true) + { + nickname = prompt("Pick a nickname") + if (nickname) break + if (nickname === null) return + alert("Invalid nickname") + } + + if (!settings.privateKey || !settings.publicKey) + await generateKeys().then((keys) => { + setSetting('publicKey', keys.publicKey) + setSetting('privateKey', keys.privateKey) + }) + + const url = new URL('https://finder.madiator.com/api/v1/profile') + url.searchParams.set('data', JSON.stringify({ nickname })) + url.searchParams.set('keys', JSON.stringify({ + signature: await sign(url.searchParams.toString(), settings.privateKey!), + publicKey: settings.publicKey + })) + const respond = await fetch(url.href, { method: "POST" }) + if (respond.ok) alert(`Your nickname has been set to ${nickname}`) + else alert((await respond.json()).message) } \ No newline at end of file diff --git a/src/popup/popup.sass b/src/popup/popup.sass index ed3c5bd..656878b 100644 --- a/src/popup/popup.sass +++ b/src/popup/popup.sass @@ -7,10 +7,17 @@ grid-auto-flow: row gap: 1.5em -.container > section +.container section display: grid grid-auto-flow: row gap: 1em button - cursor: pointer \ No newline at end of file + cursor: pointer + +.container .options + display: flex + gap: .25em + white-space: nowrap + flex-wrap: wrap + justify-content: center diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 9c63644..31819fe 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,6 +1,7 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio' +import { loginAndSetNickname } from '../common/crypto' import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings' import { setSetting, useLbrySettings } from '../common/useSettings' import { LbryPathnameCache } from '../common/yt/urlCache' @@ -15,7 +16,7 @@ const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntirie .map(([value, { name: display }]) => ({ value, display })) function WatchOnLbryPopup() { - const { redirect, targetPlatform, urlResolver } = useLbrySettings() + const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useLbrySettings() let [clearingCache, updateClearingCache] = useState(() => false) return
@@ -42,6 +43,40 @@ function WatchOnLbryPopup() {
+
+ + + + +
+
+ + {(!privateKey || !publicKey) ? + + loginAndSetNickname()} target='_blank'> + + + loginAndSetNickname()} target='_blank'> + + + + : + + loginAndSetNickname()} target='_blank'> + + + loginAndSetNickname()} target='_blank'> + + + loginAndSetNickname()} target='_blank'> + + + loginAndSetNickname()} target='_blank'> + + + + } +
diff --git a/src/scripts/storageSetup.ts b/src/scripts/storageSetup.ts index 841bd41..7f77360 100644 --- a/src/scripts/storageSetup.ts +++ b/src/scripts/storageSetup.ts @@ -17,12 +17,6 @@ async function initSettings() { chrome.storage.local.set(changeSet) } - if (!settings.privateKey || !settings.publicKey) - await generateKeys().then((keys) => { - setSetting('publicKey', keys.publicKey) - setSetting('privateKey', keys.privateKey) - }) - chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' }) } From 81f1742289e2f24668c1a855d676e5c0f1eb92f7 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Fri, 22 Apr 2022 20:42:57 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=8D=99=20updating=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.css | 61 +++++++++++++++++++++ src/popup/popup.css | 69 ++++++++++++++++++++++++ src/popup/popup.html | 4 +- src/popup/popup.sass | 23 -------- src/popup/popup.tsx | 123 ++++++++++++++++++------------------------ 5 files changed, 184 insertions(+), 96 deletions(-) create mode 100644 src/common/common.css create mode 100644 src/popup/popup.css delete mode 100644 src/popup/popup.sass diff --git a/src/common/common.css b/src/common/common.css new file mode 100644 index 0000000..3cfe324 --- /dev/null +++ b/src/common/common.css @@ -0,0 +1,61 @@ +:root { + --color-master: #488e77; + --color-slave: #458593; + --color-error: rgb(245, 81, 69); + --color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave)); + --color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%); + --color-dark: #0e1117; + --color-light: rgb(235, 237, 241); + --gradient-animation: gradient-animation 5s linear infinite alternate; +} + +:root { + letter-spacing: .2ch; +} + +body { + background: linear-gradient(to left top, var(--color-master), var(--color-slave)); + background-attachment: fixed; + color: var(--color-light); + padding: 0; + margin: 0; +} + +*, +*::before, +*::after { + box-sizing: border-box; + position: relative; +} + +.button { + display: inline-flex; + justify-content: center; + align-items: center; + padding: .5em 1em; + + background: var(--color-dark); + color: var(--color-light); + + cursor: pointer; + border-radius: .5em; +} + +.button.active { + background: var(--color-gradient-0); +} + +.button.active::before { + content: ""; + position: absolute; + inset: 0; + background: var(--color-gradient-0); + filter: blur(.5em); + z-index: -1; + border-radius: .5em; +} + +.button.disabled { + filter: saturate(0); + pointer-events: none; +} \ No newline at end of file diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..66cc6a6 --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,69 @@ +.popup { + width: 35em; + max-width: 100%; + overflow: hidden; +} + +label { + font-size: 2em; + font-weight: bold; + text-align: center; +} + +header { + display: grid; + position: sticky; + top: 0; + justify-content: end; + background: rgba(19, 19, 19, 0.95); + padding: .5em .2em; +} + +.profile { + display: grid; + grid-template-columns: auto auto; + align-items: center; + gap: .25em; +} + +.profile .name { + display: inline-block; + width: 10em; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; +} + +.profile::before { + content: ''; + display: inline-block; + width: 4ch; + aspect-ratio: 1/1; + background: var(--color-gradient-0); + border-radius: 100000%; +} + +main { + display: grid; + gap: 1em; + padding: .25em; +} + +section { + display: grid; + justify-items: center; + gap: .75em; + background: rgba(19, 19, 19, 0.75); + border-radius: 1em; + padding: .5em; +} + +.options { + display: grid; + width: 100%; + grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); + justify-content: center; + gap: .25em; +} \ No newline at end of file diff --git a/src/popup/popup.html b/src/popup/popup.html index d419cc8..1312995 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -6,9 +6,7 @@ -
-
-
+
\ No newline at end of file diff --git a/src/popup/popup.sass b/src/popup/popup.sass deleted file mode 100644 index 656878b..0000000 --- a/src/popup/popup.sass +++ /dev/null @@ -1,23 +0,0 @@ -.radio-label - font-size: 1.1rem - display: block - -.container - display: grid - grid-auto-flow: row - gap: 1.5em - -.container section - display: grid - grid-auto-flow: row - gap: 1em - -button - cursor: pointer - -.container .options - display: flex - gap: .25em - white-space: nowrap - flex-wrap: wrap - justify-content: center diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 31819fe..54dd406 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,88 +1,71 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' -import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio' -import { loginAndSetNickname } from '../common/crypto' -import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings' +import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries } from '../common/settings' import { setSetting, useLbrySettings } from '../common/useSettings' +import '../common/common.css' +import './popup.css' import { LbryPathnameCache } from '../common/yt/urlCache' -import './popup.sass' +import { loginAndSetNickname } from '../common/crypto' /** Gets all the options for redirect destinations as selection options */ -const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries() - .map(([value, { displayName: display }]) => ({ value, display })) +const targetPlatforms = getTargetPlatfromSettingsEntiries() -const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntiries() - .map(([value, { name: display }]) => ({ value, display })) +const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries() function WatchOnLbryPopup() { const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useLbrySettings() let [clearingCache, updateClearingCache] = useState(() => false) - return
-
- - setSetting('redirect', redirect.toLowerCase() === 'yes')} /> -
-
- - setSetting('targetPlatform', platform)} /> -
-
- - setSetting('urlResolver', urlResolver)} /> -
-
- { - await LbryPathnameCache.clearAll() - alert('Cleared Cache.') - }}> - - -
-
- - - - -
-
- - {(!privateKey || !publicKey) ? - - loginAndSetNickname()} target='_blank'> - + return +
+
+ + +
+
+ + + { + updateClearingCache(true) + await LbryPathnameCache.clearAll() + updateClearingCache(false) + alert("Cleared Cache!") + }} className={`button active ${clearingCache ? 'disabled' : ''}`}> + Clear Resolver Cache + +
+
} From 3ee7e530d6f4d7b302e741e96db1ad591e1e1031 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sat, 30 Apr 2022 12:23:03 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=94=A5=20New=20UI=20and=20Madiator=20?= =?UTF-8?q?Finder=20Features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.css | 4 +- src/common/crypto.ts | 149 ++++++++++++++++++++++++++--- src/common/useSettings.ts | 3 +- src/popup/popup.css | 142 +++++++++++++++++++++++---- src/popup/popup.tsx | 186 ++++++++++++++++++++++++++---------- src/scripts/storageSetup.ts | 2 - 6 files changed, 403 insertions(+), 83 deletions(-) diff --git a/src/common/common.css b/src/common/common.css index 3cfe324..294b5f5 100644 --- a/src/common/common.css +++ b/src/common/common.css @@ -1,6 +1,6 @@ :root { - --color-master: #488e77; - --color-slave: #458593; + --color-master: #499375; + --color-slave: #43889d; --color-error: rgb(245, 81, 69); --color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave)); --color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%); diff --git a/src/common/crypto.ts b/src/common/crypto.ts index c6d27d6..c5b5d39 100644 --- a/src/common/crypto.ts +++ b/src/common/crypto.ts @@ -1,7 +1,7 @@ import { getExtensionSettingsAsync } from "./settings" import { setSetting } from "./useSettings" -export async function generateKeys() { +async function generateKeys() { const keys = await window.crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", @@ -58,31 +58,154 @@ export async function sign(data: string, privateKey: string) { )).toString('base64') } -export async function loginAndSetNickname() { - const settings = await getExtensionSettingsAsync() +export function resetProfileSettings() { + setSetting('publicKey', null) + setSetting('privateKey', null) +} - let nickname; - while (true) - { +export async function generateProfileAndSetNickname(overwrite = false) { + let { publicKey, privateKey } = await getExtensionSettingsAsync() + + let nickname + while (true) { nickname = prompt("Pick a nickname") if (nickname) break if (nickname === null) return alert("Invalid nickname") } - if (!settings.privateKey || !settings.publicKey) + if (overwrite || !privateKey || !publicKey) { + resetProfileSettings() await generateKeys().then((keys) => { - setSetting('publicKey', keys.publicKey) - setSetting('privateKey', keys.privateKey) + publicKey = keys.publicKey + privateKey = keys.privateKey }) - + } + const url = new URL('https://finder.madiator.com/api/v1/profile') url.searchParams.set('data', JSON.stringify({ nickname })) url.searchParams.set('keys', JSON.stringify({ - signature: await sign(url.searchParams.toString(), settings.privateKey!), - publicKey: settings.publicKey + signature: await sign(url.searchParams.toString(), privateKey!), + publicKey })) const respond = await fetch(url.href, { method: "POST" }) - if (respond.ok) alert(`Your nickname has been set to ${nickname}`) + if (respond.ok) { + setSetting('publicKey', publicKey) + setSetting('privateKey', privateKey) + alert(`Your nickname has been set to ${nickname}`) + } else alert((await respond.json()).message) +} + +export async function purgeProfile() { + try { + if (!confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return + const settings = await getExtensionSettingsAsync() + + if (!settings.privateKey || !settings.publicKey) + throw new Error('There is no profile to be purged.') + + const url = new URL('https://finder.madiator.com/api/v1/profile/purge') + url.searchParams.set('keys', JSON.stringify({ + signature: await sign(url.searchParams.toString(), settings.privateKey!), + publicKey: settings.publicKey + })) + const respond = await fetch(url.href, { method: "POST" }) + if (respond.ok) { + resetProfileSettings() + alert(`Your profile has been purged`) + } + else throw new Error((await respond.json()).message) + } catch (error: any) { + alert(error.message) + } +} + +export async function getProfile() { + try { + const settings = await getExtensionSettingsAsync() + + if (!settings.privateKey || !settings.publicKey) + throw new Error('There is no profile.') + + const url = new URL('https://finder.madiator.com/api/v1/profile') + url.searchParams.set('data', JSON.stringify({ publicKey: settings.publicKey })) + url.searchParams.set('keys', JSON.stringify({ + signature: await sign(url.searchParams.toString(), settings.privateKey!), + publicKey: settings.publicKey + })) + const respond = await fetch(url.href, { method: "GET" }) + if (respond.ok) { + const profile = await respond.json() as { nickname: string, score: number, publickKey: string } + return profile + } + else throw new Error((await respond.json()).message) + } catch (error: any) { + console.error(error) + } +} + +export function friendlyPublicKey(publicKey: string | null) { + // This is copy paste of Madiator Finder's friendly public key + return publicKey?.substring(publicKey.length - 64, publicKey.length - 32) +} + +function download(data: string, filename: string, type: string) { + const file = new Blob([data], { type: type }) + const a = document.createElement("a") + const url = URL.createObjectURL(file) + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + setTimeout(() => { + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + }) +} + +async function readFile() { + return await new Promise((resolve) => { + const input = document.createElement("input") + input.type = 'file' + input.accept = '.wol-keys.json' + + input.click() + input.addEventListener("change", () => { + if (!input.files?.[0]) return + const myFile = input.files[0] + const reader = new FileReader() + + reader.addEventListener('load', () => resolve(reader.result?.toString() ?? null)) + reader.readAsBinaryString(myFile) + }) + }) +} + +interface ExportedProfileKeysFile { + publicKey: string + privateKey: string +} + +export async function exportProfileKeysAsFile() { + const { publicKey, privateKey } = await getExtensionSettingsAsync() + + const json = JSON.stringify({ + publicKey, + privateKey + }) + + download(json, `watch-on-lbry-profile-export-${friendlyPublicKey(publicKey)}.wol-keys.json`, 'application/json') +} + +export async function importProfileKeysFromFile() { + try { + const json = await readFile() + if (!json) throw new Error("Invalid") + const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile + setSetting('publicKey', publicKey) + setSetting('privateKey', privateKey) + } catch (error: any) { + alert(error.message) + } } \ No newline at end of file diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index 8199f35..5d7b8f1 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -1,5 +1,4 @@ import { useEffect, useReducer } from 'preact/hooks' -import { generateKeys } from './crypto' import { DEFAULT_SETTINGS, ExtensionSettings } from './settings' /** @@ -7,7 +6,7 @@ import { DEFAULT_SETTINGS, ExtensionSettings } from './settings' * * @param defaultSettings the default value. Must have all relevant keys present and should not change */ -export function useSettings(defaultSettings: ExtensionSettings) { +function useSettings(defaultSettings: ExtensionSettings) { const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), defaultSettings) const settingsKeys = Object.keys(defaultSettings) // register change listeners, gets current values, and cleans up the listeners on unload diff --git a/src/popup/popup.css b/src/popup/popup.css index 66cc6a6..2089cb9 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -1,15 +1,55 @@ -.popup { - width: 35em; - max-width: 100%; - overflow: hidden; +:root { + --color-master: #499375; + --color-slave: #43889d; + --color-error: rgb(245, 81, 69); + --color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave)); + --color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%); + --color-dark: #0e1117; + --color-light: rgb(235, 237, 241); + --gradient-animation: gradient-animation 5s linear infinite alternate; +} + +:root { + letter-spacing: .2ch; +} + +body { + background: linear-gradient(to left top, var(--color-master), var(--color-slave)); + background-attachment: fixed; + color: var(--color-light); + padding: 0; + margin: 0; +} + +body::before { + content: ""; + position: absolute; + inset: 0; + background: rgba(19, 19, 19, 0.75); +} + +*, +*::before, +*::after { + box-sizing: border-box; + position: relative; } label { - font-size: 2em; + font-size: 1.75em; font-weight: bold; text-align: center; } +a { + cursor: pointer; +} + +p { + margin: 0; + text-align: center; +} + header { display: grid; position: sticky; @@ -19,27 +59,99 @@ header { padding: .5em .2em; } +.button { + display: inline-flex; + place-items: center; + text-align: center; + padding: .5em 1em; + + background: var(--color-dark); + color: var(--color-light); + + cursor: pointer; + border-radius: .5em; +} + +.filled { + background: var(--color-gradient-0); + background-clip: text; + -webkit-background-clip: text; + font-weight: bold; + color: transparent; +} + +.button.active { + background: var(--color-gradient-0); +} + +.button.active::before { + content: ""; + position: absolute; + inset: 0; + background: var(--color-gradient-0); + filter: blur(.5em); + z-index: -1; + border-radius: .5em; +} + +.button.disabled { + filter: saturate(0); + pointer-events: none; +} + +.go-back { + color: currentColor; + text-decoration: none; + font-size: 1.5em; +} + +.popup { + width: 35em; + max-width: 100%; + overflow: hidden; +} + +.overlay { + display: grid; + place-items: center; + position: fixed; + inset: 0; + font-size: 2em; + font-weight: bold; +} + +.overlay::before { + content: ''; + position: absolute; + inset: 0; + background-color: #0e1117; + opacity: .75; +} + .profile { display: grid; grid-template-columns: auto auto; align-items: center; gap: .25em; + font-size: 1.5em; } .profile .name { - display: inline-block; - width: 10em; - max-width: 100%; + display: inline-flex; + gap: .5em; + place-items: center; + max-width: min(10em, 100%); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: bold; + color: currentColor; } -.profile::before { +.profile .name::before { content: ''; display: inline-block; - width: 4ch; + width: 1ch; aspect-ratio: 1/1; background: var(--color-gradient-0); border-radius: 100000%; @@ -47,17 +159,14 @@ header { main { display: grid; - gap: 1em; - padding: .25em; -} + gap: 2em; + padding: 1.5em 0.5em; +} section { display: grid; justify-items: center; gap: .75em; - background: rgba(19, 19, 19, 0.75); - border-radius: 1em; - padding: .5em; } .options { @@ -66,4 +175,5 @@ section { grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); justify-content: center; gap: .25em; + padding: 0 1.5em; } \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 54dd406..9357fab 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,72 +1,162 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' +import '../common/common.css' +import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, importProfileKeysFromFile, purgeProfile, resetProfileSettings } from '../common/crypto' import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries } from '../common/settings' import { setSetting, useLbrySettings } from '../common/useSettings' -import '../common/common.css' -import './popup.css' import { LbryPathnameCache } from '../common/yt/urlCache' -import { loginAndSetNickname } from '../common/crypto' +import './popup.css' /** Gets all the options for redirect destinations as selection options */ const targetPlatforms = getTargetPlatfromSettingsEntiries() - const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries() -function WatchOnLbryPopup() { +function WatchOnLbryPopup(params: { profile: Awaited> | null }) { const { redirect, targetPlatform, urlResolver, privateKey, publicKey } = useLbrySettings() - let [clearingCache, updateClearingCache] = useState(() => false) + let [loading, updateLoading] = useState(() => false) + let [popupRoute, updateRoute] = useState(() => null) + + const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...' + + async function startAsyncOperation(operation: Promise) { + try { + updateLoading(true) + await operation + } catch (error) { + console.error(error) + } + finally { + updateLoading(false) + } + } return
-
-
- - -
-
- -
- {targetPlatforms.map(([name, value]) => - setSetting('targetPlatform', name)} className={`button ${targetPlatform === name ? 'active' : ''}`}> - {value.displayName} + { + popupRoute === 'profile' ? + publicKey ? +
+ updateRoute('')} className="go-back link">⇐ Back +
+ +

{friendlyPublicKey(publicKey)}

+ Score: {params.profile?.score ?? '...'} - 🔗Leaderboard + +
+
+ +

Import and export your unique keypair.

+ +
+
+ +

Purge your profile data online and offline.

+ +
+
+ +

Generate a new keypair.

+ +
+
+ : +
+
+ +

You can either import keypair for an existing profile or generate a new profile keypair.

+ +
+
+ : +
+
+ + +
+
+ + +
+
+ + + startAsyncOperation(LbryPathnameCache.clearAll()).then(() => alert("Cleared Cache!"))} className={`button active`}> + Clear Resolver Cache - )} -
-
-
- -
+
+ + + Subscription Converter - )} -
- { - updateClearingCache(true) - await LbryPathnameCache.clearAll() - updateClearingCache(false) - alert("Cleared Cache!") - }} className={`button active ${clearingCache ? 'disabled' : ''}`}> - Clear Resolver Cache - -
- +
+ + } + {loading &&
+ Loading... +
}
} -render(, document.getElementById('root')!) +function renderPopup() { + render(, document.getElementById('root')!) + getProfile().then((profile) => render(, document.getElementById('root')!)) +} + +renderPopup() \ No newline at end of file diff --git a/src/scripts/storageSetup.ts b/src/scripts/storageSetup.ts index 7f77360..e79a159 100644 --- a/src/scripts/storageSetup.ts +++ b/src/scripts/storageSetup.ts @@ -1,6 +1,4 @@ -import { generateKeys } from '../common/crypto' import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from '../common/settings' -import { setSetting } from '../common/useSettings' /** Reset settings to default value and update the browser badge text */ async function initSettings() { From f1f4c335e9ab3bbda3a81ccca80e8d1ede926060 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sat, 30 Apr 2022 12:49:14 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=94=A5=20New=20UI=20and=20Madiator=20?= =?UTF-8?q?Finder=20Features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/popup/popup.css | 85 +++++++++++++++------------------------------ src/popup/popup.tsx | 33 +++++++++++------- 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/src/popup/popup.css b/src/popup/popup.css index 2089cb9..a26fbe9 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -52,11 +52,29 @@ p { header { display: grid; + gap: .5em; + padding: .75em; position: sticky; top: 0; - justify-content: end; - background: rgba(19, 19, 19, 0.95); - padding: .5em .2em; + background: rgba(19, 19, 19, 0.5); +} + +main { + display: grid; + gap: 2em; + padding: 1.5em 0.5em; +} + +section { + display: grid; + justify-items: center; + gap: .75em; +} + +#popup { + width: 35em; + max-width: 100%; + overflow: hidden; } .button { @@ -105,10 +123,13 @@ header { font-size: 1.5em; } -.popup { - width: 35em; - max-width: 100%; - overflow: hidden; +.options { + display: grid; + width: 100%; + grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); + justify-content: center; + gap: .25em; + padding: 0 1.5em; } .overlay { @@ -126,54 +147,4 @@ header { inset: 0; background-color: #0e1117; opacity: .75; -} - -.profile { - display: grid; - grid-template-columns: auto auto; - align-items: center; - gap: .25em; - font-size: 1.5em; -} - -.profile .name { - display: inline-flex; - gap: .5em; - place-items: center; - max-width: min(10em, 100%); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: bold; - color: currentColor; -} - -.profile .name::before { - content: ''; - display: inline-block; - width: 1ch; - aspect-ratio: 1/1; - background: var(--color-gradient-0); - border-radius: 100000%; -} - -main { - display: grid; - gap: 2em; - padding: 1.5em 0.5em; -} - -section { - display: grid; - justify-items: center; - gap: .75em; -} - -.options { - display: grid; - width: 100%; - grid-template-columns: repeat(auto-fit, minmax(3em, 1fr)); - justify-content: center; - gap: .25em; - padding: 0 1.5em; } \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 9357fab..ebe8f92 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -31,23 +31,32 @@ function WatchOnLbryPopup(params: { profile: Awaited -
- -
+ return