diff --git a/src/ao-lib/api.js b/src/ao-lib/api.js new file mode 100644 index 0000000..d5c9711 --- /dev/null +++ b/src/ao-lib/api.js @@ -0,0 +1,1127 @@ +import request from 'superagent' +import { v1 as uuidV1 } from 'uuid' +import { io } from 'socket.io-client' +import { createHash, hmacHex } from './crypto.js' +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 = 'http://' + AO_DEFAULT_HOSTNAME // was process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/' +export const socket = io(AO_SOCKET_URL, { + autoConnect: false +}) + +// The current connected state of the websocket, can be 'attemptingAuthentication' | 'authenticationSuccess' | 'authenticationFailed' +export let socketStatus + +// Load the current session cookies from the AO .env file +let currentMemberId = aoEnv('AO_CLI_SESSION_MEMBERID') +let currentSessionId = aoEnv('AO_CLI_SESSION_ID') +let currentSessionToken = aoEnv('AO_CLI_SESSION_TOKEN') + +// Performs a GET request to the specified endpoint, sending the given payload +export async function getRequest(endpoint, payload = null, alternateHost = null, verbose = true) { + const target = alternateHost || HOSTNAME + try { + if(payload) { + return await request + .get(target + endpoint) + .send(payload) + } else { + return await request.get(target + endpoint) + } + } catch (err) { + if(verbose) console.log('request failed', err) + return null + } +} + +// Performs a POST request to the specified endpoint, sending the given payload +export async function postRequest(endpoint, payload = null, verbose = true) { + if (!currentSessionToken) { + if(verbose) console.log('Session token not set, API not ready.') + return new Promise(() => null) + } + try { + if(payload) { + return await request + .post(HOSTNAME + endpoint) + .send(payload) + .set('authorization', currentSessionToken) + .set('session', currentSessionId) + } else { + return await request.post(HOSTNAME + endpoint) + .set('authorization', currentSessionToken) + .set('session', currentSessionId) + } + } catch (err) { + if(verbose) console.log('request failed', err) + return null + } +} + +// Performs a post request to the /event endpoint, sending the given JSON object as the event +export async function postEvent(event, verbose) { + return await postRequest('/events', event, verbose) +} + +// 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 + return { session, token, memberId: currentMemberId } +} + +export async function logout() { + return await postRequest('/logout') +} + +// AO p2p over tor features +export async function nameAo(newName) { + return await postEvent({ + type: 'ao-named', + alias: newName + }) +} + +// When you call startSocketListeners, it will either attempt to connect and authenticate on a web socket, or fail and return. +// onAuthenticated is a function that will be called when the client authenticates on the web socket (logs in/connects). +// In your onAuthenticated function, you should trigger fetchState or other initial fetching of state from server. +// eventCallback is a function (ev) => {} that will be called whenever an event is received on the socket. +// In this way, initial state can be fetched and then updates received after that can be used to update the local state model. +export async function startSocketListeners(onAuthenticated, onEvent, verbose = true) { + if(typeof onAuthenticated !== 'function' || typeof onEvent !== 'function') { + console.log('startSocketListeners requires two callback functions as arguments.') + return + } + socket.connect() + socket.on('connect', () => { + if(verbose) console.log('websocket connected') + socketStatus = 'attemptingAuthentication' + if(!currentSessionId || !currentSessionToken) { + if(verbose) console.log('No current session, must log in to authenticate and use socket.') + return + } + + socket.emit('authentication', { + session: currentSessionId, + token: currentSessionToken, + }) + }) + socket.on('authenticated', () => { + if(verbose) console.log('websocket authenticated') + socketStatus = 'authenticationSuccess' + socket.on('eventstream', onEvent) + onAuthenticated() + }) + socket.on('disconnect', reason => { + if(verbose) console.log('websocket disconnected') + socketStatus = 'authenticationFailed' + socket.connect() + }) +} + +// Requests the public bootstrap list by making a public (not logged in) GET request to this or the specified server +export async function getAoBootstrapList(serverOnion = null) { + const result = await getRequest('/bootstrap', undefined, serverOnion, false) + if(!result || !result.ok || result.body.addresses.length < 1) { + return null + } + return result.body.addresses +} + +// Gets the bootsrap list from the specified or our server, then recursively bootstraps from each address on the list +// The AO network is small right now so this shouldn't cause any problems for a while +export async function bootstrap(serverOnion = null) { + if(!serverOnion) serverOnion = HOSTNAME + let alreadyQueried = [ serverOnion ] + let onionList = await getAoBootstrapList(serverOnion) + if(!onionList) { + return null + } + for(let i = 0; i < onionList.length; i++) { + const onion = onionList[i] + let more = await bootstrap(onion) + if(!more) continue + more = more.filter(onion => !onionList.concat(alreadyQueried).includes(onion)) + onionList.concat(more) + } + return onionList +} + +export async function shadowchat(room, message, username) { + return await postEvent({ + type: 'shadowchat', + room: room, + name: username, + message: message, + }) +} + +export async function connectToAo(address, secret) { + return await postEvent({ + type: 'ao-outbound-connected', + address: address, + secret: secret + }) +} + +export async function deleteAoConnection(address) { + return await postEvent({ + type: 'ao-disconnected', + address: address + }) +} + +export async function relayEventToOtherAo(address, event) { + return await postEvent({ + type: 'ao-relay', + address: address, + ev: event + }) +} + +export async function linkCardOnAo(taskId, address) { + return await postEvent({ + type: 'ao-linked', + address: address, + taskId: taskId + }) +} + +// Avatar and presence features +export async function bark() { + return await postEvent({ + type: 'doge-barked', + memberId: currentMemberId + }) +} + +export async function hopped(taskId) { + return await postEvent({ + type: 'doge-hopped', + memberId: currentMemberId, + taskId: taskId + }) +} + +export async function mute() { + return await updateMemberField('muted', true) +} + +export async function unmute() { + return await updateMemberField('muted', false) +} + +// 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 + }) +} + +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 + }) +} + +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) + } + }) + .on('error', err => { + console.log('Upload failed with error:', err) + return false + }) + .then(res => { + console.log('sent files. res is', res) + return res + }) +} + +export async function cacheMeme(taskId) { + return await postEvent({ + type: 'meme-cached', + taskId + }) +} + +// Cards feature +// Returns the card and other cards as specified by the alsoGetRelevant arg +// If multiple cards are returned, they will be returned in their global deck order (global creation order on server) +export async function getCard(taskId, alsoGetRelevant = 'subcards') { + taskId = taskId.trim().toLowerCase() + let payload = { taskId: taskId } + const result = await postRequest('/fetchTaskByID', payload, false) // todo: change to flat text, not JSON (?) + if(!result || !result.body) { + //console.log('Error fetching task.') + return null + } + if(alsoGetRelevant) { + let relevantCards = await getAllRelevantCards(result.body, alsoGetRelevant) + return [result.body, ...relevantCards] + } + return [result.body] +} + +// Cards feature +export async function getCardByName(taskName, alsoGetRelevant = 'subcards') { + taskName = taskName.trim() + let payload = { taskName: taskName } + const result = await postRequest('/fetchTaskByName_exact', payload, false) // todo: change to flat text, not JSON (?) + if(!result || !result.body || result.statusCode === 204 || result.statusCode === 400) { + //console.log('Error fetching task.') + return null + } + if(alsoGetRelevant) { + let relevantCards = await getAllRelevantCards(result.body, alsoGetRelevant) + return [result.body, ...relevantCards] + } + return [result.body] +} + +// 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) + }) + } + } + + // Filter out the taskIds for tasks we already have + taskIdsToFetch = [...taskIdsToFetch].filter(taskId => { + if (!taskId) { + return false + } + const existingTask = existingTasks.get(taskId) + return !existingTask + }) + if(taskIdsToFetch.length < 1) { + return [] + } + + // Fetch the cards + try { + const result = await postRequest('/fetchTasks', { taskIds: taskIdsToFetch }) + // Filter again (overlapping queries or intelligent server can cause duplicates to be returned) + const newTasksOnly = result.body.filter( + fetchedTask => !existingTasks.get(fetchedTask.taskId) + ) + return newTasksOnly + } catch (error) { + console.log('Error fetching relevant tasks:', { taskIdsToFetch, error }) + return null + } +} + +export async function createCard( + name, + anonymous = false, + prioritized = false +) { + return await postEvent({ + type: 'task-created', + name: name, + color: 'blue', + deck: (anonymous || !currentMemberId) ? [] : [currentMemberId], + inId: anonymous ? null : currentMemberId || null, + prioritized: prioritized, + }, false) +} + +// This is different from only setting a card property because when a card's color changes, it is bumped to the top of the .subTasks color pile +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 + }) +} + +// Set arbitrary metadata on a card +export async function setCardProperty(taskId, property, value) { + return await postEvent({ + type: 'task-property-set', + taskId: taskId, + property: property, + value: value, + blame: currentMemberId + }) +} + +// Card send feature +export async function passCard(taskId, toMemberId) { + return await postEvent({ + type: 'task-passed', + taskId: taskId, + toMemberId: toMemberId, + fromMemberId: currentMemberId + }) +} + +// Send an immediate bark and notification to the member if possible, reminding them that they have an unread message from you (no automated pings) +export async function remindMember(memberId) { + return await postEvent({ + type: 'member-reminded', + toMemberId: memberId, + fromMemberId: currentMemberId + }) +} + +// Cards-in-cards feature +export async function playCard(from = null, to) { + return await postEvent({ + type: 'task-played', + from: from, + to: to, + memberId: currentMemberId + }) +} + +export async function discardCardFromCard(taskId, inId) { + return await postEvent({ + type: 'task-de-sub-tasked', + taskId: inId, + subTask: taskId, + blame: currentMemberId + }) +} + +// Empties a card's priorities and subtasks +export async function emptyCard(taskId) { + return await postEvent({ + type: 'task-emptied', + taskId: taskId, + blame: currentMemberId + }) +} + +export async function swapCard(inId, taskId1, taskId2) { + return await postEvent({ + type: 'task-swapped', + taskId: inId, + swapId1: taskId1, + swapId2: taskId2, + blame: currentMemberId + }) +} + +export async function bumpCard(taskId, inId, direction) { + return await postEvent({ + type: 'task-bumped', + taskId: inId, + bumpId: taskId, + direction: direction, + blame: currentMemberId + }) +} + +// Deck features +export async function grabCard(taskId) { + return await postEvent({ + type: 'task-grabbed', + taskId: taskId, + memberId: currentMemberId + }) +} + +export async function grabPile(taskId) { + return await postEvent({ + type: 'pile-grabbed', + taskId: taskId, + memberId: currentMemberId + }) +} + +export async function dropCard(taskId) { + return await postEvent({ + type: 'task-dropped', + taskId: taskId, + memberId: currentMemberId + }) +} + +export async function removeCards(taskIds) { + return await postEvent({ + type: 'tasks-removed', + taskIds: taskIds, + memberId: currentMemberId + }) +} + +export async function dropPile(taskId) { + return await postEvent({ + type: 'pile-dropped', + taskId: taskId, + memberId: currentMemberId + }) +} + +// Priority feature +export async function prioritizeCard(taskId, inId, position = 0, echelon = null) { + return await postEvent({ + type: 'task-prioritized', + taskId: taskId, + inId: inId, + position: position, + ...(echelon && { echelon: echelon }), + blame: currentMemberId + }) +} + +export async function prioritizePile(inId) { + return await postEvent({ + type: 'task-prioritized', + inId: inId + }) +} + +export async function refocusCard(taskId, inId) { + return await postEvent({ + type: 'task-refocused', + taskId: taskId, + inId: inId, + blame: currentMemberId + }) +} + +export async function refocusPile(inId) { + return await postEvent({ + type: 'pile-refocused', + inId: inId + }) +} + +export async function allocatePriority(inId, taskId, points = 1) { + return await postEvent({ + type: 'task-allocated', + taskId: inId, + allocatedId: taskId, + amount: points, + blame: currentMemberId + //inId: inId, + }) +} + +// Guilds feature +export async function tagCard(taskId, newTitle) { + return await postEvent({ + type: 'task-guilded', + taskId: taskId, + guild: newTitle, + blame: currentMemberId + }) +} + +export async function getCardsForTag(tag) { + return (await postRequest('/fetchTasksByGuild', { guild: tag })).body.cards +} + +// Checkmarks feature +export async function completeCard(taskId) { + return await postEvent({ + type: 'task-claimed', + taskId: taskId, + memberId: currentMemberId + }) +} + +export async function uncheckCard(taskId) { + return await postEvent({ + type: 'task-unclaimed', + taskId: taskId, + memberId: currentMemberId + }) +} + +export async function setClaimInterval(taskId, newClaimInterval) { + return await setCardProperty(taskId, claimInterval, newClaimInterval) +} + +// 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 + }) +} + +export async function useResource(resourceId, amount, charged, notes = '') { + return await postEvent({ + type: 'resource-used', + resourceId: resourceId, + memberId: currentMemberId, + amount: amount, + charged: charged, + notes: notes + }) +} + +export async function stockResource(resourceId, amount, paid, notes = '') { + return await postEvent({ + type: 'resource-stocked', + resourceId: resourceId, + memberId: currentMemberId, + amount: amount, + paid: paid, + notes: notes + }) +} + +export async function purgeResource(resourceId) { + return await postEvent({ + type: 'resource-purged', + resourceId: resourceId, + blame: currentMemberId + }) +} + +export async function bookResource(taskId, startTime, endTime) { + return await postEvent({ + type: 'resource-booked', + resourceId: taskId, + memberId: currentMemberId, + startTs: startTime, + endTs: endTime + }) +} + +// 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 + }) +} + +// Member admin features +export async function createMember(name, fob = '') { + const secret = createHash(name) + return await postEvent({ + type: 'member-created', + name, + secret, + fob + }) +} + +export async function activateMember(memberId) { + return await postEvent({ + type: 'member-activated', + memberId: memberId + }) +} + +export async function deactivateMember(memberId) { + return await postEvent({ + type: 'member-deactivated', + memberId: memberId + }) +} + +// senpai function +export async function resetPassword(memberId) { + return await postEvent({ + type: 'member-secret-reset', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} + +// senpai function +export async function promoteMember(memberId) { + return await postEvent({ + type: 'member-promoted', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} + +// senpai function +export async function banMember(memberId) { + return await postEvent({ + type: 'member-banned', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} + +// senpai function +export async function unbanMember(memberId) { + return await postEvent({ + type: 'member-unbanned', + kohaiId: memberId, + senpaiId: currentMemberId + }) +} + +// senpai function +export async function purgeMember(memberId) { + return await postEvent({ + type: 'member-purged', + memberId: memberId, + blame: currentMemberId + }) +} + +// 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 + }) +} + +// Timeclock features +export async function clockTime(seconds, taskId, date) { + return await postEvent({ + type: 'task-time-clocked', + taskId: taskId, + memberId: currentMemberId, + seconds: seconds, + date: date + }) +} + +export async function startTimeClock(taskId, inId) { + return await postEvent({ + type: 'task-started', + taskId: taskId, + inId: inId, + memberId: currentMemberId + }) +} + +export async function stopTimeClock(taskId) { + return await postEvent({ + type: 'task-stopped', + taskId: taskId, + memberId: currentMemberId + }) +} + +// Group membership features +export async function assignMembership(taskId, memberId, level) { + return await postEvent({ + type: 'task-membership', + taskId: taskId, + memberId: memberId, + level: level, + blame: currentMemberId + }) +} + +export async function stashCard(taskId, inId, level) { + return await postEvent({ + type: 'task-stashed', + taskId: taskId, + inId: inId, + level: level, + blame: currentMemberId + }) +} + +export async function unstashCard(taskId, inId, level) { + return await postEvent({ + type: 'task-unstashed', + taskId: taskId, + inId: inId, + level: level, + blame: currentMemberId + }) +} + +// 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 + }) +} + +/* +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) +} +*/ + +// 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 + }) +} + +export async function createCardWithGrid(name, height, width) { + return await postEvent({ + type: 'grid-created', + name: name, + height: height, + width: width, + color: 'blue', + deck: [currentMemberId] + }) +} + +export async function addGridToCard(taskId, height, width, spread = 'pyramid') { + return await postEvent({ + type: 'grid-added', + taskId: taskId, + spread: spread, + height: height, + width: width + }) +} + +export async function removeGridFromCard(taskId) { + return await postEvent({ + type: 'grid-removed', + taskId: taskId + }) +} + +// 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) +} + +export async function requestBtcQr(taskId) { + return await postEvent({ + type: 'address-updated', + taskId + }) +} + +export async function requestLightningInvoice(taskId, amount = 0) { + return await postEvent({ + type: 'invoice-created', + taskId, + amount: amount + }) +} + +// Proposals features +export async function signCard(taskId, opinion = 1) { + return await postEvent({ + type: 'task-signed', + taskId: taskId, + memberId: currentMemberId, + opinion: opinion + }) +} + +export async function setQuorum(quorum) { + return await postEvent({ + type: 'quorum-set', + quorum: quorum + }) +} + +/*reaction( + () => { + //return aoStore.state.socketState + }, + socketState => console.log('AO: client/api.ts: socketState: ' + socketState) +) +console.log("NODE_ENV is", process.env.NODE_ENV)*/ + +//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 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/src/ao-lib/crypto.ts b/src/ao-lib/crypto.js similarity index 100% rename from src/ao-lib/crypto.ts rename to src/ao-lib/crypto.js diff --git a/src/ao-lib/settings.js b/src/ao-lib/settings.js new file mode 100644 index 0000000..41f0582 --- /dev/null +++ b/src/ao-lib/settings.js @@ -0,0 +1,120 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import { parse, stringify } from 'envfile' + +export const AO_ENV_FILE_PATH = process.env.HOME + '/.ao/.env' + +function createAoFolderIfDoesNotExist() { + +} + +// Check for an AO env file at ~/.ao/.env and returns true if it exists +export function checkAoEnvFile() { + try { + execSync(`[ -f "${AO_ENV_FILE_PATH}" ]`) + return true + } catch(err) { + return false + } +} + +export function aoEnv(variable) { + let envFileContents = {} + try { + envFileContents = fs.readFileSync(AO_ENV_FILE_PATH) + } catch(err) { + if(err.code === 'ENOENT') { + //console.log('The .env file does not exist, so the requested value', variable, 'is empty.') + } else { + console.log('Unknown error loading .env file in aoEnv, aborting.') + } + return null + } + const parsedFile = parse(envFileContents) + + if(!parsedFile.hasOwnProperty(variable)) { + return null + } + + // Convert ENV idiom to programmatic types + switch(parsedFile[variable]) { + case '1': + case 'true': + case 'TRUE': + case 'yes': + case 'YES': + return true + case '0': + case 'false': + case 'FALSE': + case 'no': + case 'NO': + return false + } + + return parsedFile[variable] +} + +// Sets and saves the given ENV=value to the global ~/.ao/.env file +// If value is null, the env variable will be deleted +// Returns true if a change was made, false if no change was made or if it failed +export function setAoEnv(variable, value) { + createAoFolderIfDoesNotExist() + if(typeof variable !== 'string') { + console.log('ENV variable name must be a string for setAoEnv') + return false + } + + // Convert types to standard ENV file idiom + switch(value) { + case true: + case 'TRUE': + case 'yes': + case 'YES': + value = '1' + break + case false: + case 'FALSE': + case 'no': + case 'NO': + value = '0' + } + + let envFileContents = {} + try { + envFileContents = fs.readFileSync(AO_ENV_FILE_PATH) + } catch(err) { + if(err.code === 'ENOENT') { + console.log('The .env file hasn\'t been created yet, creating.') + } else { + console.log('Unknown error loading .env file in setAoEnv, aborting. Error:', err) + return false + } + } + + const parsedFile = parse(envFileContents) + if(parsedFile[variable] == value) { + console.log(variable, 'is already', value, 'so no change was made.') + return false + } + + if(value === null) { + delete parsedFile[variable] + } else { + parsedFile[variable] = value + } + + const stringified = stringify(parsedFile) + fs.writeFileSync(AO_ENV_FILE_PATH, stringified) + + // Confirm the variable was set in the .env file correctly + if(aoEnv(variable) != value) { + console.log('Value was not saved correctly, sorry.') + return false + } + return true +} + +function setAndSaveEnvironmentVariable(variable, value, path) { + +} diff --git a/src/ao-lib/utils.ts b/src/ao-lib/util.js similarity index 53% rename from src/ao-lib/utils.ts rename to src/ao-lib/util.js index 0e1c471..0ee71a3 100644 --- a/src/ao-lib/utils.ts +++ b/src/ao-lib/util.js @@ -1,3 +1,5 @@ +// General helper functions + export const cancelablePromise = promise => { let isCanceled = false @@ -13,16 +15,15 @@ export const cancelablePromise = promise => { cancel: () => (isCanceled = true), } } + export const noop = () => {} -export const delay = n => new Promise(resolve => setTimeout(resolve, n)) +export const delay = (n = 550) => new Promise(resolve => setTimeout(resolve, n)) -export const isObject = obj => { - return Object.prototype.toString.call(obj) === '[object Object]' -} +export const isObject = obj => { return Object.prototype.toString.call(obj) === '[object Object]' } -export const convertToDuration = (milliseconds: number) => { - const stringifyTime = (time: number): string => String(time).padStart(2, '0') +export const convertToDuration = (milliseconds) => { + const stringifyTime = (time) => String(time).padStart(2, '0') const seconds = Math.floor(milliseconds / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) @@ -31,7 +32,7 @@ export const convertToDuration = (milliseconds: number) => { )}:${stringifyTime(seconds % 60)}` } -export const convertToTimeWorked = (milliseconds: number) => { +export const convertToTimeWorked = (milliseconds) => { const seconds = Math.floor(milliseconds / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) @@ -42,3 +43,15 @@ export const convertToTimeWorked = (milliseconds: number) => { return `${minutes % 60}m` } } + + // Returns a random int between min and max (inclusive) +export function randomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// Returns a random item from the given array +export function selectRandom(arrayToChooseFrom) { + return arrayToChooseFrom[randomInt(0, arrayToChooseFrom.length - 1)] +}