An interactive command-line interface (CLI) tool to help you install, use, and administer an AO instance.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1080 lines
26 KiB

2 years ago
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 =
process.env.NODE_ENV === 'development' ? 'http://' + AO_DEFAULT_HOSTNAME : '/'
export const socket = io(AO_SOCKET_URL, {
autoConnect: false
})
2 years ago
// 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')
2 years ago
// Performs a post request to the specified endpoint, sending the given payload
export function postRequest(endpoint, payload) {
if (!currentSessionToken) {
console.log('Session token not set, API not ready.')
2 years ago
return new Promise(() => null)
}
try {
2 years ago
let partialRequest = request
.post(HOSTNAME + endpoint)
.set('authorization', currentSessionToken)
.set('session', currentSessionId)
2 years ago
if(payload) {
console.log('sending payload', payload)
return partialRequest.send(payload).then(res => {
//console.log("Response completed:", res)
}).catch(err => {
//console.log('response failed:', err)
})
2 years ago
} else {
return partialRequest
}
} catch (err) {
console.log('request failed')
}
2 years ago
}
// Performs a post request to the /event endpoint, sending the given JSON object as the event
export function postEvent(event) {
2 years ago
return postRequest('/event', event)
}
// 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 }
}
2 years ago
export async function logout() {
return await postRequest('/logout')
}
2 years ago
// AO p2p over tor features
export async function nameAo(newName) {
return await postEvent({
type: 'ao-named',
alias: newName
})
}
2 years ago
export async function connectToAo(address, secret) {
return await postEvent({
type: 'ao-outbound-connected',
address: address,
secret: secret
})
}
2 years ago
export async function deleteAoConnection(address) {
return await postEvent({
type: 'ao-disconnected',
address: address
})
}
2 years ago
export async function relayEventToOtherAo(address, event) {
return await postEvent({
type: 'ao-relay',
address: address,
ev: event
})
}
2 years ago
export async function linkCardOnAo(taskId, address) {
return await postEvent({
type: 'ao-linked',
address: address,
taskId: taskId
})
}
2 years ago
// Avatar and presence features
export async function bark() {
return await postEvent({
type: 'doge-barked',
memberId: currentMemberId
})
}
2 years ago
export async function hopped(taskId) {
return await postEvent({
type: 'doge-hopped',
memberId: currentMemberId,
taskId: taskId
})
}
2 years ago
export async function mute() {
return await updateMemberField('muted', true)
}
2 years ago
export async function unmute() {
return await updateMemberField('muted', false)
}
2 years ago
// 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
})
}
2 years ago
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
})
}
2 years ago
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)
}
2 years ago
})
.on('error', err => {
console.log('Upload failed with error:', err)
return false
2 years ago
})
.then(res => {
console.log('sent files. res is', res)
return res
2 years ago
})
}
2 years ago
export async function cacheMeme(taskId) {
return await postEvent({
type: 'meme-cached',
taskId
})
}
2 years ago
// 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
2 years ago
}
if(alsoGetRelevant) {
let relevantResult = await getAllRelevantCards(result.body.taskId, alsoGetRelevant)
2 years ago
}
return result.body.concat(relevantResult.body)
}
2 years ago
// 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<string, Task> 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)
})
}
2 years ago
}
// Filter out the taskIds for tasks we already have
taskIdsToFetch.filter(taskId => {
if (!taskId) {
return false
2 years ago
}
const existingTask = existingTasks.get(taskId)
return !existingTask
})
if (taskIdsToFetch.length < 1) {
return []
2 years ago
}
// Fetch the cards
try {
const result = await postRequest('/fetchTaskByID', { taskId: taskIdsToFetch })
} catch (error) {
console.log('Error fetching relevant tasks:', { taskIdsToFetch, error })
return null
2 years ago
}
// 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
}
2 years ago
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
})
}
2 years ago
export async function setCardProperty(taskId, property, value) {
return await postEvent({
type: 'task-property-set',
taskId: taskId,
property: property,
value: value,
blame: currentMemberId
})
}
2 years ago
// Card send feature
export async function passCard(taskId, toMemberId) {
return await postEvent({
type: 'task-passed',
taskId: taskId,
toMemberId: toMemberId,
fromMemberId: currentMemberId
})
}
2 years ago
export async function remindMember(memberId) {
return await postEvent({
type: 'member-reminded',
toMemberId: memberId,
fromMemberId: currentMemberId
})
}
2 years ago
// Cards-in-cards feature
export async function playCard(from = null, to) {
return await postEvent({
type: 'task-played',
from: from,
to: to,
memberId: currentMemberId
})
}
2 years ago
export async function discardCardFromCard(taskId, inId) {
return await postEvent({
type: 'task-de-sub-tasked',
taskId: taskId,
subTaskId,
blame: currentMemberId
})
}
2 years ago
// Empties a card's priorities and subtasks
export async function emptyCard(taskId) {
return await postEvent({
type: 'task-emptied',
taskId: taskId,
blame: currentMemberId
})
}
2 years ago
export async function swapCard(inId, taskId1, taskId2) {
return await postEvent({
type: 'task-swapped',
taskId: inId,
swapId1: taskId1,
swapId2: taskId2,
blame: currentMemberId
})
}
2 years ago
export async function bumpCard(taskId, inId, direction) {
return await postEvent({
type: 'task-bumped',
taskId: inId,
bumpId: taskId,
direction: direction,
blame: currentMemberId
})
}
2 years ago
// Deck features
export async function grabCard(taskId) {
return await postEvent({
type: 'task-grabbed',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
export async function grabPile(taskId) {
return await postEvent({
type: 'pile-grabbed',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
export async function dropCard(taskId) {
return await postEvent({
type: 'task-dropped',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
export async function removeCards(taskIds) {
return await postEvent({
type: 'tasks-removed',
taskIds: taskIds,
memberId: currentMemberId
})
}
2 years ago
export async function dropPile(taskId) {
return await postEvent({
type: 'pile-dropped',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
// Priority feature
export async function prioritizeCard(taskId, inId, position = 0) {
return await postEvent({
type: 'task-prioritized',
2 years ago
taskId,
inId: inId,
position: position,
blame: currentMemberId
})
}
2 years ago
export async function prioritizePile(inId) {
return await postEvent({
type: 'task-prioritized',
inId: inId
})
}
2 years ago
export async function refocusCard(taskId, inId) {
return await postEvent({
type: 'task-refocused',
taskId: taskId,
inId: inId,
blame: currentMemberId
})
}
2 years ago
export async function refocusPile(inId) {
return await postEvent({
type: 'pile-refocused',
inId: inId
})
}
2 years ago
export async function allocatePriority(inId, taskId, points = 1) {
return await postEvent({
type: 'task-allocated',
taskId: inId,
allocatedId: taskId,
amount: points,
blame: currentMemberId
//inId: inId,
})
}
2 years ago
// Guilds feature
export async function titleMissionCard(taskId, newTitle) {
return await postEvent({
type: 'task-guilded',
taskId: taskId,
guild: newTitle,
blame: currentMemberId
})
}
2 years ago
// Checkmarks feature
export async function completeCard(taskId) {
return await postEvent({
type: 'task-claimed',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
export async function uncheckCard(taskId) {
return await postEvent({
type: 'task-unclaimed',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
export async function setClaimInterval(taskId, newClaimInterval) {
return await setCardProperty(taskId, claimInterval, newClaimInterval)
}
2 years ago
// 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
})
}
2 years ago
export async function useResource(resourceId, amount, charged, notes = '') {
return await postEvent({
type: 'resource-used',
resourceId: resourceId,
memberId: currentMemberId,
amount: amount,
charged: charged,
notes: notes
})
}
2 years ago
export async function stockResource(resourceId, amount, paid, notes = '') {
return await postEvent({
type: 'resource-stocked',
resourceId: resourceId,
memberId: currentMemberId,
amount: amount,
paid: paid,
notes: notes
})
}
2 years ago
export async function purgeResource(resourceId) {
return await postEvent({
type: 'resource-purged',
resourceId: resourceId,
blame: currentMemberId
})
}
2 years ago
export async function bookResource(taskId, startTime, endTime) {
return await postEvent({
type: 'resource-booked',
resourceId: taskId,
memberId: currentMemberId,
startTs: startTime,
endTs: endTime
})
}
2 years ago
// 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
})
}
2 years ago
// Member admin features
export async function createMember(name, fob = '') {
const secret = createHash(name)
return await postEvent({
type: 'member-created',
name,
secret,
fob
})
}
2 years ago
export async function activateMember(memberId) {
return await postEvent({
type: 'member-activated',
memberId: memberId
})
}
2 years ago
export async function deactivateMember(memberId) {
return await postEvent({
type: 'member-deactivated',
memberId: memberId
})
}
2 years ago
// senpai function
export async function resetPassword(memberId) {
return await postEvent({
type: 'member-secret-reset',
kohaiId: memberId,
senpaiId: currentMemberId
})
}
2 years ago
// senpai function
export async function promoteMember(memberId) {
return await postEvent({
type: 'member-promoted',
kohaiId: memberId,
senpaiId: currentMemberId
})
}
2 years ago
// senpai function
export async function banMember(memberId) {
return await postEvent({
type: 'member-banned',
kohaiId: memberId,
senpaiId: currentMemberId
})
}
2 years ago
// senpai function
export async function unbanMember(memberId) {
return await postEvent({
type: 'member-unbanned',
kohaiId: memberId,
senpaiId: currentMemberId
})
}
2 years ago
// senpai function
export async function purgeMember(memberId) {
return await postEvent({
type: 'member-purged',
memberId: memberId,
blame: currentMemberId
})
}
2 years ago
// 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
})
}
2 years ago
// Timeclock features
export async function clockTime(seconds, taskId, date) {
return await postEvent({
type: 'task-time-clocked',
taskId: taskId,
memberId: currentMemberId,
seconds: seconds,
date: date
})
}
2 years ago
export async function startTimeClock(taskId, inId) {
return await postEvent({
type: 'task-started',
taskId: taskId,
inId: inId,
memberId: currentMemberId
})
}
2 years ago
export async function stopTimeClock(taskId) {
return await postEvent({
type: 'task-stopped',
taskId: taskId,
memberId: currentMemberId
})
}
2 years ago
// Group membership features
export async function assignMembership(taskId, memberId, level) {
return await postEvent({
type: 'task-membership',
taskId: taskId,
memberId: memberId,
level: level,
blame: currentMemberId
})
}
2 years ago
export async function stashCard(taskId, inId, level) {
return await postEvent({
type: 'task-stashed',
taskId: taskId,
inId: inId,
level: level,
blame: currentMemberId
})
}
2 years ago
export async function unstashCard(taskId, inId, level) {
return await postEvent({
type: 'task-unstashed',
taskId: taskId,
inId: inId,
level: level,
blame: currentMemberId
})
}
2 years ago
// 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
})
}
2 years ago
/*
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)
}
*/
2 years ago
// 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
})
}
2 years ago
export async function createCardWithGrid(name, height, width) {
return await postEvent({
type: 'grid-created',
name: name,
height: height,
width: width,
color: 'blue',
deck: [currentMemberId]
})
}
2 years ago
export async function addGridToCard(taskId, height, width, spread = 'pyramid') {
return await postEvent({
type: 'grid-added',
taskId: taskId,
spread: spread,
height: height,
width: width
})
}
2 years ago
export async function removeGridFromCard(taskId) {
return await postEvent({
type: 'grid-removed',
taskId: taskId
})
}
2 years ago
// 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)
}
2 years ago
export async function requestBtcQr(taskId) {
return await postEvent({
type: 'address-updated',
taskId
})
}
2 years ago
export async function requestLightningInvoice(taskId, amount = 0) {
return await postEvent({
type: 'invoice-created',
taskId,
amount: amount
})
}
2 years ago
// Proposals features
export async function signCard(taskId, opinion = 1) {
return await postEvent({
type: 'task-signed',
taskId: taskId,
memberId: currentMemberId,
opinion: opinion
})
}
2 years ago
export async function setQuorum(quorum) {
return await postEvent({
type: 'quorum-set',
quorum: quorum
})
}
2 years ago
/*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
}
})
2 years ago
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')
2 years ago
this.fetchState().then(() => {
2 years ago
runInAction(() => {
aoStore.state.socketState = 'authenticationSuccess'
2 years ago
})
})
this.socket.on('eventstream', ev => {
console.log('AO: client/api.ts: socketListener: event:', ev)
2 years ago
aoStore.applyEvent(ev)
2 years ago
})
})
this.socket.on('disconnect', reason => {
console.log('disconnected')
2 years ago
runInAction(() => {
aoStore.state.socketState = 'authenticationFailed'
2 years ago
})
this.socket.connect()
})
}*/
2 years ago
/*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 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,
})
}*/