From 90b22aa6115c9c97ca8a179c79aa17541b481e93 Mon Sep 17 00:00:00 2001 From: deicidus <> Date: Fri, 10 Jun 2022 07:13:52 -0700 Subject: [PATCH] stuck on POST bug (server not receiving payload) --- .prettierignore | 15 + .prettierrc | 13 + README.md | 44 +- index.js | 144 +- package-lock.json | 24 + package.json | 6 +- scripts/api.js | 2143 +++++++++++-------------- scripts/bootstrap.js | 1 + scripts/features.js | 12 +- scripts/manual.js | 2 +- scripts/priority.js | 23 + scripts/session.js | 62 + scripts/settings.js | 2 +- scripts/{chalkStyles.js => styles.js} | 0 scripts/tests.js | 6 +- scripts/util.js | 2 + scripts/welcome.js | 20 +- 17 files changed, 1266 insertions(+), 1253 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 scripts/priority.js create mode 100644 scripts/session.js rename scripts/{chalkStyles.js => styles.js} (100%) diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9f399d7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules +/build +/dist +/production +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b904656 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + printWidth: 80, + useTabs: false, + tabWidth: 2, + semi: false, + singleQuote: true, + quoteProps: 'as-needed', + trailingComma: 'none', + bracketSpacing: true, + arrowParens: 'avoid', + htmlWhitespaceSensitivity: strict, + vueIndentScriptAndStyle: true, +} diff --git a/README.md b/README.md index 23ead7c..5321320 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ao-cli -`ao-cli` (alias `ao`) is a command-line interface (CLI) that helps you install, use, and configure the Autonomous Organization (AO). `ao-cli` is a Node/JavaScript CLI tool that wraps the functionality of Alchemy, AO administration, plus key AO features into one convenient interface. Command-line social networking. +`ao-cli` (alias `ao`) is a command-line interface (CLI) that helps you install, use, and configure the Autonomous Organization (AO). Command-line social networking for hackers. To run immediately: @@ -10,10 +10,46 @@ To install: `npm i -g @autonomousorganization/ao-cli` -Then you can run with `ao-cli`. (Inside the menu you will find an option to add 'ao' as a shortcut.) +Then you can run with `ao-cli`. (In the menus you will find an option to add `ao` as an alias.) -### Version History +## Features +These features work right now: + +* Browse the [AO User Manual](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual) and automatically download and keep it updated +* Manages your AO configuration file for you +* Wraps the functionality of (some of) Zen's Alchemy suite of scripts (system configuration, AO installation) +* `ao-cli` can self-update to the newest version +* Run AO unit tests to verify the up-to-spec functioning of the system's running AO API server + +## Upcoming Features + +These features are planned and many are mocked up in the menus: + +* Join the AO .onion bootstrapping network and find public AO chatrooms p2p over tor +* Operate essential AO client features (like creating and sending cards p2p via tor) +* Easily install and configure your AO server installation +* Easily use hardware-owner-only god-mode features for your AO server including resetting any password or deleting any member +* Easily monitor your AO server status and start/stop the service +* Easily switch between serving different AO frontends: `ao-svelte`, `ao-3` (Vue), or `ao-react` +* Easily install/uninstall and turn on/off option AO features +* Easily update all your remote AOs at once +* Easily install your preferred flavor of Unix on any unsecured Windows computer given its IP address (j/k) +* Full interactive wizard to walk you through setting up and connecting new AO hardware resources to your AO server + +## Important Locations + +* `~/.ao/` Your AO saved data folder +* `~/.ao/database.sqlite3` Location of your AO database (copy to back up) +* `~/ao-cli/` Typical location for `ao-cli` +* `~/ao-svelte/` Typical location for `ao-svelte` +* `~/ao-3/` Typical location for `ao-3` +* `~/.ao/ao-manual/` Typical location of the AO manual (Markdown files) +* `~/Alchemy/` Typical location of Zen's Alchemy + +## Version History + +* 0.0.8 Added self-update feature and --version/-v arg * 0.0.6 User manual downloads and updates automatically from [official ao-manual repo](https://git.coalitionofinvisiblecolleges.org/autonomousorganization/ao-manual) -* 0.0.5 Added browsable manual (must download ao-svelte) +* 0.0.5 Added browsable manual * 0.0.1 Menus prototyped diff --git a/index.js b/index.js index 6af36d8..5f08fbc 100644 --- a/index.js +++ b/index.js @@ -10,10 +10,13 @@ import { printManualPage, manualFolderAsMenu, AO_MANUAL_PATH } from './scripts/m import { isFolder, loadJsonFile } from './scripts/files.js' import { sleep } from './scripts/util.js' import { tests } from './scripts/tests.js' -import { headerStyle } from './scripts/chalkStyles.js' +import { headerStyle, greenChalk } from './scripts/styles.js' import './scripts/strings.js' import { installAoAlias, getAoCliVersion, selfUpdate, downloadManual, updateManual } from './scripts/features.js' import { startPublicBootstrap } from './scripts/bootstrap.js' +import { isLoggedIn, loginPrompt, logout } from './scripts/session.js' +import { AO_DEFAULT_HOSTNAME } from './scripts/api.js' +import { getTopPriorityText } from './scripts/priority.js' // These should become .env variables that are loaded intelligently let distro @@ -32,15 +35,13 @@ function exitIfRoot() { // Prints the AO Main Menu and executes the user's choice async function mainMenu() { console.log(`\n${headerStyle('AO Main Menu')}\n`) - const mainMenuChoices = [ - 'Chat', - 'Alchemy', - 'Deck', + let mainMenuChoices = [ + 'AO', + 'Features', 'Admin', - 'Tests', + 'Alchemy', 'Manual', - 'Log Out', - 'Exit', + 'Exit' ] const answer = await inquirer.prompt({ name: 'main_menu', @@ -50,20 +51,17 @@ async function mainMenu() { pageSize: mainMenuChoices.length }) switch(answer.main_menu) { - case 'Chat': - while(await chatMenu()) {} + case 'AO': + while(await useAoMenu()) {} break - case 'Alchemy': - while(await alchemyMenu()) {} - break - case 'Deck': - await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day']) + case 'Features': + while(await featuresMenu()) {} break case 'Admin': while(await adminMenu()) {} break - case 'Tests': - while(await testsMenu()) {} + case 'Alchemy': + while(await alchemyMenu()) {} break case 'Manual': if(!isFolder(AO_MANUAL_PATH)) { @@ -78,14 +76,11 @@ async function mainMenu() { updateManual() } await printManualPage(AO_MANUAL_PATH) // Fencepost case - print overview page - let previousChoice = 0 + let previousChoice do { previousChoice = await manualFolderAsMenu(AO_MANUAL_PATH, 'AO User Manual', 'Back to Main Menu', previousChoice + 1) } while(previousChoice !== false) break - case 'Log Out': - await spinnerWait('Logging out... (just kidding)') - break case 'Exit': farewell() await sleep(310) @@ -94,6 +89,50 @@ async function mainMenu() { return true } +// Prints the Use AO Menu and executes the user's choice +async function useAoMenu() { + const loggedIn = isLoggedIn() + console.log(`\n${headerStyle('AO')}\n`) + console.log('Top priority:', await getTopPriorityText()) + let aoMenuChoices = [] + if(loggedIn) { + aoMenuChoices.push( + 'Chat', + 'Deck', + ) + } + aoMenuChoices.push( + loggedIn ? 'Log Out' : 'Log In', + 'Back to Main Menu' + ) + const answer = await inquirer.prompt({ + name: 'ao_menu', + type: 'list', + message: 'Please choose:', + choices: aoMenuChoices, + pageSize: aoMenuChoices.length + }) + switch(answer.ao_menu) { + case 'Chat': + while(await chatMenu()) {} + break + case 'Deck': + await todoList('My Todo List', ['Add full AO install process to ao-cli in convenient format', 'Add AO server unit tests to ao-cli', 'Get groceries', 'Play music every day']) + break + case 'Log In': + console.log('\nao-cli will use the AO API to log into the AO server at', (aoEnv('AO_CLI_TARGET_HOSTNAME') || AO_DEFAULT_HOSTNAME) + '.') + await loginPrompt() + break + case 'Log Out': + await logout() + //await spinnerWait('Logging out...') + break + case 'Back to Main Menu': + return false + } + return true +} + // Prints a menu that allows you to join the global AO chatrooms let publicBootstrapStarted = false async function chatMenu() { @@ -159,6 +198,7 @@ async function chatMenu() { } // Prints the AO Admin Menu and executes the user's choice +// Maybe Alchemy menu should be installation and update, and admin menu should be more configuration & AO member admin async function adminMenu() { console.log(`\n${headerStyle('AO Admin Menu')}`) const adminChoices = [ @@ -166,12 +206,13 @@ async function adminMenu() { 'Update ao-cli', 'Check AO install', 'Update AO', - 'Configure AO features', + 'Switch AO target server', 'Switch AO database', 'Import/Export state/decks', 'Check AO service', 'Watch logs now', 'Start/Stop AO service', + 'Tests', 'Back to Main Menu' ] const answer = await inquirer.prompt({ @@ -190,9 +231,7 @@ async function adminMenu() { break case 'Check AO install': case 'Update AO': - case 'Configure AO features': - while(await featuresMenu()) {} - break + case 'Switch AO target server': case 'Switch AO database': case 'Import/Export state/decks': case 'Check AO service': @@ -200,6 +239,9 @@ async function adminMenu() { case 'Start/Stop AO service': console.log("Not yet implemented.") break + case 'Tests': + while(await testsMenu()) {} + break default: return false } @@ -262,21 +304,29 @@ async function alchemyMenu() { // Prints the Configure AO Features menu and executes the user's choice async function featuresMenu() { console.log(`\n${headerStyle('Configure AO Features')}`) + const status = { + off: ' ' + chalk.grey('Off') + ' ', + ins: chalk.yellow('Installed') + ' ', + ena: ' ' + greenChalk('Enabled') + ' ', + run: ' ' + greenChalk('Running') + ' ', + err: ' ' + chalk.red('Error') + ' ' + } + let widest = 9 const features = [ - 'nginx host AO publicly over the world wide web', - 'SSL/Certbot HTTPS for public web AO', - 'Tor connect AOs p2p', - 'Bitcoin payments', - 'Lightning payments', - 'Jitsi secure video chat', - 'Signal notifications', - 'File hosting file attachments on cards', - 'youtube-dl cache web videos', - 'Borg backup', - 'Encryption serverside secret messages', - 'Themes custom themes', - 'Glossary custom glossary', - 'Jubilee monthly points creation event', + `Tor ${status.run} connect AOs p2p`, + `Bitcoin ${status.run} payments`, + `Lightning ${status.ins} payments`, + `nginx ${status.ins} host AO publicly over the world wide web`, + `SSL/Certbot ${status.ins} HTTPS for public web AO`, + `Jitsi ${status.err} secure video chat`, + `Signal ${status.off} notifications`, + `File hosting ${status.err} file attachments on cards`, + `youtube-dl ${status.ins} cache web videos`, + `Borg ${status.ins} backup`, + `Encryption ${status.err} serverside secret messages`, + `Themes ${status.err} custom themes`, + `Glossary ${status.err} custom glossary`, + `Jubilee ${status.ena} monthly points creation event`, 'Back to Main Menu' ] const answer = await inquirer.prompt({ @@ -296,16 +346,6 @@ async function featuresMenu() { return true } -// Ask the user for their name and returns it -async function askName() { - const answer = await inquirer.prompt({ - name: 'member_name', - type: 'input', - message: 'What username would you like?' - }) - return answer.member_name -} - // Prints the given todoItems (array of strings) and allows items to be checked and unchecked async function todoList(title, todoItems) { console.log(`\n${headerStyle(title)}`) @@ -349,7 +389,11 @@ async function main() { exitIfRoot() // Loading screen, display some quick info during the fun animation - distro = detectOS() + distro = aoEnv('DISTRO') + if(!distro) { + distro = detectOS() + setAoEnv('DISTRO', distro) + } if(checkAoEnvFile()) { console.log('AO .env file exists at', AO_ENV_FILE_PATH) } else { diff --git a/package-lock.json b/package-lock.json index 277f610..5c0fd8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,9 @@ }, "bin": { "ao-cli": "index.js" + }, + "devDependencies": { + "prettier": "^2.6.2" } }, "node_modules/@babel/code-frame": { @@ -1500,6 +1503,21 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "node_modules/prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -3135,6 +3153,12 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "dev": true + }, "qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", diff --git a/package.json b/package.json index c5baffa..d2c0f57 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "type": "module", "scripts": { "start": "node .", - "version": "node . -v" + "version": "node . -v", + "prettier": "npx prettier" }, "keywords": [ "AO", @@ -41,5 +42,8 @@ }, "publishConfig": { "access": "public" + }, + "devDependencies": { + "prettier": "^2.6.2" } } diff --git a/scripts/api.js b/scripts/api.js index c6459e2..b80ea75 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -2,1304 +2,845 @@ import request from 'superagent' import { v1 as uuidV1 } from 'uuid' import { io } from 'socket.io-client' import { createHash, hmacHex } from './crypto.js' -//import { runInAction, reaction } from 'mobx' - -let currentMemberId -let currentSessionId -let currentSessionToken - -const HOST = '127.0.0.1' -const PORT = 8003 -const HOSTNAME = HOST + ':' + PORT +import { isObject } from './util.js' +import { aoEnv } from './settings.js' + +// The AO API server endpoint this ao-cli client will attempt to connect to +export const AO_DEFAULT_HOSTNAME = 'localhost:8003' +const HOSTNAME = aoEnv('AO_CLI_TARGET_HOSTNAME') || AO_DEFAULT_HOSTNAME +const [HOST, PORT] = HOSTNAME.split(':') + +// The AO API server websocket endpoint this ao-cli client will attempt to connect to +const AO_SOCKET_URL = + process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/' +export const socket = io(AO_SOCKET_URL, { + autoConnect: false +}) -export function setCurrentMemberId(newMemberId) { - currentMemberId = newMemberId -} +// Load the current session cookies from the AO .env file +let currentMemberId = aoEnv('AO_CLI_SESSION_USERNAME') +let currentSessionId = aoEnv('AO_CLI_SESSION_ID') +let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN') // Performs a post request to the specified endpoint, sending the given payload -function postRequest(endpoint, payload) { - if(!currentSessionToken) { - console.log("Session token not set, API not ready.") +export function postRequest(endpoint, payload) { + if (!currentSessionToken) { + console.log('Session token not set, API not ready.') return new Promise(() => null) } + try { let partialRequest = request .post(HOSTNAME + endpoint) .set('authorization', currentSessionToken) .set('session', currentSessionId) - + if(payload) { - return partialRequest.send(payload) + console.log('sending payload', payload) + return partialRequest.send(payload).then(res => { + //console.log("Response completed:", res) + }).catch(err => { + //console.log('response failed:', err) + }) } else { return partialRequest } + } catch (err) { + console.log('request failed') + } } // Performs a post request to the /event endpoint, sending the given JSON object as the event -function postEvent(event) { +export function postEvent(event) { return postRequest('/event', event) } -// Helper functions copied from crypto.js and utils.js -const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' - -// The AO API Object (split into separate functions if possible) -class AoApi { - constructor(socket) {} +// Attempts login with the given username and password combo. If successful, returns the generated session and token (login cookies). +export async function createSession(user, pass) { + const session = uuidV1() + let sessionKey = createHash(session + createHash(pass)) + const token = hmacHex(session, sessionKey) + const result = await request + .post(HOSTNAME + '/session') + .set('authorization', token) + .set('session', session) + .set('name', user) + .on('error', () => false) + currentMemberId = result.body.memberId + currentSessionToken = token + currentSessionId = session // Not used in this api.js yet + return { session, token, memberId: currentMemberId } +} - async createSession(user, pass) { - const session = uuidV1() - let sessionKey = createHash(session + createHash(pass)) - const token = hmacHex(session, sessionKey) - return request - .post(HOSTNAME + '/session') - .set('authorization', token) - .set('session', session) - .set('name', user) - .on('error', () => false) - .then(res => { - if(typeof window !== 'undefined') { - // clear any existing stale data from localstorage - window.localStorage.removeItem('user') - window.localStorage.removeItem('token') - window.localStorage.removeItem('session') - // set new session info - window.localStorage.setItem('user', user) - window.localStorage.setItem('token', token) - window.localStorage.setItem('session', session) - } else { - currentSessionToken = token - currentSessionId = session // Not used in this api.js yet - } - return true - }) - } +export async function logout() { + return await postRequest('/logout') +} - /*async fetchState() { - const session = window.localStorage.getItem('session') - const token = window.localStorage.getItem('token') - const user = window.localStorage.getItem('user') - if (session && token && user) { - return request - .post('/state') - .set('Authorization', token) - .then(res => { - aoStore.state.user = user - // console.log( - // 'AO: client/api.ts: fetchState: initial state: ', - // res.body - // ) - - let dataPackageToSendToClient = res.body - // Get the memberId - let memberId - dataPackageToSendToClient.stateToSend.sessions.forEach(sessionItem => { - if (session === sessionItem.session) { - memberId = sessionItem.ownerId - } - }) - - // See if we got our member card - let foundMemberCard - if(!memberId) { - console.log("memberId missing when loading state") - } else { - dataPackageToSendToClient.stateToSend.tasks.forEach(task => { - if(task.taskId === memberId) { - foundMemberCard = task - } - }) - } - if(foundMemberCard) { - console.log("State includes member card:", foundMemberCard) - } else { - console.log("State does not include member card") - } - - aoStore.initializeState(dataPackageToSendToClient.stateToSend) +// AO p2p over tor features +export async function nameAo(newName) { + return await postEvent({ + type: 'ao-named', + alias: newName + }) +} - let metaData = dataPackageToSendToClient.metaData - aoStore.memberDeckSize = metaData.memberDeckSize - aoStore.bookmarksTaskId = metaData.bookmarksTaskId +export async function connectToAo(address, secret) { + return await postEvent({ + type: 'ao-outbound-connected', + address: address, + secret: secret + }) +} - return true - }) - .catch(() => false) - } - return Promise.resolve(false) - }*/ +export async function deleteAoConnection(address) { + return await postEvent({ + type: 'ao-disconnected', + address: address + }) +} - async nameAo(newName) { - const act = { - type: 'ao-named', - alias: newName, - } - return postEvent(act).then(res => { - return res - }) - } +export async function relayEventToOtherAo(address, event) { + return await postEvent({ + type: 'ao-relay', + address: address, + ev: event + }) +} - async connectToAo( - address, - secret - ) { - const act = { - type: 'ao-outbound-connected', - address: address, - secret: secret, - } - return postEvent(act).then(res => { - return res - }) - } - - async deleteAoConnection( - address, - ) { - const act = { - type: 'ao-disconnected', - address: address, - } - return postEvent(act).then(res => { - return res - }) - } - - async linkCardOnAo( - taskId, - address - ) { - const act = { - type: 'ao-linked', - address: address, - taskIdId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function linkCardOnAo(taskId, address) { + return await postEvent({ + type: 'ao-linked', + address: address, + taskId: taskId + }) +} - async relayEventToOtherAo( - address, - event - ) { - const act = { - type: 'ao-relay', - address: address, - ev: event, - } - return postEvent(act).then(res => { - return res - }) - } +// Avatar and presence features +export async function bark() { + return await postEvent({ + type: 'doge-barked', + memberId: currentMemberId + }) +} - async setQuorum(quorum) { - const act = { - type: 'quorum-set', - quorum: quorum, - } - // console.log('act is ', act) - return postEvent(act).then(res => { - return res - }) - } +export async function hopped(taskId) { + return await postEvent({ + type: 'doge-hopped', + memberId: currentMemberId, + taskId: taskId + }) +} - async bark() { - const act = { - type: 'doge-barked', - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function mute() { + return await updateMemberField('muted', true) +} - async hopped(taskId) { - const act = { - type: 'doge-hopped', - memberId: currentMemberId, - taskIdId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function unmute() { + return await updateMemberField('muted', false) +} - async mute() { - const act = { - type: 'member-field-updated', - memberId: currentMemberId, - field: 'muted', - newfield: true, - } - return postEvent(act).then(res => { - return res - }) - } +// Memes feature +export async function fetchMeme(memeHash, progressCallback) { + return request + .get(HOSTNAME + '/meme/' + memeHash) + .responseType('blob') + .set('Authorization', currentSessionToken) + .on('progress', function (e) { + progressCallback(e.percent) + }) + .then(res => { + console.log('got meme! res is ', res) + return res.body + }) +} - async unmute() { - const act = { - type: 'member-field-updated', - memberId: currentMemberId, - field: 'muted', - newfield: false, - } - return postEvent(act).then(res => { - return res - }) - } +export async function downloadMeme(memeHash) { + return request + .get(HOSTNAME + '/download/' + memeHash) + .set('Authorization', currentSessionToken) + .then(res => { + // console.log('got meme! res is ', res) + return res + }) +} -/* - async createCard( - name, - anonymous - ) { - const act = { - type: 'task-created', - name: name, - color: 'blue', - deck: anonymous - ? [] - : aoStore.member && currentMemberId - ? [currentMemberId] - : [], - inId: anonymous ? null : aoStore.memberCard.taskId || null, - prioritized: false, - } - // console.log('AO: client/api.ts: createCard: ', { - // act, - // 'aoStore.memberCard': aoStore.memberCard, - // }) - return postEvent(act).then(res => { - return res - }) - } - */ - - /* - async createCardIfDoesNotExist( - name, - color, - anonymous - ) { - return new Promise((resolve, reject) => { - aoStore.getTaskByName_async(name, (task) => { - if (isObject(task)) { - console.log("task exists!") - resolve(task) - } else { - const act = { - type: 'task-created', - name: name, - color: color || 'blue', - deck: [currentMemberId], - inId: null, - prioritized: false, - } - postEvent(act).then((res) => { - aoStore.getTaskByName_async(name, (task) => { - resolve(task) - }) - }) - } - }) +export async function uploadMemes(formData, progressCallback) { + return postRequest('/upload', formData) + .on('progress', function (e) { + console.log('Percentage done: ', e) + if (e && e.hasOwnProperty('percent') && e.percent >= 0) { + progressCallback(e.percent) + } }) - } -*/ - async playCard( - from = null, - to - ) { - const act = { - type: 'task-played', - from: from, - to: to, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } - - /* - async createAndPlayCard(name, color, anonymous, to) { - return new Promise((resolve, reject) => { - this.createCardIfDoesNotExist(name, color, anonymous).then(success => { - aoStore.getTaskByName_async(name, found => { - to.taskId = found.taskId - resolve(this.playCard(null, to)) - }) - }) + .on('error', err => { + console.log('Upload failed with error:', err) + return false }) - } - */ - /*async findOrCreateCardInCard( - name, - inId, - prioritizedpostEvent(act) = false, - color = 'blue', - anonymous - ) { - return new Promise((resolve, reject) => { - aoStore.getTaskByName_async(name, found => { - console.log('gotTaskByName name was ', name, 'and found is ', found) - let act - if (found) { - if (prioritized) { - resolve(this.prioritizeCard(found.taskId, inId)) - return - } else { - act = { - type: 'task-sub-tasked', - taskId: inId, - subTask: found.taskId, - memberId: anonymous ? null : currentMemberId, - } - } - } else { - act = { - type: 'task-created', - name: name, - color: color, - deck: anonymous ? [] : [currentMemberId], - inId: inId, - prioritized: prioritized, - } - } - resolve( - postEvent(act).then(res => { - return res - }) - ) - }) + .then(res => { + console.log('sent files. res is', res) + return res }) - }*/ +} - async discardCardFromCard( - taskId, - inId - ) { - const act = { - type: 'task-de-sub-tasked', - taskId: inId, - subTaskId, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function cacheMeme(taskId) { + return await postEvent({ + type: 'meme-cached', + taskId + }) +} - // Empties a card's priorities and subtasks - async emptyCard(taskId) { - const act = { - type: 'task-emptied', - taskIdId, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) +// Cards feature +export async function getCard(taskId, alsoGetRelevant = 'subcards') { + taskId = taskId.trim().toLowerCase() + console.log('taskId to fetch is', taskId) + const payload = { 'taskId': taskId } + const result = await postRequest('/fetchTaskByID', payload) // todo: change to flat text, not JSON + if(!result || !result.body) { + console.log('Error fetching task.') + return null } - - async colorCard(taskId, color) { - const act = { - type: 'task-colored', - taskIdId, - color: color, - inId: null, // add this when we have context, mutation works on server - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) + if(alsoGetRelevant) { + let relevantResult = await getAllRelevantCards(result.body.taskId, alsoGetRelevant) } + return result.body.concat(relevantResult.body) +} - async grabCard(taskId) { - const act = { - type: 'task-grabbed', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) +// Fetches all cards related to the given card object, i.e., cards that could be seen or navigated to immediately from that card +// scope = 'priority' returns only the first/top priority card within the specified card +// scope = 'priorities' returns only the priorities within the specified card +// scope = 'subcards' returns all subcards (priorities, pinned, subTasks, completed) +// Further scopes are not currently needed because the server also includes some related cards with each send +// If existingTasks: Map is provided, those cards will be skipped +// Returns the new cards that were fetched (not any existingTasks), plus cards the server chooses to also include +export async function getAllRelevantCards( + seedTask, + scope = 'priorities', + existingTasks +) { + if(existingTasks === undefined) { + existingTasks = new Map() + } + let taskIdsToFetch + + // Choose which taskIds we are going to request from the server + switch (scope) { + case 'priority': + taskIdsToFetch = new Set(seedTask.priorities.at(-1)) + break + case 'priorities': + taskIdsToFetch = new Set(seedTask.priorities) + break + case 'subcards': + taskIdsToFetch = new Set( + seedTask.priorities.concat(seedTask.subTasks, seedTask.completed) + ) + if (seedTask.pins && seedTask.pins.length >= 1) { + seedTask.pins.forEach(pin => { + taskIdsToFetch.add(pin.taskId) + }) + } } - async grabPile(taskId) { - const act = { - type: 'pile-grabbed', - taskIdId, - memberId: currentMemberId, + // Filter out the taskIds for tasks we already have + taskIdsToFetch.filter(taskId => { + if (!taskId) { + return false } - return postEvent(act).then(res => { - return res - }) + const existingTask = existingTasks.get(taskId) + return !existingTask + }) + if (taskIdsToFetch.length < 1) { + return [] } - async dropCard(taskId) { - const act = { - type: 'task-dropped', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) + // Fetch the cards + try { + const result = await postRequest('/fetchTaskByID', { taskId: taskIdsToFetch }) + } catch (error) { + console.log('Error fetching relevant tasks:', { taskIdsToFetch, error }) + return null } - async removeCards(taskIds) { - const act = { - type: 'tasks-removed', - taskIdsIds, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } + // Filter again (overlapping queries or intelligent server can cause duplicates to be returned) + const newTasksOnly = result.body.foundThisTaskList.filter( + fetchedTask => + !existingTasks.some( + existingTask => existingTask.taskId === fetchedTask.taskId + ) + ) + return newTasksOnly +} - async dropPile(taskId) { - const act = { - type: 'pile-dropped', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function colorCard(taskId, color) { + return await postEvent({ + type: 'task-colored', + taskId: taskId, + color: color, + inId: null, // add this when we have context, mutation works on server + blame: currentMemberId + }) +} - async passCard( - taskId, - toMemberId - ) { - const act = { - type: 'task-passed', - taskIdId, - toMemberId: toMemberId, - fromMemberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function setCardProperty(taskId, property, value) { + return await postEvent({ + type: 'task-property-set', + taskId: taskId, + property: property, + value: value, + blame: currentMemberId + }) +} - async remindMember( - memberId - ) { - const act = { - type: 'member-reminded', - toMemberId: memberId, - fromMemberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// Card send feature +export async function passCard(taskId, toMemberId) { + return await postEvent({ + type: 'task-passed', + taskId: taskId, + toMemberId: toMemberId, + fromMemberId: currentMemberId + }) +} - async swapCard( - inId, - taskId1, - taskId2 - ) { - const act = { - type: 'task-swapped', - taskId: inId, - swapId1Id1, - swapId2Id2, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function remindMember(memberId) { + return await postEvent({ + type: 'member-reminded', + toMemberId: memberId, + fromMemberId: currentMemberId + }) +} - async bumpCard( - taskId, - inId, - direction - ) { - const act = { - type: 'task-bumped', - taskId: inId, - bumpIdId, - direction: direction, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// Cards-in-cards feature +export async function playCard(from = null, to) { + return await postEvent({ + type: 'task-played', + from: from, + to: to, + memberId: currentMemberId + }) +} - /*async prioritizeCard( - taskId, - inId, - position = 0 - ) { - const act = { - type: 'task-prioritized', - taskIdId, - inId: inId, - position: position, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - }*/ +export async function discardCardFromCard(taskId, inId) { + return await postEvent({ + type: 'task-de-sub-tasked', + taskId: taskId, + subTaskId, + blame: currentMemberId + }) +} - async prioritizePile(inId) { - const act = { - type: 'task-prioritized', - inId: inId, - } - return postEvent(act).then(res => { - return res - }) - } +// Empties a card's priorities and subtasks +export async function emptyCard(taskId) { + return await postEvent({ + type: 'task-emptied', + taskId: taskId, + blame: currentMemberId + }) +} - async refocusCard(taskId, inId) { - const act = { - type: 'task-refocused', - taskIdId, - inId: inId, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function swapCard(inId, taskId1, taskId2) { + return await postEvent({ + type: 'task-swapped', + taskId: inId, + swapId1: taskId1, + swapId2: taskId2, + blame: currentMemberId + }) +} - async refocusPile(inId) { - const act = { - type: 'pile-refocused', - inId: inId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function bumpCard(taskId, inId, direction) { + return await postEvent({ + type: 'task-bumped', + taskId: inId, + bumpId: taskId, + direction: direction, + blame: currentMemberId + }) +} - async allocatePriority( - inId, - taskId, - points = 1 - ) { - const act = { - type: 'task-allocated', - taskId: inId, - allocatedIdId, - amount: points, - blame: currentMemberId, - //inId: inId, - } - return postEvent(act).then(res => { - return res - }) - } +// Deck features +export async function grabCard(taskId) { + return await postEvent({ + type: 'task-grabbed', + taskId: taskId, + memberId: currentMemberId + }) +} - async titleMissionCard( - taskId, - newTitle - ) { - const act = { - type: 'task-guilded', - taskIdId, - guild: newTitle, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function grabPile(taskId) { + return await postEvent({ + type: 'pile-grabbed', + taskId: taskId, + memberId: currentMemberId + }) +} - async setCardProperty( - taskId, - property, - value - ) { - const act = { - type: 'task-property-set', - taskIdId, - property: property, - value: value, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function dropCard(taskId) { + return await postEvent({ + type: 'task-dropped', + taskId: taskId, + memberId: currentMemberId + }) +} - async completeCard(taskId) { - const act = { - type: 'task-claimed', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function removeCards(taskIds) { + return await postEvent({ + type: 'tasks-removed', + taskIds: taskIds, + memberId: currentMemberId + }) +} - async uncheckCard(taskId) { - const act = { - type: 'task-unclaimed', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function dropPile(taskId) { + return await postEvent({ + type: 'pile-dropped', + taskId: taskId, + memberId: currentMemberId + }) +} - async setClaimInterval( +// Priority feature +export async function prioritizeCard(taskId, inId, position = 0) { + return await postEvent({ + type: 'task-prioritized', taskId, - newClaimInterval - ) { - const act = { - type: 'task-property-set', - taskIdId, - property: 'claimInterval', - value: newClaimInterval, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } + inId: inId, + position: position, + blame: currentMemberId + }) +} - async createResource( - resourceId, - name, - charged, - secret, - trackStock - ) { - const act = { - type: 'resource-created', - resourceId: resourceId, - name: name, - charged: charged, - secret: secret, - trackStock: trackStock, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function prioritizePile(inId) { + return await postEvent({ + type: 'task-prioritized', + inId: inId + }) +} - async useResource( - resourceId, - amount, - charged, - notes = '' - ) { - const act = { - type: 'resource-used', - resourceId: resourceId, - memberId: currentMemberId, - amount: amount, - charged: charged, - notes: notes, - } - return postEvent(act).then(res => { - return res - }) - } +export async function refocusCard(taskId, inId) { + return await postEvent({ + type: 'task-refocused', + taskId: taskId, + inId: inId, + blame: currentMemberId + }) +} - async stockResource( - resourceId, - amount, - paid, - notes = '' - ) { - const act = { - type: 'resource-stocked', - resourceId: resourceId, - memberId: currentMemberId, - amount: amount, - paid: paid, - notes: notes, - } - return postEvent(act).then(res => { - return res - }) - } +export async function refocusPile(inId) { + return await postEvent({ + type: 'pile-refocused', + inId: inId + }) +} - async purgeResource(resourceId) { - const act = { - type: 'resource-purged', - resourceId: resourceId, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function allocatePriority(inId, taskId, points = 1) { + return await postEvent({ + type: 'task-allocated', + taskId: inId, + allocatedId: taskId, + amount: points, + blame: currentMemberId + //inId: inId, + }) +} - async bookResource( - taskId, - startTime, - endTime - ) { - const act = { - type: 'resource-booked', - resourceIdId, - memberId: currentMemberId, - startTs: startTime, - endTs: endTime, - } - return postEvent(act).then(res => { - return res - }) - } +// Guilds feature +export async function titleMissionCard(taskId, newTitle) { + return await postEvent({ + type: 'task-guilded', + taskId: taskId, + guild: newTitle, + blame: currentMemberId + }) +} - async createMember( - name, - fob = '' - ) { - const secret = createHash(name) - const act = { - type: 'member-created', - name, - secret, - fob, - } - return postEvent(act).then(res => { - return res - }) - } +// Checkmarks feature +export async function completeCard(taskId) { + return await postEvent({ + type: 'task-claimed', + taskId: taskId, + memberId: currentMemberId + }) +} - async activateMember(memberId) { - const act = { - type: 'member-activated', - memberId: memberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function uncheckCard(taskId) { + return await postEvent({ + type: 'task-unclaimed', + taskId: taskId, + memberId: currentMemberId + }) +} - async deactivateMember(memberId) { - const act = { - type: 'member-deactivated', - memberId: memberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function setClaimInterval(taskId, newClaimInterval) { + return await setCardProperty(taskId, claimInterval, newClaimInterval) +} - // senpai function - async resetPassword(memberId) { - const act = { - type: 'member-secret-reset', - kohaiId: memberId, - senpaiId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// Hardware resources feature +export async function createResource( + resourceId, + name, + charged, + secret, + trackStock +) { + return await postEvent({ + type: 'resource-created', + resourceId: resourceId, + name: name, + charged: charged, + secret: secret, + trackStock: trackStock, + blame: currentMemberId + }) +} - // senpai function - async promoteMember(memberId) { - const act = { - type: 'member-promoted', - kohaiId: memberId, - senpaiId: currentMemberId, - } +export async function useResource(resourceId, amount, charged, notes = '') { + return await postEvent({ + type: 'resource-used', + resourceId: resourceId, + memberId: currentMemberId, + amount: amount, + charged: charged, + notes: notes + }) +} - return postEvent(act).then(res => { - return res - }) - } +export async function stockResource(resourceId, amount, paid, notes = '') { + return await postEvent({ + type: 'resource-stocked', + resourceId: resourceId, + memberId: currentMemberId, + amount: amount, + paid: paid, + notes: notes + }) +} - // senpai function - async banMember(memberId) { - const act = { - type: 'member-banned', - kohaiId: memberId, - senpaiId: currentMemberId, - } +export async function purgeResource(resourceId) { + return await postEvent({ + type: 'resource-purged', + resourceId: resourceId, + blame: currentMemberId + }) +} - return postEvent(act).then(res => { - return res - }) - } +export async function bookResource(taskId, startTime, endTime) { + return await postEvent({ + type: 'resource-booked', + resourceId: taskId, + memberId: currentMemberId, + startTs: startTime, + endTs: endTime + }) +} - // senpai function - async unbanMember(memberId) { - const act = { - type: 'member-unbanned', - kohaiId: memberId, - senpaiId: currentMemberId, - } +// Member account features +export async function updateMemberField(field, newValue) { + if (field === 'secret') { + newValue = createHash(newValue) + } + return await postEvent({ + type: 'member-field-updated', + memberId: currentMemberId, + field: field, + newfield: newValue + }) +} - return postEvent(act).then(res => { - return res - }) - } +// Member admin features +export async function createMember(name, fob = '') { + const secret = createHash(name) + return await postEvent({ + type: 'member-created', + name, + secret, + fob + }) +} - // senpai function - async purgeMember(memberId) { - const act = { - type: 'member-purged', - memberId: memberId, - blame: currentMemberId, - } +export async function activateMember(memberId) { + return await postEvent({ + type: 'member-activated', + memberId: memberId + }) +} - return postEvent(act).then(res => { - return res - }) - } +export async function deactivateMember(memberId) { + return await postEvent({ + type: 'member-deactivated', + memberId: memberId + }) +} - async updateMemberField( - field, - newValue - ) { - if (field === 'secret') { - newValue = createHash(newValue) - } - const act = { - type: 'member-field-updated', - memberId: currentMemberId, - field: field, - newfield: newValue, - } - return postEvent(act).then(res => { - return res - }) - } +// senpai function +export async function resetPassword(memberId) { + return await postEvent({ + type: 'member-secret-reset', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} - // Each member has a list of tickers. Each ticker is a string. - // Sets the ticker at position tickerListIndex to symbol coinSymbol. - async setTicker( - fromCoin, - toCoin, - tickerListIndex - ) { - const act = { - type: 'member-ticker-set', - memberId: currentMemberId, - fromCoin: fromCoin, - toCoin: toCoin, - index: tickerListIndex, - } - return postEvent(act).then(res => { - return res - }) - } +// senpai function +export async function promoteMember(memberId) { + return await postEvent({ + type: 'member-promoted', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} - async clockTime(seconds, taskId, date) { - const act = { - type: 'task-time-clocked', - taskIdId, - memberId: currentMemberId, - seconds: seconds, - date: date, - } - return postEvent(act).then(res => { - return res - }) - } +// senpai function +export async function banMember(memberId) { + return await postEvent({ + type: 'member-banned', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} - async startTimeClock(taskId, inId) { - const act = { - type: 'task-started', - taskIdId, - inId: inId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// senpai function +export async function unbanMember(memberId) { + return await postEvent({ + type: 'member-unbanned', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} - async stopTimeClock(taskId) { - const act = { - type: 'task-stopped', - taskIdId, - memberId: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// senpai function +export async function purgeMember(memberId) { + return await postEvent({ + type: 'member-purged', + memberId: memberId, + blame: currentMemberId + }) +} - async signCard(taskId, opinion = 1) { - const act = { - type: 'task-signed', - taskIdId, - memberId: currentMemberId, - opinion: opinion, - } - return postEvent(act).then(res => { - return res - }) - } +// Each member has a list of tickers. Each ticker is a string. +// Sets the ticker at position tickerListIndex to symbol coinSymbol. +export async function setTicker(fromCoin, toCoin, tickerListIndex) { + return await postEvent({ + type: 'member-ticker-set', + memberId: currentMemberId, + fromCoin: fromCoin, + toCoin: toCoin, + index: tickerListIndex + }) +} - async assignMembership( - taskId, - memberId, - level - ) { - const act = { - type: 'task-membership', - taskIdId, - memberId: memberId, - level: level, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +// Timeclock features +export async function clockTime(seconds, taskId, date) { + return await postEvent({ + type: 'task-time-clocked', + taskId: taskId, + memberId: currentMemberId, + seconds: seconds, + date: date + }) +} - async stashCard( - taskId, - inId, - level - ) { - const act = { - type: 'task-stashed', - taskIdId, - inId: inId, - level: level, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function startTimeClock(taskId, inId) { + return await postEvent({ + type: 'task-started', + taskId: taskId, + inId: inId, + memberId: currentMemberId + }) +} - async unstashCard( - taskId, - inId, - level - ) { - const act = { - type: 'task-unstashed', - taskIdId, - inId: inId, - level: level, - blame: currentMemberId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function stopTimeClock(taskId) { + return await postEvent({ + type: 'task-stopped', + taskId: taskId, + memberId: currentMemberId + }) +} - async visitCard( - taskId, - inChat = false, - notify = false - ) { - const act = { - type: 'task-visited', - taskIdId, - memberId: currentMemberId, - area: inChat ? 1 : 0, - notify: notify, - } - return postEvent(act).then(res => { - return res - }) - } +// Group membership features +export async function assignMembership(taskId, memberId, level) { + return await postEvent({ + type: 'task-membership', + taskId: taskId, + memberId: memberId, + level: level, + blame: currentMemberId + }) +} -/* - async markSeen(taskId) { - const task = aoStore.hashMap.get(taskId) - const act = { - type: 'task-seen', - taskId: taskId, - memberId: currentMemberId, - } - // console.log('card marked seen') - return postEvent(act).then(res => { - return res - }) - } -*/ - async resizeGrid( - taskId, - newHeight, - newWidth, - newSize - ) { - const act = { - type: 'grid-resized', - taskIdId, - height: newHeight, - width: newWidth, - size: newSize || 9, - } - return postEvent(act).then(res => { - return res - }) - } +export async function stashCard(taskId, inId, level) { + return await postEvent({ + type: 'task-stashed', + taskId: taskId, + inId: inId, + level: level, + blame: currentMemberId + }) +} - async createCardWithGrid( - name, - height, - width - ) { - const act = { - type: 'grid-created', - name: name, - height: height, - width: width, - color: 'blue', - deck: [currentMemberId], - } - return postEvent(act).then(res => { - return res - }) - } +export async function unstashCard(taskId, inId, level) { + return await postEvent({ + type: 'task-unstashed', + taskId: taskId, + inId: inId, + level: level, + blame: currentMemberId + }) +} - async addGridToCard( - taskId, - height, - width, - spread = 'pyramid' - ) { - const act = { - type: 'grid-added', - taskIdId, - spread: spread, - height: height, - width: width, - } - return postEvent(act).then(res => { - return res - }) - } +// Unreads feature +export async function visitCard(taskId, inChat = false, notify = false) { + return await postEvent({ + type: 'task-visited', + taskId: taskId, + memberId: currentMemberId, + area: inChat ? 1 : 0, + notify: notify + }) +} - async removeGridFromCard(taskId) { - const act = { - type: 'grid-removed', - taskIdId, - } - return postEvent(act).then(res => { - return res - }) - } +/* +export async function markSeen(taskId) { + const task = aoStore.hashMap.get(taskId) + const act = { + type: 'task-seen', + taskId: taskId, + memberId: currentMemberId, + } + // console.log('card marked seen') + return await postEvent(act) +} +*/ - /*async pinCardToGrid( - x, - y, - name, - inId - ) { - return new Promise((resolve, reject) => { - aoStore.getTaskByName_async(name, (task) => { - console.log('gotTaskByName name was ', name, 'and found is ', task) - // console.log("AO: client/api.ts: pinCardToGrid: ", {x, y, name, inId, task}) - - if (isObject(task)) { - const fromLocation - const toLocation: CardLocation = { - taskId.taskId, - inId: inId, - coords: { y, x } - } - playCard() - } else { - const act = { - type: 'task-created', - name: name, - color: 'blue', - deck: [currentMemberId], - inId: inId, - prioritized: false, - } - postEvent(act).then(res => { - const taskId = JSON.parse(res.text).event.taskId - const gridAct = { - type: 'grid-pin', - inId: inId, - taskIdId, - x: x, - y: y, - memberId: currentMemberId, - } - resolve( - request - .post('/events') - .set('Authorization', currentSessionToken) - .send(gridAct) - ) - }) - } - }) - }) - }*/ +// Pinboard feature +export async function resizeGrid(taskId, newHeight, newWidth, newSize) { + return await postEvent({ + type: 'grid-resized', + taskId: taskId, + height: newHeight, + width: newWidth, + size: newSize || 9 + }) +} - /*async unpinCardFromGrid( - x, - y, - inId - ) { - const act = { - type: 'grid-unpin', - x, - y, - inId, - } - return postEvent(act).then(res => { - return res - }) - }*/ +export async function createCardWithGrid(name, height, width) { + return await postEvent({ + type: 'grid-created', + name: name, + height: height, + width: width, + color: 'blue', + deck: [currentMemberId] + }) +} - async fetchMeme(memeHash, progressCallback) { - return request - .get(HOSTNAME + '/meme/' + memeHash) - .responseType('blob') - .set('Authorization', currentSessionToken) - .on('progress', function (e) { - progressCallback(e.percent) - }) - .then(res => { - console.log('got meme! res is ', res) - return res.body - }) - } +export async function addGridToCard(taskId, height, width, spread = 'pyramid') { + return await postEvent({ + type: 'grid-added', + taskId: taskId, + spread: spread, + height: height, + width: width + }) +} - async downloadMeme(memeHash) { - return request - .get(HOSTNAME + '/download/' + memeHash) - .set('Authorization', currentSessionToken) - .then(res => { - // console.log('got meme! res is ', res) - return res - }) - } +export async function removeGridFromCard(taskId) { + return await postEvent({ + type: 'grid-removed', + taskId: taskId + }) +} - async uploadMemes(formData, progressCallback) { - return postRequest('/upload', formData) - .on('progress', function (e) { - console.log('Percentage done: ', e) - if (e && e.hasOwnProperty('percent') && e.percent >= 0) { - progressCallback(e.percent) - } - }) - .on('error', err => { - console.log('Upload failed with error:', err) - return false - }) - .then(res => { - console.log('sent files. res is', res) - return res - }) - } +// This function encodes whatever is passed by the search box as a URIComponent and passes it to a search endpoint, returning the response when supplied +export async function search(querystring, take = 10, skip = 0) { + const qs = encodeURIComponent(querystring) + const params = `?take=${take}&skip=${skip}` + return await postRequest('/search/' + qs + params) +} - async cacheMeme(taskId) { - const act = { - type: 'meme-cached', - taskId, - } - return postEvent(act).then(res => { - return res - }) - } +export async function requestBtcQr(taskId) { + return await postEvent({ + type: 'address-updated', + taskId + }) +} - async logout() { - //aoStore.resetState() - if(typeof window !== 'undefined') window.localStorage.clear() - //clear cookie +export async function requestLightningInvoice(taskId, amount = 0) { + return await postEvent({ + type: 'invoice-created', + taskId, + amount: amount + }) +} - return postRequest('/logout', {}) - .then(res => { - return res - }) - } +// Proposals features +export async function signCard(taskId, opinion = 1) { + return await postEvent({ + type: 'task-signed', + taskId: taskId, + memberId: currentMemberId, + opinion: opinion + }) +} - // This function encodes whatever is passed by the search box as a URIComponent and passes it to a search endpoint, returning the response when supplied - async search(querystring, take = 10, skip = 0) { - const qs = encodeURIComponent(querystring) - const params = `?take=${take}&skip=${skip}` - return postRequest('/search/' + qs + params) - .then(res => { - return res - }) - } +export async function setQuorum(quorum) { + return await postEvent({ + type: 'quorum-set', + quorum: quorum + }) +} - async requestBtcQr(taskId) { - const act = { - type: 'address-updated', - taskId, - } - return postEvent(act).then(res => { - return res - }) - } - - async requestLightningInvoice(taskId, amount = 0) { - const act = { - type: 'invoice-created', - taskId, - amount: amount, - } - return postEvent(act).then(res => { - return res - }) - } +/*startSocketListeners() { + this.socket.connect() + this.socket.on('connect', () => { + console.log('connected', { 'aoStore.state': aoStore.state }) + + runInAction(() => { + aoStore.state.socketState = 'attemptingAuthentication' + const loadedSession = window.localStorage.getItem('session') + if(loadedSession) { + aoStore.state.session = loadedSession + } + const loadedToken = window.localStorage.getItem('token') + if(loadedToken) { + currentSessionToken = loadedToken + } + }) - /*startSocketListeners() { - this.socket.connect() - this.socket.on('connect', () => { - console.log('connected', { 'aoStore.state': aoStore.state }) + console.log( + 'emit auth: session: ' + + window.localStorage.getItem('session') + + ', token: ' + + window.localStorage.getItem('token') + ) + this.socket.emit('authentication', { + session: window.localStorage.getItem('session'), + token: window.localStorage.getItem('token'), + }) + }) + this.socket.on('authenticated', () => { + console.log('authenticated') + this.fetchState().then(() => { runInAction(() => { - aoStore.state.socketState = 'attemptingAuthentication' - const loadedSession = window.localStorage.getItem('session') - if(loadedSession) { - aoStore.state.session = loadedSession - } - const loadedToken = window.localStorage.getItem('token') - if(loadedToken) { - currentSessionToken = loadedToken - } - }) - - console.log( - 'emit auth: session: ' + - window.localStorage.getItem('session') + - ', token: ' + - window.localStorage.getItem('token') - ) - this.socket.emit('authentication', { - session: window.localStorage.getItem('session'), - token: window.localStorage.getItem('token'), + aoStore.state.socketState = 'authenticationSuccess' }) }) - this.socket.on('authenticated', () => { - console.log('authenticated') - - this.fetchState().then(() => { - runInAction(() => { - aoStore.state.socketState = 'authenticationSuccess' - }) - }) - this.socket.on('eventstream', ev => { - console.log('AO: client/api.ts: socketListener: event:', ev) + this.socket.on('eventstream', ev => { + console.log('AO: client/api.ts: socketListener: event:', ev) - aoStore.applyEvent(ev) - }) + aoStore.applyEvent(ev) }) - this.socket.on('disconnect', reason => { - console.log('disconnected') + }) + this.socket.on('disconnect', reason => { + console.log('disconnected') - runInAction(() => { - aoStore.state.socketState = 'authenticationFailed' - }) - - this.socket.connect() + runInAction(() => { + aoStore.state.socketState = 'authenticationFailed' }) - }*/ -} + + this.socket.connect() + }) +}*/ /*reaction( () => { @@ -1308,9 +849,231 @@ class AoApi { socketState => console.log('AO: client/api.ts: socketState: ' + socketState) ) console.log("NODE_ENV is", process.env.NODE_ENV)*/ -const socketUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8003' : '/' -const socket = io(socketUrl, { - autoConnect: false, -}) -const api = new AoApi(socket) -export default api + +//const api = new AoApi(socket) +//export default api + +/*async fetchState() { + const session = window.localStorage.getItem('session') + const token = window.localStorage.getItem('token') + const user = window.localStorage.getItem('user') + if (session && token && user) { + return request + .post('/state') + .set('Authorization', token) + .then(res => { + aoStore.state.user = user + // console.log( + // 'AO: client/api.ts: fetchState: initial state: ', + // res.body + // ) + + let dataPackageToSendToClient = res.body + // Get the memberId + let memberId + dataPackageToSendToClient.stateToSend.sessions.forEach(sessionItem => { + if (session === sessionItem.session) { + memberId = sessionItem.ownerId + } + }) + + // See if we got our member card + let foundMemberCard + if(!memberId) { + console.log("memberId missing when loading state") + } else { + dataPackageToSendToClient.stateToSend.tasks.forEach(task => { + if(task.taskId === memberId) { + foundMemberCard = task + } + }) + } + if(foundMemberCard) { + console.log("State includes member card:", foundMemberCard) + } else { + console.log("State does not include member card") + } + + aoStore.initializeState(dataPackageToSendToClient.stateToSend) + + let metaData = dataPackageToSendToClient.metaData + aoStore.memberDeckSize = metaData.memberDeckSize + aoStore.bookmarksTaskId = metaData.bookmarksTaskId + + return true + }) + .catch(() => false) + } + return Promise.resolve(false) +}*/ + +/* +export async createCard( + name, + anonymous +) { + const act = { + type: 'task-created', + name: name, + color: 'blue', + deck: anonymous + ? [] + : aoStore.member && currentMemberId + ? [currentMemberId] + : [], + inId: anonymous ? null : aoStore.memberCard.taskId || null, + prioritized: false, + } + // console.log('AO: client/api.ts: createCard: ', { + // act, + // 'aoStore.memberCard': aoStore.memberCard, + // }) + return await postEvent(act) +} +*/ + +/* +export async createCardIfDoesNotExist( + name, + color, + anonymous +) { +return new Promise((resolve, reject) => { + aoStore.getTaskByName_async(name, (task) => { + if (isObject(task)) { + console.log("task exists!") + resolve(task) + } else { + const act = { + type: 'task-created', + name: name, + color: color || 'blue', + deck: [currentMemberId], + inId: null, + prioritized: false, + } + postEvent(act).then((res) => { + aoStore.getTaskByName_async(name, (task) => { + resolve(task) + }) + }) + } + }) + }) +} +*/ + +/* +export async createAndPlayCard(name, color, anonymous, to) { + return new Promise((resolve, reject) => { + this.createCardIfDoesNotExist(name, color, anonymous).then(success => { + aoStore.getTaskByName_async(name, found => { + to.taskId = found.taskId + resolve(this.playCard(null, to)) + }) + }) + }) +} +*/ +/*async findOrCreateCardInCard( + name, + inId, + prioritizedpostEvent(act) = false, + color = 'blue', + anonymous +) { + return new Promise((resolve, reject) => { + aoStore.getTaskByName_async(name, found => { + console.log('gotTaskByName name was ', name, 'and found is ', found) + let act + if (found) { + if (prioritized) { + resolve(this.prioritizeCard(found.taskId, inId)) + return + } else { + act = { + type: 'task-sub-tasked', + taskId: inId, + subTask: found.taskId, + memberId: anonymous ? null : currentMemberId, + } + } + } else { + act = { + type: 'task-created', + name: name, + color: color, + deck: anonymous ? [] : [currentMemberId], + inId: inId, + prioritized: prioritized, + } + } + resolve( + postEvent(act).then(res => return res) + ) + }) + }) +}*/ + +/*async function pinCardToGrid( + x, + y, + name, + inId +) { + return new Promise((resolve, reject) => { + aoStore.getTaskByName_async(name, (task) => { + console.log('gotTaskByName name was ', name, 'and found is ', task) + // console.log("AO: client/api.ts: pinCardToGrid: ", {x, y, name, inId, task}) + + if (isObject(task)) { + const fromLocation + const toLocation: CardLocation = { + taskId.taskId, + inId: inId, + coords: { y, x } + } + playCard() + } else { + const act = { + type: 'task-created', + name: name, + color: 'blue', + deck: [currentMemberId], + inId: inId, + prioritized: false, + } + postEvent(act).then(res => { + const taskId = JSON.parse(res.text).event.taskId + const gridAct = { + type: 'grid-pin', + inId: inId, + taskId: taskId, + x: x, + y: y, + memberId: currentMemberId, + } + resolve( + request + .post('/events') + .set('Authorization', currentSessionToken) + .send(gridAct) + ) + }) + } + }) + }) +}*/ + +/*async function unpinCardFromGrid( + x, + y, + inId +) { + return await postEvent({ + type: 'grid-unpin', + x, + y, + inId, + }) +}*/ diff --git a/scripts/bootstrap.js b/scripts/bootstrap.js index 40d0635..04b1c8e 100644 --- a/scripts/bootstrap.js +++ b/scripts/bootstrap.js @@ -1,6 +1,7 @@ // The bootstrapping module uses the glossary in peers.json (later will use members from DB?) // to look up tor addresses for the give shortname or SSH public key. // We could just do all this in the AO, but the bootstrapper is for public / loose ties and the AO's explicit p2p is for close / private ties. +// The other main difference is that the AO stores data, and the chat server does not (ao-cli only uses database for Use AO Features). // The bootstrapper occasionally queries all of the tor addresses in your address book. // If they are an AO with bootstrapping turned on, the AO server will respond with its public directory information. // Since you have connected to them via their .onion address, it is assumed they are a known trusted party, diff --git a/scripts/features.js b/scripts/features.js index 4478e37..c9485dd 100644 --- a/scripts/features.js +++ b/scripts/features.js @@ -8,6 +8,16 @@ import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +// It is possible to run ao-cli with npx @autonomousorganization/ao-cli. In this case, it can help you install it permanently. +export function installAoCli() { + try { + execSync('npm i -g @autonomousorganization/ao-cli 2>&1') + console.log('Installed ao-cli.') + } catch(err) { + console.log('Error installing ao-cli:', err) + } +} + // Adds a line to .bashrc to make 'ao' an alias for 'ao-cli', to simplify using the AO from the command line export function installAoAlias() { try { @@ -31,7 +41,7 @@ export async function selfUpdate() { const result = execSync('npm update -g @autonomousorganization/ao-cli 2>&1') const afterVersionNumber = await getAoCliVersion() if(beforeVersionNumber === afterVersionNumber) { - console.log("AO version is already current.") + console.log("ao-cli version is already current.") } else { console.log('\nao-cli self-updated automatically from version', beforeVersionNumber, 'to version', afterVersionNumber, 'from the official npm repository.') } diff --git a/scripts/manual.js b/scripts/manual.js index 195d5e3..25e1912 100644 --- a/scripts/manual.js +++ b/scripts/manual.js @@ -3,7 +3,7 @@ import chalk from 'chalk' import inquirer from 'inquirer' import { loadYamlMarkdownFile, lsFolder, isFolder } from './files.js' import { repeatString, centerLines } from './strings.js' -import { headerStyle, manualTitleStyle } from './chalkStyles.js' +import { headerStyle, manualTitleStyle } from './styles.js' import { basename } from 'path' import { marked } from 'marked' import TerminalRenderer from 'marked-terminal' diff --git a/scripts/priority.js b/scripts/priority.js new file mode 100644 index 0000000..86f0a1a --- /dev/null +++ b/scripts/priority.js @@ -0,0 +1,23 @@ +import { aoEnv } from './settings.js' +import { getCard } from './api.js' + +// Prints the text (.name) of the first card prioritized in the logged-in users member card +export async function getTopPriorityText() { + // Get the first priority of my member card + const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') + if(!memberId) { + return 'Not logged in' + } + const fetchedCards = await getCard(memberId, 'priority') + console.log('fetch result:', fetchedCards) + if(!fetchedCards || fetchedCards.length < 2) { + return 'None' + } + const firstPriorityCard = fetchedCards[1] + return firstPriorityCard.name +} + +// Makes an API request to get the first prioritized card in the member card of the logged-in user +async function getFirstPriorityCard() { + +} \ No newline at end of file diff --git a/scripts/session.js b/scripts/session.js new file mode 100644 index 0000000..52583f2 --- /dev/null +++ b/scripts/session.js @@ -0,0 +1,62 @@ +import { createSession, logout as apiLogout } from './api.js' +import { aoEnv, setAoEnv } from './settings.js' +import { askQuestionText } from './welcome.js' + +// Returns true if there is a session cookie for ao-cli saved in the AO .env file (=ready to make session requests) +export function isLoggedIn() { + const username = aoEnv('AO_CLI_SESSION_USERNAME') + const sessionId = aoEnv('AO_CLI_SESSION_ID') + const sessionToken = aoEnv('AO_CLI_SESSION_TOKEN') + return username && sessionId && sessionToken +} + +// Interactive prompt to log in. Performs the login request. +export async function loginPrompt() { + const username = await askQuestionText('Username:') + const password = await askQuestionText('Password:', { type: 'password' }) + await login(username, password) +} + +export async function login(username, password) { + try { + console.log('Attempting login as', username, 'with password', '*'.repeat(password.length)) + const response = await createSession(username, password) + if(response) { + setAoEnv('AO_CLI_SESSION_USERNAME', username) + setAoEnv('AO_CLI_SESSION_MEMBERID', response.memberId) // might not need to save this actually + setAoEnv('AO_CLI_SESSION_ID', response.session) + setAoEnv('AO_CLI_SESSION_TOKEN', response.token) + console.log('Logged in as', username + '.', 'memberId:', response.memberId) + return true + } else { + console.log('Login failed. Response:', response) + return false + } + } catch(err) { + if(err.status === 401) { + console.log("No account on the AO server matched the username and password you entered. (401 Unauthorized)") + } else { + console.log(err) + } + return false + } +} + +export async function logout() { + try { + console.log('Logging out...') + const response = await apiLogout() + if(response.statusCode === 200) { + setAoEnv('AO_CLI_SESSION_USERNAME', null) + setAoEnv('AO_CLI_SESSION_ID', null) + setAoEnv('AO_CLI_SESSION_TOKEN', null) + console.log('Logged out') + } else { + console.log('Logout failed. Response:', response) + return false + } + } catch(err) { + console.log(err) + } + return true +} \ No newline at end of file diff --git a/scripts/settings.js b/scripts/settings.js index 1f82624..80bd194 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -99,7 +99,7 @@ export function setAoEnv(variable, value) { } if(value === null) { - delete parsedFile.variable + delete parsedFile[variable] } else { parsedFile[variable] = value } diff --git a/scripts/chalkStyles.js b/scripts/styles.js similarity index 100% rename from scripts/chalkStyles.js rename to scripts/styles.js diff --git a/scripts/tests.js b/scripts/tests.js index 51211a1..fd03df2 100644 --- a/scripts/tests.js +++ b/scripts/tests.js @@ -2,14 +2,14 @@ // The tests actually happen so your database will be modified (future: allow switching databases or automatically switch) // The tests use an AO API file saved in the same directory; this file must be kept up-to-date // Maybe in the future a precompiled api.js created from api.ts can be hosted so that ao-cli does not have to compile any TypeScript -import api from './api.js' +import { createSession, logout } from './api.js' async function testLoginAndOut() { const username = 'ao' const password = 'ao' try { console.log('Attempting login as', username, 'with password', '*'.repeat(password.length)) - const response = await api.createSession(username, password) + const response = await createSession(username, password) if(response === true) { console.log('Logged in as', username) } else { @@ -26,7 +26,7 @@ async function testLoginAndOut() { try { console.log('Logging out...') - const response = await api.logout() + const response = await logout() if(response.statusCode === 200) { console.log('Logged out') } else { diff --git a/scripts/util.js b/scripts/util.js index 9a352e7..835aa88 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -16,3 +16,5 @@ export function selectRandom(arrayToChooseFrom) { export function sleep(ms = 550) { return new Promise((r) => setTimeout(r, ms)) } + +export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' \ No newline at end of file diff --git a/scripts/welcome.js b/scripts/welcome.js index edbde73..142a1f9 100644 --- a/scripts/welcome.js +++ b/scripts/welcome.js @@ -1,6 +1,7 @@ import chalk from 'chalk' +import inquirer from 'inquirer' import { selectRandom } from './util.js' -import { greenChalk, theAO, theMenu } from './chalkStyles.js' +import { greenChalk, theAO, theMenu } from './styles.js' // Different sets of messages that can be randomly selected from const welcomeMessages = [ @@ -22,7 +23,9 @@ const welcomeMessages = [ `You are offered a choice between two pills. However, you have secretly built up an immunity to both pills, and trick your opponent into taking one. Inconceviably, you are in ${theAO}.`, `A young man with spiky hair and golden skin appears before you in a halo of light. He guides you to ${theAO}.`, `You enter a gap between two hedges and, after struggling through the brush, emerge into a sunny estate garden. You've found the AO.`, - `You find a small animal burrow dug near the riverside. Crawling in, you find a network of caves that lead to the AO.` + `You find a small animal burrow dug-out near the riverside. Crawling in, you find a network of caves that lead to ${theAO}.`, + `You receive a handwritten letter in the mail, which reads, in fine calligraphy:, "Dear —, You are in ${theAO}."`, + `An unexpected calm settles over you. The ${greenChalk.bold('AO')} is here: Here is ${theAO}.` ] const menuMessages = [ `You see ${theMenu}:`, @@ -89,6 +92,19 @@ export function roger() { return selectRandom(rogerMessages) } +// Returns a random farewell message export function farewell() { console.log(chalk.yellow.bold(selectRandom(farewellMessages))) } + +// Ask the user the given question and returns their textual response +export async function askQuestionText(prompt = 'Please enter a string:', promptOptions = {}) { + let options = { + name: 'text', + type: 'input', + message: prompt + } + Object.assign(options, promptOptions) + const answer = await inquirer.prompt(options) + return answer.text +} \ No newline at end of file