diff --git a/.gitignore b/.gitignore index 0db3c80f6..f5d79e09d 100755 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ *.py[cod] __pycache__ -# C extensions -*.so - # Packages *.egg *.egg-info diff --git a/frontend/src/basic/RobotPage/Onboarding.tsx b/frontend/src/basic/RobotPage/Onboarding.tsx index 38d54aac7..1d6d2fad3 100644 --- a/frontend/src/basic/RobotPage/Onboarding.tsx +++ b/frontend/src/basic/RobotPage/Onboarding.tsx @@ -178,7 +178,7 @@ const Onboarding = ({ /> - {slot?.hashId ? ( + {slot?.nickname ? ( {t('Hi! My name is')} diff --git a/frontend/src/basic/RobotPage/RobotProfile.tsx b/frontend/src/basic/RobotPage/RobotProfile.tsx index 2ac4f354d..4a21bfbb8 100644 --- a/frontend/src/basic/RobotPage/RobotProfile.tsx +++ b/frontend/src/basic/RobotPage/RobotProfile.tsx @@ -90,7 +90,7 @@ const RobotProfile = ({ sx={{ width: '100%' }} > - {slot?.hashId ? ( + {slot?.nickname ? (
{ const token = urlToken ?? garage.currentSlot; if (token !== undefined && token !== null && page === 'robot') { setInputToken(token); - if (window.NativeRobosats === undefined || torStatus === '"Done"') { + if (window.NativeRobosats === undefined || torStatus === 'ON') { getGenerateRobot(token); setView('profile'); } @@ -83,7 +83,7 @@ const RobotPage = (): JSX.Element => { garage.deleteSlot(); }; - if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) { + if (!(window.NativeRobosats === undefined) && !(torStatus === 'ON')) { return ( { - {garage.getSlot()?.nickname !== undefined && ( + {!garage.getSlot()?.nickname && (
= ({ const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar'; useEffect(() => { - // TODO: HANDLE ANDROID AVATARS TOO (when window.NativeRobosats !== undefined) if (hashId !== undefined) { - robohash - .generate(hashId, small ? 'small' : 'large') + roboidentitiesClient + .generateRobohash(hashId, small ? 'small' : 'large') .then((avatar) => { setAvatarSrc(avatar); }) @@ -78,9 +77,7 @@ const RobotAvatar: React.FC = ({ ); } else { setAvatarSrc( - `file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}${ - small ? ' .small' : '' - }.webp`, + `file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}.webp`, ); } setTimeout(() => { diff --git a/frontend/src/components/RobotInfo/index.tsx b/frontend/src/components/RobotInfo/index.tsx index b5ce895f3..382a222ba 100644 --- a/frontend/src/components/RobotInfo/index.tsx +++ b/frontend/src/components/RobotInfo/index.tsx @@ -95,7 +95,6 @@ const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) = (signedInvoice) => { console.log('Signed message:', signedInvoice); void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => { - console.log(data); setBadInvoice(data.bad_invoice ?? ''); setShowRewardsSpinner(false); setWithdrawn(data.successful_withdrawal); diff --git a/frontend/src/components/SettingsForm/index.tsx b/frontend/src/components/SettingsForm/index.tsx index 7d1f149db..1624675b2 100644 --- a/frontend/src/components/SettingsForm/index.tsx +++ b/frontend/src/components/SettingsForm/index.tsx @@ -226,7 +226,6 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => { value={settings.network} onChange={(e, network) => { setSettings({ ...settings, network }); - void federation.updateUrls(origin, { ...settings, network }, hostUrl); systemClient.setItem('settings_network', network); }} > diff --git a/frontend/src/components/TorConnection.tsx b/frontend/src/components/TorConnection.tsx index 77e33b88a..006dd1b07 100644 --- a/frontend/src/components/TorConnection.tsx +++ b/frontend/src/components/TorConnection.tsx @@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => { return <>; } - if (torStatus === 'NOTINIT') { + if (torStatus === 'OFF' || torStatus === 'STOPPING') { return ( { title={t('Connecting to TOR network')} /> ); - } else if (torStatus === '"Done"' || torStatus === 'DONE') { + } else if (torStatus === 'ON') { return ; } else { return ( diff --git a/frontend/src/components/TorConnection/index.tsx b/frontend/src/components/TorConnection/index.tsx index deffc0da1..4484c2138 100644 --- a/frontend/src/components/TorConnection/index.tsx +++ b/frontend/src/components/TorConnection/index.tsx @@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => { return <>; } - if (torStatus === 'NOTINIT') { + if (torStatus === 'OFF' || torStatus === 'STOPING') { return ( { title={t('Connecting to TOR network')} /> ); - } else if (torStatus === '"Done"' || torStatus === 'DONE') { + } else if (torStatus === 'ON') { return ; } else { return ( diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx index b2a792600..45b4a84ce 100644 --- a/frontend/src/contexts/AppContext.tsx +++ b/frontend/src/contexts/AppContext.tsx @@ -37,7 +37,7 @@ export interface SlideDirection { out: 'left' | 'right' | undefined; } -export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE'; +export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF'; export const isNativeRoboSats = !(window.NativeRobosats === undefined); @@ -155,8 +155,8 @@ export interface UseAppStoreType { export const initialAppContext: UseAppStoreType = { theme: undefined, - torStatus: 'NOTINIT', - settings: getSettings(), + torStatus: 'STARTING', + settings: new Settings(), setSettings: () => {}, page: entryPage, setPage: () => {}, @@ -225,7 +225,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E () => { setTorStatus(event?.detail); }, - event?.detail === '"Done"' ? 5000 : 0, + event?.detail === 'ON' ? 5000 : 0, ); }); }, []); diff --git a/frontend/src/contexts/FederationContext.tsx b/frontend/src/contexts/FederationContext.tsx index a1f3ac8e1..dc146e0d9 100644 --- a/frontend/src/contexts/FederationContext.tsx +++ b/frontend/src/contexts/FederationContext.tsx @@ -9,12 +9,13 @@ import React, { type ReactNode, } from 'react'; -import { type Order, Federation } from '../models'; +import { type Order, Federation, Settings } from '../models'; import { federationLottery } from '../utils'; import { AppContext, type UseAppStoreType } from './AppContext'; import { GarageContext, type UseGarageStoreType } from './GarageContext'; +import NativeRobosats from '../services/Native'; // Refresh delays (ms) according to Order status const defaultDelay = 5000; @@ -61,7 +62,7 @@ export interface UseFederationStoreType { } export const initialFederationContext: UseFederationStoreType = { - federation: new Federation(), + federation: new Federation('onion', new Settings(), ''), sortedCoordinators: [], setDelay: () => {}, currentOrderId: { id: null, shortAlias: null }, @@ -79,7 +80,7 @@ export const FederationContextProvider = ({ const { settings, page, origin, hostUrl, open, torStatus } = useContext(AppContext); const { setMaker, garage, setBadOrder } = useContext(GarageContext); - const [federation, setFederation] = useState(initialFederationContext.federation); + const [federation] = useState(new Federation(origin, settings, hostUrl)); const sortedCoordinators = useMemo(() => federationLottery(federation), []); const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState( new Date().toISOString(), @@ -101,19 +102,20 @@ export const FederationContextProvider = ({ setMaker((maker) => { return { ...maker, coordinator: sortedCoordinators[0] }; }); // default MakerForm coordinator is decided via sorted lottery - }, []); - - useEffect(() => { - // On bitcoin network change we reset book, limits and federation info and fetch everything again - const newFed = initialFederationContext.federation; - newFed.registerHook('onFederationUpdate', () => { + federation.registerHook('onFederationUpdate', () => { setFederationUpdatedAt(new Date().toISOString()); }); - newFed.registerHook('onCoordinatorUpdate', () => { + federation.registerHook('onCoordinatorUpdate', () => { setCoordinatorUpdatedAt(new Date().toISOString()); }); - void newFed.start(origin, settings, hostUrl); - setFederation(newFed); + }, []); + + useEffect(() => { + // On bitcoin network change we reset book, limits and federation info and fetch everything again + if (window.NativeRobosats === undefined || torStatus === 'ON') { + void federation.updateUrl(origin, settings, hostUrl); + void federation.update(); + } }, [settings.network, torStatus]); const onOrderReceived = (order: Order): void => { diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts index cd902e16d..ef1abb19d 100644 --- a/frontend/src/models/Coordinator.model.ts +++ b/frontend/src/models/Coordinator.model.ts @@ -6,11 +6,11 @@ import { type Order, type Garage, } from '.'; +import { roboidentitiesClient } from '../services/Roboidentities/Web'; import { apiClient } from '../services/api'; import { validateTokenEntropy } from '../utils'; import { compareUpdateLimit } from './Limit.model'; import { defaultOrder } from './Order.model'; -import { robohash } from '../components/RobotAvatar/RobohashGenerator'; export interface Contact { nostr?: string | undefined; @@ -97,7 +97,7 @@ function calculateSizeLimit(inputDate: Date): number { } export class Coordinator { - constructor(value: any) { + constructor(value: any, origin: Origin, settings: Settings, hostUrl: string) { const established = new Date(value.established); this.longAlias = value.longAlias; this.shortAlias = value.shortAlias; @@ -115,6 +115,8 @@ export class Coordinator { this.testnetNodesPubkeys = value.testnetNodesPubkeys; this.url = ''; this.basePath = ''; + + this.updateUrl(origin, settings, hostUrl); } // These properties are loaded from federation.json @@ -145,22 +147,7 @@ export class Coordinator { public loadingLimits: boolean = false; public loadingRobot: boolean = true; - start = async ( - origin: Origin, - settings: Settings, - hostUrl: string, - onUpdate: (shortAlias: string) => void = () => {}, - ): Promise => { - if (this.enabled !== true) return; - void this.updateUrl(settings, origin, hostUrl, onUpdate); - }; - - updateUrl = async ( - settings: Settings, - origin: Origin, - hostUrl: string, - onUpdate: (shortAlias: string) => void = () => {}, - ): Promise => { + updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => { if (settings.selfhostedClient && this.shortAlias !== 'local') { this.url = hostUrl; this.basePath = `/${settings.network}/${this.shortAlias}`; @@ -168,9 +155,6 @@ export class Coordinator { this.url = String(this[settings.network][origin]); this.basePath = ''; } - void this.update(() => { - onUpdate(this.shortAlias); - }); }; update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise => { @@ -191,7 +175,7 @@ export class Coordinator { generateAllMakerAvatars = async (data: [PublicOrder]): Promise => { for (const order of data) { - void robohash.generate(order.maker_hash_id, 'small'); + roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small'); } }; @@ -370,7 +354,6 @@ export class Coordinator { return await apiClient .get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders) .then((data) => { - console.log('data', data); const order: Order = { ...defaultOrder, ...data, diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index 4b26623c2..7f72531e7 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -14,14 +14,14 @@ import { updateExchangeInfo } from './Exchange.model'; type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate'; export class Federation { - constructor() { + constructor(origin: Origin, settings: Settings, hostUrl: string) { this.coordinators = Object.entries(defaultFederation).reduce( (acc: Record, [key, value]: [string, any]) => { if (getHost() !== '127.0.0.1:8000' && key === 'local') { // Do not add `Local Dev` unless it is running on localhost return acc; } else { - acc[key] = new Coordinator(value); + acc[key] = new Coordinator(value, origin, settings, hostUrl); return acc; } }, @@ -36,7 +36,16 @@ export class Federation { onCoordinatorUpdate: [], onFederationUpdate: [], }; + this.loading = true; + this.exchange.loadingCoordinators = Object.keys(this.coordinators).length; + + const host = getHost(); + const url = `${window.location.protocol}//${host}`; + const tesnetHost = Object.values(this.coordinators).find((coor) => { + return Object.values(coor.testnet).includes(url); + }); + if (tesnetHost) settings.network = 'testnet'; } public coordinators: Record; @@ -69,38 +78,10 @@ export class Federation { this.triggerHook('onFederationUpdate'); }; - // Setup - start = async (origin: Origin, settings: Settings, hostUrl: string): Promise => { - const onCoordinatorStarted = (): void => { - this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1; - this.onCoordinatorSaved(); - }; - - this.loading = true; - this.exchange.loadingCoordinators = Object.keys(this.coordinators).length; - - const host = getHost(); - const url = `${window.location.protocol}//${host}`; - const tesnetHost = Object.values(this.coordinators).find((coor) => { - return Object.values(coor.testnet).includes(url); - }); - if (tesnetHost) settings.network = 'testnet'; - - for (const coor of Object.values(this.coordinators)) { - if (coor.enabled) { - await coor.start(origin, settings, hostUrl, onCoordinatorStarted); - } - } - this.updateEnabledCoordinators(); - }; - - // On Testnet/Mainnet change - updateUrls = async (origin: Origin, settings: Settings, hostUrl: string): Promise => { - this.loading = true; + updateUrl = async (origin: Origin, settings: Settings, hostUrl: string): Promise => { for (const coor of Object.values(this.coordinators)) { - await coor.updateUrl(settings, origin, hostUrl); + coor.updateUrl(origin, settings, hostUrl); } - this.loading = false; }; update = async (): Promise => { @@ -115,9 +96,12 @@ export class Federation { lifetime_volume: 0, version: { major: 0, minor: 0, patch: 0 }, }; + this.exchange.onlineCoordinators = 0; this.exchange.loadingCoordinators = Object.keys(this.coordinators).length; + this.updateEnabledCoordinators(); for (const coor of Object.values(this.coordinators)) { await coor.update(() => { + this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1; this.onCoordinatorSaved(); }); } diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts index a6258cd1a..47204b918 100644 --- a/frontend/src/models/Garage.model.ts +++ b/frontend/src/models/Garage.model.ts @@ -59,7 +59,9 @@ class Garage { const rawSlots = JSON.parse(slotsDump); Object.values(rawSlots).forEach((rawSlot: Record) => { if (rawSlot?.token) { - this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}); + this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}, () => + this.triggerHook('onRobotUpdate'), + ); Object.keys(rawSlot.robots).forEach((shortAlias) => { const rawRobot = rawSlot.robots[shortAlias]; @@ -113,9 +115,10 @@ class Garage { if (!token || !shortAliases) return; if (this.getSlot(token) === null) { - this.slots[token] = new Slot(token, shortAliases, attributes); + this.slots[token] = new Slot(token, shortAliases, attributes, () => + this.triggerHook('onRobotUpdate'), + ); this.save(); - this.triggerHook('onRobotUpdate'); } }; diff --git a/frontend/src/models/Slot.model.ts b/frontend/src/models/Slot.model.ts index 864820ed4..76909bf09 100644 --- a/frontend/src/models/Slot.model.ts +++ b/frontend/src/models/Slot.model.ts @@ -1,17 +1,24 @@ import { sha256 } from 'js-sha256'; import { Robot, type Order } from '.'; -import { robohash } from '../components/RobotAvatar/RobohashGenerator'; -import { generate_roboname } from 'robo-identities-wasm'; +import { roboidentitiesClient } from '../services/Roboidentities/Web'; class Slot { - constructor(token: string, shortAliases: string[], robotAttributes: Record) { + constructor( + token: string, + shortAliases: string[], + robotAttributes: Record, + onRobotUpdate: () => void, + ) { this.token = token; this.hashId = sha256(sha256(this.token)); - this.nickname = generate_roboname(this.hashId); - // trigger RoboHash avatar generation in webworker and store in RoboHash class cache. - void robohash.generate(this.hashId, 'small'); - void robohash.generate(this.hashId, 'large'); + this.nickname = null; + roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => { + this.nickname = nickname; + onRobotUpdate(); + }); + roboidentitiesClient.generateRobohash(this.hashId, 'small'); + roboidentitiesClient.generateRobohash(this.hashId, 'large'); this.robots = shortAliases.reduce((acc: Record, shortAlias: string) => { acc[shortAlias] = new Robot(robotAttributes); @@ -22,6 +29,7 @@ class Slot { this.activeShortAlias = null; this.lastShortAlias = null; this.copiedToken = false; + onRobotUpdate(); } token: string | null; diff --git a/frontend/src/services/Native/index.d.ts b/frontend/src/services/Native/index.d.ts index e93edd55d..680c8427e 100644 --- a/frontend/src/services/Native/index.d.ts +++ b/frontend/src/services/Native/index.d.ts @@ -15,7 +15,7 @@ export interface ReactNativeWebView { export interface NativeWebViewMessageHttp { id?: number; category: 'http'; - type: 'post' | 'get' | 'put' | 'delete' | 'xhr'; + type: 'post' | 'get' | 'put' | 'delete'; path: string; baseUrl: string; headers?: object; @@ -30,7 +30,19 @@ export interface NativeWebViewMessageSystem { detail?: string; } -export declare type NativeWebViewMessage = NativeWebViewMessageHttp | NativeWebViewMessageSystem; +export interface NativeWebViewMessageRoboidentities { + id?: number; + category: 'roboidentities'; + type: 'roboname' | 'robohash'; + string?: string; + size?: string; +} + +export declare type NativeWebViewMessage = + | NativeWebViewMessageHttp + | NativeWebViewMessageSystem + | NativeWebViewMessageRoboidentities + | NA; export interface NativeRobosatsPromise { resolve: (value: object | PromiseLike) => void; diff --git a/frontend/src/services/Roboidentities/Native.ts b/frontend/src/services/Roboidentities/Native.ts new file mode 100644 index 000000000..250b9a81a --- /dev/null +++ b/frontend/src/services/Roboidentities/Native.ts @@ -0,0 +1,4 @@ +import RoboidentitiesClientNativeClient from './RoboidentitiesNativeClient'; +import { RoboidentitiesClient } from './type'; + +export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientNativeClient(); diff --git a/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts b/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts new file mode 100644 index 000000000..85b55b8b5 --- /dev/null +++ b/frontend/src/services/Roboidentities/RoboidentitiesNativeClient/index.ts @@ -0,0 +1,42 @@ +import { type RoboidentitiesClient } from '../type'; + +class RoboidentitiesNativeClient implements RoboidentitiesClient { + private robonames: Record = {}; + private robohashes: Record = {}; + + public generateRoboname: (initialString: string) => Promise = async (initialString) => { + if (this.robonames[initialString]) { + return this.robonames[initialString]; + } else { + const response = await window.NativeRobosats?.postMessage({ + category: 'roboidentities', + type: 'roboname', + detail: initialString, + }); + const result = response ? Object.values(response)[0] : ''; + this.robonames[initialString] = result; + return result; + } + }; + + public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise = + async (initialString, size) => { + const key = `${initialString};${size === 'small' ? 80 : 256}`; + + if (this.robohashes[key]) { + return this.robohashes[key]; + } else { + const response = await window.NativeRobosats?.postMessage({ + category: 'roboidentities', + type: 'robohash', + detail: key, + }); + const result = response ? Object.values(response)[0] : ''; + const image = `data:image/png;base64,${result}`; + this.robohashes[key] = image; + return image; + } + }; +} + +export default RoboidentitiesNativeClient; diff --git a/frontend/src/components/RobotAvatar/RobohashGenerator.ts b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts similarity index 98% rename from frontend/src/components/RobotAvatar/RobohashGenerator.ts rename to frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts index 038cdf924..f00075e07 100644 --- a/frontend/src/components/RobotAvatar/RobohashGenerator.ts +++ b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/RobohashGenerator.ts @@ -81,7 +81,7 @@ class RoboGenerator { hash, size, ) => { - const cacheKey = `${size}px;${hash}`; + const cacheKey = `${hash};${size}`; if (this.assetsCache[cacheKey]) { return this.assetsCache[cacheKey]; } else { diff --git a/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts new file mode 100644 index 000000000..20e2e1375 --- /dev/null +++ b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/index.ts @@ -0,0 +1,18 @@ +import { type RoboidentitiesClient } from '../type'; +import { generate_roboname } from 'robo-identities-wasm'; +import { robohash } from './RobohashGenerator'; + +class RoboidentitiesClientWebClient implements RoboidentitiesClient { + public generateRoboname: (initialString: string) => Promise = async (initialString) => { + return new Promise(async (resolve, _reject) => { + resolve(generate_roboname(initialString)); + }); + }; + + public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise = + async (initialString, size) => { + return robohash.generate(initialString, size); + }; +} + +export default RoboidentitiesClientWebClient; diff --git a/frontend/src/components/RobotAvatar/robohash.worker.ts b/frontend/src/services/Roboidentities/RoboidentitiesWebClient/robohash.worker.ts similarity index 100% rename from frontend/src/components/RobotAvatar/robohash.worker.ts rename to frontend/src/services/Roboidentities/RoboidentitiesWebClient/robohash.worker.ts diff --git a/frontend/src/services/Roboidentities/Web.ts b/frontend/src/services/Roboidentities/Web.ts new file mode 100644 index 000000000..8730491c6 --- /dev/null +++ b/frontend/src/services/Roboidentities/Web.ts @@ -0,0 +1,4 @@ +import RoboidentitiesClientWebClient from './RoboidentitiesWebClient'; +import { RoboidentitiesClient } from './type'; + +export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientWebClient(); diff --git a/frontend/src/services/Roboidentities/type.ts b/frontend/src/services/Roboidentities/type.ts new file mode 100644 index 000000000..4a54ea996 --- /dev/null +++ b/frontend/src/services/Roboidentities/type.ts @@ -0,0 +1,4 @@ +export interface RoboidentitiesClient { + generateRoboname: (initialString: string) => Promise; + generateRobohash: (initialString: string, size: 'small' | 'large') => Promise; +} diff --git a/frontend/src/services/api/ApiNativeClient/index.ts b/frontend/src/services/api/ApiNativeClient/index.ts index 21620f42f..6f794eecc 100644 --- a/frontend/src/services/api/ApiNativeClient/index.ts +++ b/frontend/src/services/api/ApiNativeClient/index.ts @@ -89,41 +89,6 @@ class ApiNativeClient implements ApiClient { headers: this.getHeaders(auth), }).then(this.parseResponse); }; - - public fileImageUrl: (baseUrl: string, path: string) => Promise = async ( - baseUrl, - path, - ) => { - if (path === '') { - return await Promise.resolve(''); - } - - if (this.assetsCache[path] != null) { - return await Promise.resolve(this.assetsCache[path]); - } else if (this.assetsPromises.has(path)) { - return await this.assetsPromises.get(path); - } - - this.assetsPromises.set( - path, - new Promise((resolve, reject) => { - window.NativeRobosats?.postMessage({ - category: 'http', - type: 'xhr', - baseUrl, - path, - }) - .then((fileB64: { b64Data: string }) => { - this.assetsCache[path] = `data:image/png;base64,${fileB64.b64Data}`; - this.assetsPromises.delete(path); - resolve(this.assetsCache[path]); - }) - .catch(reject); - }), - ); - - return await this.assetsPromises.get(path); - }; } export default ApiNativeClient; diff --git a/frontend/src/services/api/index.ts b/frontend/src/services/api/index.ts index b9ac94d86..d00133241 100644 --- a/frontend/src/services/api/index.ts +++ b/frontend/src/services/api/index.ts @@ -11,7 +11,6 @@ export interface ApiClient { put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise; get: (baseUrl: string, path: string, auth?: Auth) => Promise; delete: (baseUrl: string, path: string, auth?: Auth) => Promise; - fileImageUrl?: (baseUrl: string, path: string) => Promise; } export const apiClient: ApiClient = diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index d3625542f..442ae97c7 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -56,6 +56,15 @@ const configMobile: Configuration = { async: true, }, }, + { + test: path.resolve(__dirname, 'src/services/Roboidentities/Web.ts'), + loader: 'file-replace-loader', + options: { + condition: 'if-replacement-exists', + replacement: path.resolve(__dirname, 'src/services/Roboidentities/Native.ts'), + async: true, + }, + }, { test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'), loader: 'file-replace-loader', @@ -81,6 +90,10 @@ const configMobile: Configuration = { from: path.resolve(__dirname, 'static/assets/sounds'), to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/sounds'), }, + { + from: path.resolve(__dirname, 'static/federation'), + to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/federation'), + }, ], }), ], diff --git a/mobile/App.tsx b/mobile/App.tsx index 66a2babee..d61444be8 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -1,23 +1,45 @@ -import React, { useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; -import { SafeAreaView, Text, Platform, Appearance } from 'react-native'; +import { SafeAreaView, Text, Platform, Appearance, DeviceEventEmitter } from 'react-native'; import TorClient from './services/Tor'; import Clipboard from '@react-native-clipboard/clipboard'; -import NetInfo from '@react-native-community/netinfo'; import EncryptedStorage from 'react-native-encrypted-storage'; import { name as app_name, version as app_version } from './package.json'; +import TorModule from './native/TorModule'; +import RoboIdentitiesModule from './native/RoboIdentitiesModule'; const backgroundColors = { light: 'white', dark: 'black', }; +export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF'; + const App = () => { const colorScheme = Appearance.getColorScheme() ?? 'light'; const torClient = new TorClient(); const webViewRef = useRef(); const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html'; + useEffect(() => { + TorModule.start(); + DeviceEventEmitter.addListener('TorStatus', (payload) => { + if (payload.torStatus === 'OFF') TorModule.restart(); + injectMessage({ + category: 'system', + type: 'torStatus', + detail: payload.torStatus, + }); + }); + }, []); + + useEffect(() => { + const interval = setInterval(() => { + TorModule.getTorStatus(); + }, 2000); + return () => clearInterval(interval); + }, []); + const injectMessageResolve = (id: string, data?: object) => { const json = JSON.stringify(data || {}); webViewRef.current?.injectJavaScript( @@ -72,7 +94,7 @@ const App = () => { const onMessage = async (event: WebViewMessageEvent) => { const data = JSON.parse(event.nativeEvent.data); if (data.category === 'http') { - sendTorStatus(); + TorModule.getTorStatus(); if (data.type === 'get') { torClient .get(data.baseUrl, data.path, data.headers) @@ -80,7 +102,7 @@ const App = () => { injectMessageResolve(data.id, response); }) .catch((e) => onCatch(data.id, e)) - .finally(sendTorStatus); + .finally(TorModule.getTorStatus); } else if (data.type === 'post') { torClient .post(data.baseUrl, data.path, data.body, data.headers) @@ -88,7 +110,7 @@ const App = () => { injectMessageResolve(data.id, response); }) .catch((e) => onCatch(data.id, e)) - .finally(sendTorStatus); + .finally(TorModule.getTorStatus); } else if (data.type === 'delete') { torClient .delete(data.baseUrl, data.path, data.headers) @@ -96,15 +118,7 @@ const App = () => { injectMessageResolve(data.id, response); }) .catch((e) => onCatch(data.id, e)) - .finally(sendTorStatus); - } else if (data.type === 'xhr') { - torClient - .request(data.baseUrl, data.path) - .then((response: object) => { - injectMessageResolve(data.id, response); - }) - .catch((e) => onCatch(data.id, e)) - .finally(sendTorStatus); + .finally(TorModule.getTorStatus); } } else if (data.category === 'system') { if (data.type === 'init') { @@ -116,6 +130,14 @@ const App = () => { } else if (data.type === 'deleteCookie') { EncryptedStorage.removeItem(data.key); } + } else if (data.category === 'roboidentities') { + if (data.type === 'roboname') { + const roboname = await RoboIdentitiesModule.generateRoboname(data.detail); + injectMessageResolve(data.id, { roboname }); + } else if (data.type === 'robohash') { + const robohash = await RoboIdentitiesModule.generateRobohash(data.detail); + injectMessageResolve(data.id, { robohash }); + } } }; @@ -132,23 +154,6 @@ const App = () => { } catch (error) {} }; - const sendTorStatus = async (event?: any) => { - NetInfo.fetch().then(async (state) => { - let daemonStatus = 'ERROR'; - if (state.isInternetReachable) { - try { - daemonStatus = await torClient.daemon.getDaemonStatus(); - } catch {} - } - - injectMessage({ - category: 'system', - type: 'torStatus', - detail: daemonStatus, - }); - }); - }; - return ( + android:theme="@style/AppTheme" + android:extractNativeLibs="true" + > getPackages() { List packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); + packages.add(new RobosatsPackage()); + return packages; } diff --git a/mobile/android/app/src/main/java/com/robosats/RoboIdentities.java b/mobile/android/app/src/main/java/com/robosats/RoboIdentities.java new file mode 100644 index 000000000..c37f85056 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/RoboIdentities.java @@ -0,0 +1,22 @@ +package com.robosats; + +import android.util.Log; + +public class RoboIdentities { + static { + System.loadLibrary("robonames"); + System.loadLibrary("robohash"); + } + + public String generateRoboname(String initial_string) { + return nativeGenerateRoboname(initial_string); + } + + public String generateRobohash(String initial_string) { + return nativeGenerateRobohash(initial_string); + } + + // Native functions implemented in Rust. + private static native String nativeGenerateRoboname(String initial_string); + private static native String nativeGenerateRobohash(String initial_string); +} diff --git a/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java new file mode 100644 index 000000000..2eca00805 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java @@ -0,0 +1,30 @@ +package com.robosats; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.robosats.modules.RoboIdentitiesModule; +import com.robosats.modules.TorModule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RobosatsPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new TorModule(reactContext)); + modules.add(new RoboIdentitiesModule(reactContext)); + + return modules; + } +} diff --git a/mobile/android/app/src/main/java/com/robosats/modules/RoboIdentitiesModule.java b/mobile/android/app/src/main/java/com/robosats/modules/RoboIdentitiesModule.java new file mode 100644 index 000000000..c18d131b4 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/modules/RoboIdentitiesModule.java @@ -0,0 +1,37 @@ +package com.robosats.modules; + +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.robosats.RoboIdentities; + +public class RoboIdentitiesModule extends ReactContextBaseJavaModule { + private ReactApplicationContext context; + + public RoboIdentitiesModule(ReactApplicationContext reactContext) { + context = reactContext; + } + + @Override + public String getName() { + return "RoboIdentitiesModule"; + } + + @ReactMethod + public void generateRoboname(String initial_string, final Promise promise) { + String roboname = new RoboIdentities().generateRoboname(initial_string); + promise.resolve(roboname); + } + + @ReactMethod + public void generateRobohash(String initial_string, final Promise promise) { + String robohash = new RoboIdentities().generateRobohash(initial_string); + promise.resolve(robohash); + } +} diff --git a/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java b/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java new file mode 100644 index 000000000..224bc5d81 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java @@ -0,0 +1,162 @@ +package com.robosats.modules; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.robosats.tor.TorKmpManager; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + + +public class TorModule extends ReactContextBaseJavaModule { + private TorKmpManager torKmpManager; + private ReactApplicationContext context; + public TorModule(ReactApplicationContext reactContext) { + context = reactContext; + } + + @Override + public String getName() { + return "TorModule"; + } + + @ReactMethod + public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout + .readTimeout(30, TimeUnit.SECONDS) // Set read timeout + .proxy(torKmpManager.getProxy()).build(); + + Request.Builder requestBuilder = new Request.Builder().url(url); + + JSONObject headersObject = new JSONObject(headers); + headersObject.keys().forEachRemaining(key -> { + String value = headersObject.optString(key); + requestBuilder.addHeader(key, value); + }); + + if (Objects.equals(action, "DELETE")) { + requestBuilder.delete(); + } else if (Objects.equals(action, "POST")) { + RequestBody requestBody = RequestBody.create(body, MediaType.get("application/json; charset=utf-8")); + requestBuilder.post(requestBody); + } else { + requestBuilder.get(); + } + + Request request = requestBuilder.build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + Log.d("RobosatsError", e.toString()); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + String body = response.body() != null ? response.body().string() : "{}"; + JSONObject headersJson = new JSONObject(); + response.headers().names().forEach(name -> { + try { + headersJson.put(name, response.header(name)); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + if (response.code() != 200 && response.code() != 201) { + Log.d("RobosatsError", "Request error code: " + response.code()); + } else if (response.isSuccessful()) { + promise.resolve("{\"json\":" + body + ", \"headers\": " + headersJson +"}"); + } + } + }); + } + + @ReactMethod + public void getTorStatus() { + String torState = torKmpManager.getTorState().getState().name(); + WritableMap payload = Arguments.createMap(); + payload.putString("torStatus", torState); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorStatus", payload); + } + + @ReactMethod + public void isConnected() { + String isConnected = String.valueOf(torKmpManager.isConnected()); + WritableMap payload = Arguments.createMap(); + payload.putString("isConnected", isConnected); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorIsConnected", payload); + } + + @ReactMethod + public void isStarting() { + String isStarting = String.valueOf(torKmpManager.isStarting()); + WritableMap payload = Arguments.createMap(); + payload.putString("isStarting", isStarting); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorIsStarting", payload); + } + + @ReactMethod + public void stop() { + torKmpManager.getTorOperationManager().stopQuietly(); + WritableMap payload = Arguments.createMap(); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorStop", payload); + } + + @ReactMethod + public void start() { + torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication()); + torKmpManager.getTorOperationManager().startQuietly(); + WritableMap payload = Arguments.createMap(); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorStart", payload); + } + + @ReactMethod + public void restart() { + torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication()); + torKmpManager.getTorOperationManager().restartQuietly(); + WritableMap payload = Arguments.createMap(); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorRestart", payload); + } + + @ReactMethod + public void newIdentity() { + torKmpManager.newIdentity(context.getCurrentActivity().getApplication()); + WritableMap payload = Arguments.createMap(); + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("TorNewIdentity", payload); + } +} diff --git a/mobile/android/app/src/main/java/com/robosats/tor/EnumTorState.kt b/mobile/android/app/src/main/java/com/robosats/tor/EnumTorState.kt new file mode 100644 index 000000000..8ddc7da58 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/tor/EnumTorState.kt @@ -0,0 +1,8 @@ +package com.robosats.tor + +enum class EnumTorState { + STARTING, + ON, + STOPPING, + OFF +} diff --git a/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt b/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt new file mode 100644 index 000000000..bed7fbc52 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt @@ -0,0 +1,389 @@ +package com.robosats.tor + +import android.app.Application +import android.util.Log +import android.widget.Toast +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid +import io.matthewnelson.kmp.tor.TorConfigProviderAndroid +import io.matthewnelson.kmp.tor.common.address.* +import io.matthewnelson.kmp.tor.controller.common.config.TorConfig +import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.* +import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.* +import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet +import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal +import io.matthewnelson.kmp.tor.controller.common.events.TorEvent +import io.matthewnelson.kmp.tor.manager.TorManager +import io.matthewnelson.kmp.tor.manager.TorServiceConfig +import io.matthewnelson.kmp.tor.manager.common.TorControlManager +import io.matthewnelson.kmp.tor.manager.common.TorOperationManager +import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent +import io.matthewnelson.kmp.tor.manager.common.state.isOff +import io.matthewnelson.kmp.tor.manager.common.state.isOn +import io.matthewnelson.kmp.tor.manager.common.state.isStarting +import io.matthewnelson.kmp.tor.manager.common.state.isStopping +import io.matthewnelson.kmp.tor.manager.R +import kotlinx.coroutines.* +import java.net.InetSocketAddress +import java.net.Proxy + +class TorKmpManager(application : Application) { + + private val TAG = "TorListener" + + private val providerAndroid by lazy { + object : TorConfigProviderAndroid(context = application) { + override fun provide(): TorConfig { + return TorConfig.Builder { + // Set multiple ports for all of the things + val dns = Ports.Dns() + put(dns.set(AorDorPort.Value(PortProxy(9252)))) + put(dns.set(AorDorPort.Value(PortProxy(9253)))) + + val socks = Ports.Socks() + put(socks.set(AorDorPort.Value(PortProxy(9254)))) + put(socks.set(AorDorPort.Value(PortProxy(9255)))) + + val http = Ports.HttpTunnel() + put(http.set(AorDorPort.Value(PortProxy(9258)))) + put(http.set(AorDorPort.Value(PortProxy(9259)))) + + val trans = Ports.Trans() + put(trans.set(AorDorPort.Value(PortProxy(9262)))) + put(trans.set(AorDorPort.Value(PortProxy(9263)))) + + // If a port (9263) is already taken (by ^^^^ trans port above) + // this will take its place and "overwrite" the trans port entry + // because port 9263 is taken. + put(socks.set(AorDorPort.Value(PortProxy(9263)))) + + // Set Flags + socks.setFlags(setOf( + Ports.Socks.Flag.OnionTrafficOnly + )).setIsolationFlags(setOf( + Ports.IsolationFlag.IsolateClientAddr, + )).set(AorDorPort.Value(PortProxy(9264))) + put(socks) + + // reset our socks object to defaults + socks.setDefault() + + // Not necessary, as if ControlPort is missing it will be + // automatically added for you; but for demonstration purposes... +// put(Ports.Control().set(AorDorPort.Auto)) + + // Use a UnixSocket instead of TCP for the ControlPort. + // + // A unix domain socket will always be preferred on Android + // if neither Ports.Control or UnixSockets.Control are provided. + put(UnixSockets.Control().set(FileSystemFile( + workDir.builder { + + // Put the file in the "data" directory + // so that we avoid any directory permission + // issues. + // + // Note that DataDirectory is automatically added + // for you if it is not present in your provided + // config. If you set a custom Path for it, you + // should use it here. + addSegment(DataDirectory.DEFAULT_NAME) + + addSegment(UnixSockets.Control.DEFAULT_NAME) + } + ))) + + // Use a UnixSocket instead of TCP for the SocksPort. + put(UnixSockets.Socks().set(FileSystemFile( + workDir.builder { + + // Put the file in the "data" directory + // so that we avoid any directory permission + // issues. + // + // Note that DataDirectory is automatically added + // for you if it is not present in your provided + // config. If you set a custom Path for it, you + // should use it here. + addSegment(DataDirectory.DEFAULT_NAME) + + addSegment(UnixSockets.Socks.DEFAULT_NAME) + } + ))) + + // For Android, disabling & reducing connection padding is + // advisable to minimize mobile data usage. + put(ConnectionPadding().set(AorTorF.False)) + put(ConnectionPaddingReduced().set(TorF.True)) + + // Tor default is 24h. Reducing to 10 min helps mitigate + // unnecessary mobile data usage. + put(DormantClientTimeout().set(Time.Minutes(10))) + + // Tor defaults this setting to false which would mean if + // Tor goes dormant, the next time it is started it will still + // be in the dormant state and will not bootstrap until being + // set to "active". This ensures that if it is a fresh start, + // dormancy will be cancelled automatically. + put(DormantCanceledByStartup().set(TorF.True)) + + // If planning to use v3 Client Authentication in a persistent + // manner (where private keys are saved to disk via the "Persist" + // flag), this is needed to be set. + put(ClientOnionAuthDir().set(FileSystemDir( + workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) } + ))) + + val hsPath = workDir.builder { + addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME) + addSegment("test_service") + } + // Add Hidden services + put(HiddenService() + .setPorts(ports = setOf( + // Use a unix domain socket to communicate via IPC instead of over TCP + HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder { + addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME) + }), + )) + .setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2)) + .setMaxStreamsCloseCircuit(value = TorF.True) + .set(FileSystemDir(path = hsPath)) + ) + + put(HiddenService() + .setPorts(ports = setOf( + HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http + HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https + )) + .set(FileSystemDir(path = + workDir.builder { + addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME) + addSegment("test_service_2") + } + )) + ) + }.build() + } + } + } + + private val loaderAndroid by lazy { + KmpTorLoaderAndroid(provider = providerAndroid) + } + + private val manager: TorManager by lazy { + TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null) + } + + // only expose necessary interfaces + val torOperationManager: TorOperationManager get() = manager + val torControlManager: TorControlManager get() = manager + + private val listener = TorListener() + + val events: LiveData get() = listener.eventLines + + private val appScope by lazy { + CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + } + + val torStateLiveData: MutableLiveData = MutableLiveData() + get() = field + var torState: TorState = TorState() + get() = field + + var proxy: Proxy? = null + get() = field + + init { + manager.debug(true) + manager.addListener(listener) + listener.addLine(TorServiceConfig.getMetaData(application).toString()) + } + + fun isConnected(): Boolean { + return manager.state.isOn() && manager.state.bootstrap >= 100 + } + + fun isStarting(): Boolean { + return manager.state.isStarting() || + (manager.state.isOn() && manager.state.bootstrap < 100); + } + + + fun newIdentity(appContext: Application) { + appScope.launch { + val result = manager.signal(TorControlSignal.Signal.NewNym) + result.onSuccess { + if (it !is String) { + listener.addLine(TorControlSignal.NEW_NYM_SUCCESS) + Toast.makeText(appContext, TorControlSignal.NEW_NYM_SUCCESS, Toast.LENGTH_SHORT).show() + return@onSuccess + } + + val post: String? = when { + it.startsWith(TorControlSignal.NEW_NYM_RATE_LIMITED) -> { + // Rate limiting NEWNYM request: delaying by 8 second(s) + val seconds: Int? = it.drop(TorControlSignal.NEW_NYM_RATE_LIMITED.length) + .substringBefore(' ') + .toIntOrNull() + + if (seconds == null) { + it + } else { + appContext.getString( + R.string.kmp_tor_newnym_rate_limited, + seconds + ) + } + } + it == TorControlSignal.NEW_NYM_SUCCESS -> { + appContext.getString(R.string.kmp_tor_newnym_success) + } + else -> { + null + } + } + + if (post != null) { + listener.addLine(post) + Toast.makeText(appContext, post, Toast.LENGTH_SHORT).show() + } + } + result.onFailure { + val msg = "Tor identity change failed" + listener.addLine(msg) + Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show() + } + } + } + + + private inner class TorListener: TorManagerEvent.Listener() { + private val _eventLines: MutableLiveData = MutableLiveData("") + val eventLines: LiveData = _eventLines + private val events: MutableList = ArrayList(50) + fun addLine(line: String) { + synchronized(this) { + if (events.size > 49) { + events.removeAt(0) + } + events.add(line) + //Log.i(TAG, line) + //_eventLines.value = events.joinToString("\n") + _eventLines.postValue(events.joinToString("\n")) + } + } + + override fun onEvent(event: TorManagerEvent) { + + if (event is TorManagerEvent.State) { + val stateEvent: TorManagerEvent.State = event + val state = stateEvent.torState + torState.progressIndicator = state.bootstrap + val liveTorState = TorState() + liveTorState.progressIndicator = state.bootstrap + + if (state.isOn()) { + if (state.bootstrap >= 100) { + torState.state = EnumTorState.ON + liveTorState.state = EnumTorState.ON + } else { + torState.state = EnumTorState.STARTING + liveTorState.state = EnumTorState.STARTING + } + } else if (state.isStarting()) { + torState.state = EnumTorState.STARTING + liveTorState.state = EnumTorState.STARTING + } else if (state.isOff()) { + torState.state = EnumTorState.OFF + liveTorState.state = EnumTorState.OFF + } else if (state.isStopping()) { + torState.state = EnumTorState.STOPPING + liveTorState.state = EnumTorState.STOPPING + } + torStateLiveData.postValue(liveTorState) + } + addLine(event.toString()) + super.onEvent(event) + } + + override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) { + addLine("$event - $output") + + super.onEvent(event, output) + } + + override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List) { + addLine("multi-line event: $event. See Logs.") + + // these events are many many many lines and should be moved + // off the main thread if ever needed to be dealt with. + val enabled = false + if (enabled) { + appScope.launch(Dispatchers.IO) { + Log.d(TAG, "-------------- multi-line event START: $event --------------") + for (line in output) { + Log.d(TAG, line) + } + Log.d(TAG, "--------------- multi-line event END: $event ---------------") + } + } + + super.onEvent(event, output) + } + + override fun managerEventError(t: Throwable) { + t.printStackTrace() + } + + override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) { + if (info.isNull) { + // Tear down HttpClient + } else { + info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress -> + @Suppress("UNUSED_VARIABLE") + val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value) + proxy = Proxy(Proxy.Type.SOCKS, socket) + } + } + } + + override fun managerEventStartUpCompleteForTorInstance() { + // Do one-time things after we're bootstrapped + + appScope.launch { + torControlManager.onionAddNew( + type = OnionAddress.PrivateKey.Type.ED25519_V3, + hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))), + flags = null, + maxStreams = null, + ).onSuccess { hsEntry -> + addLine( + "New HiddenService: " + + "\n - Address: https://${hsEntry.address.canonicalHostname()}" + + "\n - PrivateKey: ${hsEntry.privateKey}" + ) + + torControlManager.onionDel(hsEntry.address).onSuccess { + addLine("Aaaaaaaaand it's gone...") + }.onFailure { t -> + t.printStackTrace() + } + }.onFailure { t -> + t.printStackTrace() + } + + delay(20_000L) + + torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime -> + addLine("Uptime - $uptime") + }.onFailure { t -> + t.printStackTrace() + } + } + } + } +} diff --git a/mobile/android/app/src/main/java/com/robosats/tor/TorState.kt b/mobile/android/app/src/main/java/com/robosats/tor/TorState.kt new file mode 100644 index 000000000..510525717 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/tor/TorState.kt @@ -0,0 +1,14 @@ +package com.robosats.tor + +class TorState { + var state : EnumTorState = EnumTorState.OFF + get() = field + set(value) { + field = value + } + var progressIndicator : Int = 0 + get() = field + set(value) { + field = value + } +} diff --git a/mobile/android/app/src/main/jniLibs/arm64-v8a/librobohash.so b/mobile/android/app/src/main/jniLibs/arm64-v8a/librobohash.so new file mode 100755 index 000000000..8bc019992 Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/arm64-v8a/librobohash.so differ diff --git a/mobile/android/app/src/main/jniLibs/arm64-v8a/librobonames.so b/mobile/android/app/src/main/jniLibs/arm64-v8a/librobonames.so new file mode 100755 index 000000000..3e13c6d88 Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/arm64-v8a/librobonames.so differ diff --git a/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobohash.so b/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobohash.so new file mode 100755 index 000000000..3775d2f03 Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobohash.so differ diff --git a/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobonames.so b/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobonames.so new file mode 100755 index 000000000..2cf9568ef Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/armeabi-v7a/librobonames.so differ diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 322d35628..19e525176 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -9,7 +9,6 @@ buildscript { compileSdkVersion = 33 targetSdkVersion = 33 kotlin_version = "1.8.21" - kotlinVersion = "1.8.21" //for react-native-tor if (System.properties['os.arch'] == "aarch64") { // For M1 Users we need to use the NDK 24 which added support for aarch64 diff --git a/mobile/native/RoboIdentitiesModule.ts b/mobile/native/RoboIdentitiesModule.ts new file mode 100644 index 000000000..a5c3c83ea --- /dev/null +++ b/mobile/native/RoboIdentitiesModule.ts @@ -0,0 +1,9 @@ +import { NativeModules } from 'react-native'; +const { RoboIdentitiesModule } = NativeModules; + +interface RoboIdentitiesModuleInterface { + generateRoboname: (initialString: String) => Promise; + generateRobohash: (initialString: String) => Promise; +} + +export default RoboIdentitiesModule as RoboIdentitiesModuleInterface; diff --git a/mobile/native/TorModule.ts b/mobile/native/TorModule.ts new file mode 100644 index 000000000..d071aeee8 --- /dev/null +++ b/mobile/native/TorModule.ts @@ -0,0 +1,11 @@ +import { NativeModules } from 'react-native'; +const { TorModule } = NativeModules; + +interface TorModuleInterface { + start: () => void; + restart: () => void; + getTorStatus: () => void; + sendRequest: (action: string, url: string, headers: string, body: string) => Promise; +} + +export default TorModule as TorModuleInterface; diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 4253c61cc..1ee9e8853 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -13,7 +13,6 @@ "react": "18.2.0", "react-native": "^0.71.8", "react-native-encrypted-storage": "^4.0.3", - "react-native-tor": "^0.1.8", "react-native-webview": "^13.3.0" }, "devDependencies": { @@ -4330,10 +4329,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@types/async": { - "version": "3.2.20", - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", @@ -12972,18 +12967,6 @@ "version": "0.71.18", "license": "MIT" }, - "node_modules/react-native-tor": { - "version": "0.1.8", - "license": "MIT", - "dependencies": { - "@types/async": "^3.2.6", - "async": "^3.2.0" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-webview": { "version": "13.3.0", "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.3.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index d494ad289..ca8861d37 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -17,7 +17,6 @@ "react": "18.2.0", "react-native": "^0.71.8", "react-native-encrypted-storage": "^4.0.3", - "react-native-tor": "^0.1.8", "react-native-webview": "^13.3.0" }, "devDependencies": { diff --git a/mobile/services/Tor/index.ts b/mobile/services/Tor/index.ts index 35b4f2c43..15e22d0a9 100644 --- a/mobile/services/Tor/index.ts +++ b/mobile/services/Tor/index.ts @@ -1,29 +1,6 @@ -import Tor from 'react-native-tor'; +import TorModule from '../../native/TorModule'; class TorClient { - daemon: ReturnType; - - constructor() { - this.daemon = Tor({ - stopDaemonOnBackground: false, - numberConcurrentRequests: 0, - }); - } - - private readonly connectDaemon: () => void = async () => { - try { - this.daemon.startIfNotStarted(); - } catch { - console.log('TOR already started'); - } - }; - - public reset: () => void = async () => { - console.log('Reset TOR'); - await this.daemon.stopIfRunning(); - await this.daemon.startIfNotStarted(); - }; - public get: (baseUrl: string, path: string, headers: object) => Promise = async ( baseUrl, path, @@ -31,9 +8,13 @@ class TorClient { ) => { return await new Promise(async (resolve, reject) => { try { - const response = await this.daemon.get(`${baseUrl}${path}`, headers); - - resolve(response); + const response = await TorModule.sendRequest( + 'GET', + `${baseUrl}${path}`, + JSON.stringify(headers), + '{}', + ); + resolve(JSON.parse(response)); } catch (error) { reject(error); } @@ -47,28 +28,13 @@ class TorClient { ) => { return await new Promise(async (resolve, reject) => { try { - const response = await this.daemon.delete(`${baseUrl}${path}`, '', headers); - - resolve(response); - } catch (error) { - reject(error); - } - }); - }; - - public request: (baseUrl: string, path: string) => Promise = async ( - baseUrl: string, - path, - ) => { - return await new Promise(async (resolve, reject) => { - try { - const response = await this.daemon - .request(`${baseUrl}${path}`, 'GET', '', {}, true) - .then((resp) => { - resolve(resp); - }); - - resolve(response); + const response = await TorModule.sendRequest( + 'DELETE', + `${baseUrl}${path}`, + JSON.stringify(headers), + '{}', + ); + resolve(JSON.parse(response)); } catch (error) { reject(error); } @@ -80,9 +46,13 @@ class TorClient { return await new Promise(async (resolve, reject) => { try { const json = JSON.stringify(body); - const response = await this.daemon.post(`${baseUrl}${path}`, json, headers); - - resolve(response); + const response = await TorModule.sendRequest( + 'POST', + `${baseUrl}${path}`, + JSON.stringify(headers), + json, + ); + resolve(JSON.parse(response)); } catch (error) { reject(error); }