From d0d91b0caf8cd0e495572d95557ba83758cf4a9e Mon Sep 17 00:00:00 2001 From: Arvid Lunnemark Date: Fri, 15 Jul 2022 12:57:28 -0400 Subject: [PATCH] Fix bugs! (#49) * make things work! * version 013 * do not install CLI on startup * add cli instructions * fix main * fix readme * remove dead code --- README.md | 14 +++ daemon/crypto/constants.hpp | 2 +- gui/release/app/package-lock.json | 4 +- gui/release/app/package.json | 2 +- gui/src/{preload.d.ts => global.d.ts} | 8 +- gui/src/main/constants.ts | 2 +- gui/src/main/main.ts | 88 +++++++++++-------- gui/src/main/preload.ts | 17 ++-- gui/src/renderer/Main.tsx | 53 ++++++++++- .../components/AddFriend/AddFriend.tsx | 4 +- .../AddFriend/AddFriendInPerson.tsx | 12 ++- gui/src/renderer/components/Compose/Write.tsx | 6 +- gui/src/renderer/components/MessageList.tsx | 8 +- 13 files changed, 147 insertions(+), 73 deletions(-) rename gui/src/{preload.d.ts => global.d.ts} (70%) diff --git a/README.md b/README.md index 7559ff1f..dee26e46 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,17 @@ For the gui, run: ```bash ./wrap2.sh ./gui/start.sh ``` + +## Installing the CLI + +Run: + +```bash +sudo mkdir -p /usr/local/bin +sudo ln -sf /Applications/Anysphere.app/Contents/Resources/bin/anysphere /usr/local/bin/anysphere +cat << EOF >> ~/.zprofile +export PATH="\$PATH:/usr/local/bin" +EOF +``` + +Replace `.zprofile` with `.bash_profile` if you use bash instead. diff --git a/daemon/crypto/constants.hpp b/daemon/crypto/constants.hpp index f985e90d..43911178 100644 --- a/daemon/crypto/constants.hpp +++ b/daemon/crypto/constants.hpp @@ -59,7 +59,7 @@ constexpr auto DEFAULT_ROUND_DELAY_SECONDS = 60; constexpr auto DEFAULT_SERVER_ADDRESS = "server1.anysphere.co:443"; // this commit hash will be automatically updated by gui/package.json. -constexpr auto RELEASE_COMMIT_HASH = "276f09d7735dac0ce030cbebcff57863715db831"; +constexpr auto RELEASE_COMMIT_HASH = "92a11656e89a471e4380afd45237a2b757edb44f"; // this is the number of friends that will be received from in each round // (ideally, they can all be received in a single PIR request using batch PIR) diff --git a/gui/release/app/package-lock.json b/gui/release/app/package-lock.json index 9e626948..559c372b 100644 --- a/gui/release/app/package-lock.json +++ b/gui/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "anysphere", - "version": "0.1.1", + "version": "0.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "anysphere", - "version": "0.1.1", + "version": "0.1.3", "hasInstallScript": true } } diff --git a/gui/release/app/package.json b/gui/release/app/package.json index 956a5e06..25c6d49f 100644 --- a/gui/release/app/package.json +++ b/gui/release/app/package.json @@ -1,6 +1,6 @@ { "name": "anysphere", - "version": "0.1.1", + "version": "0.1.3", "author": "Anysphere, Inc.", "description": "The world's first completely private messenger.", "main": "./dist/main/main.js", diff --git a/gui/src/preload.d.ts b/gui/src/global.d.ts similarity index 70% rename from gui/src/preload.d.ts rename to gui/src/global.d.ts index 1a18b3da..538e349f 100644 --- a/gui/src/preload.d.ts +++ b/gui/src/global.d.ts @@ -6,12 +6,12 @@ import { Daemon } from "./main/daemon"; declare global { - interface Window { + // eslint-disable-next-line no-var + var logger: Console; + + interface Window extends Daemon { copyToClipboard(s: string): void; isPlatformMac(): boolean; - - daemon: Daemon; } } - export {}; diff --git a/gui/src/main/constants.ts b/gui/src/main/constants.ts index b5593ab3..18b638f3 100644 --- a/gui/src/main/constants.ts +++ b/gui/src/main/constants.ts @@ -3,7 +3,7 @@ import { exit } from "process"; import path from "path"; // this commit hash will be automatically updated by gui/package.json. -export const RELEASE_COMMIT_HASH = "276f09d7735dac0ce030cbebcff57863715db831"; +export const RELEASE_COMMIT_HASH = "92a11656e89a471e4380afd45237a2b757edb44f"; export const PLIST_PATH = () => { if (process.platform === "darwin" && process.env.HOME) { diff --git a/gui/src/main/main.ts b/gui/src/main/main.ts index 8168b3ff..648b5d3f 100644 --- a/gui/src/main/main.ts +++ b/gui/src/main/main.ts @@ -18,6 +18,7 @@ import * as daemon_pb from "../daemon/schema/daemon_pb"; import { PLIST_CONTENTS, PLIST_PATH, RELEASE_COMMIT_HASH } from "./constants"; import { exit } from "process"; import fs from "fs"; +import { Console } from "console"; const daemon = new DaemonImpl(); @@ -27,7 +28,32 @@ const isDevelopment = if (process.env["NODE_ENV"] === "production") { const sourceMapSupport = require("source-map-support"); - sourcemapsupport.install(); + sourceMapSupport.install(); +} + +function setupLogger(): void { + let logPath = ""; + if (process.env["XDG_CACHE_HOME"] !== undefined) { + logPath = path.join(process.env["XDG_CACHE_HOME"], "anysphere", "logs"); + } else if (process.env["HOME"] !== undefined) { + logPath = path.join(process.env["HOME"], ".anysphere", "cache", "logs"); + } else { + process.stderr.write( + "$HOME or $XDG_CACHE_HOME not set! Cannot create log path, aborting :(" + ); + exit(1); + } + app.setAppLogsPath(logPath); + + // in debug mode, use console.log. in production, use the log file. + if (isDevelopment) { + global.logger = console; + } else { + global.logger = new Console({ + stdout: fs.createWriteStream(path.join(logPath, "anysphere-gui.log")), + stderr: fs.createWriteStream(path.join(logPath, "anysphere-gui.err")), + }); + } } async function startDaemonIfNeeded(pkgPath: string): Promise { @@ -38,27 +64,19 @@ async function startDaemonIfNeeded(pkgPath: string): Promise { throw new Error("incorrect release hash"); } // daemon is running, correct version, nothing to do + logger.log("Daemon is running, with the correct version!"); return; } catch (e) { // if development, we don't want to start the daemon (want to do it manually) if (isDevelopment) { - process.stdout.write( + logger.log( `Daemon is either not running or running the wrong version. Please start it; we're not doing anything because we're in DEV mode. Error: ${e}.` ); return; - } - - // first copy the CLI - const cliPath = path.join(pkgPath, "bin", "anysphere"); - // ln -sf link it! - // TODO(arvid): just add to PATH instead, because not everyone has /usr/local/bin in their PATH - const mkdir = await exec(`mkdir -p /usr/local/bin`); - if (mkdir.stderr) { - process.stderr.write(mkdir.stderr); - } - const clilink = await exec(`ln -sf ${cliPath} /usr/local/bin/anysphere`); - if (clilink.stderr) { - process.stderr.write(clilink.stderr); + } else { + logger.log( + `Daemon is either not running or running the wrong version. Error: ${e}.` + ); } // TODO(arvid): handle windows and linux too @@ -68,36 +86,30 @@ async function startDaemonIfNeeded(pkgPath: string): Promise { // 0: create the directory const mkdirPlist = await exec(`mkdir -p ${path.dirname(plistPath)}`); if (mkdirPlist.stderr) { - process.stderr.write(mkdirPlist.stderr); + logger.error(mkdirPlist.stderr); } + logger.log("Successfully created the plist directory."); // 1: unload plist await exec("launchctl unload " + plistPath); // we don't care if it fails or not! - let logPath = ""; - if (process.env["XDG_CACHE_HOME"] !== undefined) { - logPath = path.join(process.env["XDG_CACHE_HOME"], "anysphere", "logs"); - } else if (process.env["HOME"] !== undefined) { - logPath = path.join(process.env["HOME"], ".anysphere", "cache", "logs"); - } else { - process.stderr.write( - "$HOME or $XDG_CACHE_HOME not set! Cannot create daemon, aborting :(" - ); - exit(1); - } + const logPath = app.getPath("logs"); const contents = PLIST_CONTENTS(pkgPath, logPath); + logger.log("Successfully unloaded the plist."); // 2: write plist await fs.promises.writeFile(plistPath, contents); + logger.log("Successfully wrote the new plist."); // 3: load plist const response = await exec("launchctl load " + plistPath); if (response.stderr) { - process.stderr.write(response.stderr); + logger.error(response.stderr); exit(1); } + logger.log("Successfully loaded the new plist."); } } -const installExtensions = async () => { +async function installExtensions(): Promise { const installer = require("electron-devtools-installer"); - const forceDownload = !!process.env.UPGRADE_EXTENSIONS; + const forceDownload = !!process.env["UPGRADE_EXTENSIONS"]; const extensions = ["REACT_DEVELOPER_TOOLS"]; return installer @@ -105,8 +117,8 @@ const installExtensions = async () => { extensions.map((name) => installer[name]), forceDownload ) - .catch(console.log); -}; + .catch(logger.log); +} const createWindow = async () => { if (isDevelopment) { @@ -140,9 +152,6 @@ const createWindow = async () => { mainWindow.loadURL(resolveHtmlPath("index.html")); mainWindow.on("ready-to-show", () => { - if (!mainWindow) { - throw new Error('"mainWindow" is not defined'); - } if (process.env["START_MINIMIZED"] === "true") { mainWindow.minimize(); } else { @@ -155,12 +164,17 @@ const createWindow = async () => { // Don't allow ANY requests to any origin! This means that the app will // only not be able to communicate with the internet at all, which is PERFECT. + session.defaultSession.enableNetworkEmulation({ + offline: true, + }); session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, + // eslint-disable-next-line @typescript-eslint/naming-convention "Content-Security-Policy": [ - "default-src 'self' style-src 'self' 'unsafe-inline'", + "default-src 'self'; style-src 'self' 'unsafe-inline'", // required for tailwind + // "default-src 'self'", ], }, }); @@ -207,6 +221,8 @@ function registerForNotifications(): () => void { app .whenReady() .then(() => { + setupLogger(); + autoUpdater.checkForUpdatesAndNotify(); startDaemonIfNeeded(path.dirname(app.getAppPath())); diff --git a/gui/src/main/preload.ts b/gui/src/main/preload.ts index c8e3aa53..088ad0ab 100644 --- a/gui/src/main/preload.ts +++ b/gui/src/main/preload.ts @@ -15,12 +15,11 @@ contextBridge.exposeInMainWorld("isPlatformMac", () => { }); const daemonI = new DaemonImpl(); -const classToObject = (theClass) => { - const originalClass = theClass || {}; - const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(originalClass)); - return keys.reduce((classAsObj, key) => { - classAsObj[key] = originalClass[key]; - return classAsObj; - }, {}); -}; -contextBridge.exposeInMainWorld("daemon", classToObject(daemonI)); +const methods = Object.getOwnPropertyNames( + Object.getPrototypeOf(daemonI) +).filter((x) => x !== "constructor"); +for (const method of methods) { + contextBridge.exposeInMainWorld(method, (...args) => { + return daemonI[method](...args); + }); +} diff --git a/gui/src/renderer/Main.tsx b/gui/src/renderer/Main.tsx index a20e63d8..d5d9eb18 100644 --- a/gui/src/renderer/Main.tsx +++ b/gui/src/renderer/Main.tsx @@ -25,6 +25,7 @@ import { StatusHandler, StatusContext } from "./components/Status"; import { SideBar } from "./components/SideBar/SideBar"; import { SideBarButton } from "./components/SideBar/SideBarProps"; import AddFriend from "./components/AddFriend/AddFriend"; +import Modal from "./components/Modal"; const defaultTabs: Tab[] = [ { type: TabType.New, name: "New", data: null, unclosable: true, id: "new" }, @@ -62,7 +63,7 @@ function Main(): JSX.Element { return; } } - window.daemon.messageSeen({ id: message.uid }).catch(console.error); + window.messageSeen({ id: message.uid }).catch(console.error); let title = ""; if ("fromDisplayName" in message) { title = `${truncate(message.message, 10)} - ${message.fromDisplayName}`; @@ -194,7 +195,7 @@ function Main(): JSX.Element { } React.useEffect(() => { - window.daemon + window .hasRegistered() .then((registered: boolean) => { if (!registered) { @@ -202,7 +203,7 @@ function Main(): JSX.Element { {}} // should not be able to close modal by clicking outside onRegister={(username: string, key: string) => { - window.daemon + window .registerUser({ name: username, betaKey: key }) .then(() => { closeModal(); @@ -336,6 +337,52 @@ function Main(): JSX.Element { shortcut: ["h"], keywords: "help", }, + { + id: "install-cli", + name: "Install 'anysphere' command", + keywords: "install, cli, command", + perform: () => { + setModal( + { + closeModal(); + }} + > +
+

+ Install 'anysphere' command +

+
+
+

+ Run the following sequence of commands in your terminal: +

+
+ + {`sudo mkdir -p /usr/local/bin +sudo ln -sf /Applications/Anysphere.app/Contents/Resources/bin/anysphere /usr/local/bin/anysphere +cat << EOF >> ~/.zprofile +export PATH="\$PATH:/usr/local/bin" +EOF`} + +
+
+ Replace `.zprofile` with `.bash_profile` or something else + if you use a different shell. +
+
+ Why do we ask you to run this yourself? Administrator + privileges are needed to install the command, and we don't + want to ask you for your password without you seeing exactly + what is being run. +
+
+
+
+
+ ); + }, + }, // { // id: "quit", // name: "Quit", diff --git a/gui/src/renderer/components/AddFriend/AddFriend.tsx b/gui/src/renderer/components/AddFriend/AddFriend.tsx index 24fcd33c..8cf8242e 100644 --- a/gui/src/renderer/components/AddFriend/AddFriend.tsx +++ b/gui/src/renderer/components/AddFriend/AddFriend.tsx @@ -33,7 +33,7 @@ export default function AddFriend({ onClose={onClose} setStatus={setStatus} chooseInperson={() => { - window.daemon + window .getMyPublicID() .then((publicID) => { setStory(publicID.story); @@ -48,7 +48,7 @@ export default function AddFriend({ }); }} chooseRemote={() => { - window.daemon + window .getMyPublicID() .then((publicID) => { setPublicID(publicID.publicId); diff --git a/gui/src/renderer/components/AddFriend/AddFriendInPerson.tsx b/gui/src/renderer/components/AddFriend/AddFriendInPerson.tsx index 7e71ffc6..cb01d598 100644 --- a/gui/src/renderer/components/AddFriend/AddFriendInPerson.tsx +++ b/gui/src/renderer/components/AddFriend/AddFriendInPerson.tsx @@ -5,7 +5,6 @@ import seedrandom from "seedrandom"; import { motion } from "framer-motion"; import spellCheck from "./SpellCheck"; - const DEBUG_COLORS = false; // const DEBUG_COLORS = true; @@ -75,7 +74,7 @@ export function StoryForm({ focus:border-asbrown-300 focus:ring-0" onChange={(e) => { let sentence = e.target.value; - e.target.style.color = 'black'; + e.target.style.color = "black"; if (sentence.length > 0) { let words = sentence.split(" "); let correct = true; @@ -92,10 +91,9 @@ export function StoryForm({ } console.log(correct); if (!correct) { - e.target.style.color = 'red'; - } - else { - e.target.style.color = 'black'; + e.target.style.color = "red"; + } else { + e.target.style.color = "black"; } } setTheirStory( @@ -451,7 +449,7 @@ export default function AddFriendInPerson({ }); return; } - window.daemon + window .addSyncFriend({ uniqueName, displayName, diff --git a/gui/src/renderer/components/Compose/Write.tsx b/gui/src/renderer/components/Compose/Write.tsx index bb47c009..b5faf6f7 100644 --- a/gui/src/renderer/components/Compose/Write.tsx +++ b/gui/src/renderer/components/Compose/Write.tsx @@ -201,7 +201,7 @@ function Write({ // get both the complete friends and the sync invitations // the sync invitations have verified each other so it is safe to treat as a real friend // in the daemon.proto we keep them separate because we still want to display progress information - window.daemon + window .getFriendList() .then((friends: Friend[]) => { setFriends((f) => [ @@ -223,7 +223,7 @@ function Write({ // get both the complete friends and the sync invitations // the sync invitations have verified each other so it is safe to treat as a real friend // in the daemon.proto we keep them separate because we still want to display progress information - window.daemon + window .getOutgoingSyncInvitations() .then( ( @@ -285,7 +285,7 @@ function Write({ const displayNames = data.multiSelectState.friends.map( (friend) => friend.displayName ); - window.daemon + window .sendMessage({ uniqueNameList: uniqueNames, message: content, diff --git a/gui/src/renderer/components/MessageList.tsx b/gui/src/renderer/components/MessageList.tsx index aab5495d..b8e80943 100644 --- a/gui/src/renderer/components/MessageList.tsx +++ b/gui/src/renderer/components/MessageList.tsx @@ -95,7 +95,7 @@ export function IncomingMessageList({ React.useEffect(() => { if (type === "new") { setMessages([]); - const cancel = window.daemon.getMessagesStreamed( + const cancel = window.getMessagesStreamed( { filter: daemon_pb.GetMessagesRequest.Filter.NEW }, (messages: IncomingMessage[]) => { setMessages((prev: IncomingMessage[]) => { @@ -121,7 +121,7 @@ export function IncomingMessageList({ return cancel; } else if (type === "all") { setMessages([]); - const cancel = window.daemon.getMessagesStreamed( + const cancel = window.getMessagesStreamed( { filter: daemon_pb.GetMessagesRequest.Filter.ALL }, (messages: IncomingMessage[]) => { setMessages((prev: IncomingMessage[]) => { @@ -195,7 +195,7 @@ export function OutgoingMessageList({ React.useEffect(() => { if (type === "outbox") { - window.daemon + window .getOutboxMessages({}) .then(({ messagesList }: { messagesList: OutgoingMessage[] }) => { setMessages(messagesList); @@ -204,7 +204,7 @@ export function OutgoingMessageList({ console.error(err); }); } else if (type === "sent") { - window.daemon + window .getSentMessages({}) .then(({ messagesList }: { messagesList: OutgoingMessage[] }) => { setMessages(messagesList);