deicidus
2 years ago
20 changed files with 446 additions and 1390 deletions
@ -0,0 +1,3 @@
|
||||
[submodule "ao-lib"] |
||||
path = ao-lib |
||||
url = http://git.coalitionofinvisiblecolleges.org:3009/autonomousorganization/ao-lib.git |
@ -1,30 +0,0 @@
|
||||
import crypto from 'crypto' // Does not work on client because this is a Node library, but works for below server-only functions
|
||||
// These libraries are old but they work and can be included on both server and client
|
||||
import shajs from 'sha.js' |
||||
import hmac from 'hash.js/lib/hash/hmac.js'
|
||||
import sha256 from 'hash.js/lib/hash/sha/256.js' // Only works for shorter hashes, not in createHash used for hashing meme files
|
||||
|
||||
export function createHash(payload) { |
||||
return shajs('sha256').update(payload).digest('hex') |
||||
} |
||||
|
||||
export function hmacHex(data, signingKey) { |
||||
return hmac(sha256, signingKey).update(data).digest('hex') |
||||
} |
||||
|
||||
export function derivePublicKey(p) { |
||||
return crypto.createPublicKey(p).export({ |
||||
type: 'spki', |
||||
format: 'pem', |
||||
}) |
||||
} |
||||
|
||||
export function encryptToPublic(pub, info) { |
||||
return crypto.publicEncrypt(pub, new Buffer(info)).toString('hex') |
||||
} |
||||
|
||||
export function decryptFromPrivate(priv, hiddenInfo) { |
||||
return crypto |
||||
.privateDecrypt(priv, Buffer.from(hiddenInfo, 'hex')) |
||||
.toString('latin1') |
||||
} |
@ -0,0 +1,178 @@
|
||||
// View and create cards with the .subtask of other cards, which is an array of taskIds
|
||||
import { headerStyle } from './styles.js' |
||||
import { aoEnv } from '../ao-lib/settings.js' |
||||
import { getCard, playCard, completeCard, uncheckCard, discardCardFromCard, prioritizeCard, grabCard, dropCard } from '../ao-lib/api.js' |
||||
import { getNewHighestEchelonScore } from './priority.js' |
||||
import { createCardInteractive } from './cards.js' |
||||
import { promptMenu } from './welcome.js' |
||||
import { tagCardInteractive, viewTagMenu } from './tags.js' |
||||
|
||||
// Displays the subtasks of the given taskId in a menu. Selecting a card shows a menu for that card. If taskId is null, member card is used.
|
||||
// The terms tasks and cards are used mostly interchangeably in the code. For the user, 'subcards' is preferred for clarity/generality,
|
||||
// but can be used to refer either to the .subTasks or all of the cards in another card (including subTasks, priorities, completed, and pinned)
|
||||
export async function subcardsMenu(taskId = null, previousIndex = null) { |
||||
console.log(`\n${headerStyle('Cards in My Hand')}`) |
||||
let subtaskChoices = [] |
||||
|
||||
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') |
||||
if(!memberId) { |
||||
console.log('Not logged in.') |
||||
return false |
||||
} |
||||
if(!taskId) { |
||||
// Get the subtasks of my member card
|
||||
taskId = memberId |
||||
} |
||||
const fetchedCards = await getCard(taskId, 'subcards') // will fetch both priorities and subtasks in one array
|
||||
if(!fetchedCards || fetchedCards.length < 1) { |
||||
console.log('Failed to fetch the specified card, this is bad.') |
||||
return false |
||||
} |
||||
const card = fetchedCards[0] // first card is the requested card itself
|
||||
// Separate fetched cards into correct ordered list of priorities and subtasks
|
||||
let priorityCards = card.priorities.map((priorityTaskId, i) => { |
||||
const priorityCard = fetchedCards.find(p => p.taskId === priorityTaskId) |
||||
if(!priorityCard) { |
||||
return 'Missing card, repair your database' |
||||
} |
||||
return priorityCard |
||||
}) |
||||
priorityCards.reverse() |
||||
const subtaskCards = card.subTasks.map((subtaskTaskId, i) => { |
||||
const subtaskCard = fetchedCards.find(st => st.taskId === subtaskTaskId) |
||||
if(!subtaskCard) { |
||||
return 'Missing card, repair your database' |
||||
} |
||||
return subtaskCard |
||||
}) |
||||
console.log('There are', subtaskCards.length, 'subcards in this card') |
||||
subtaskChoices = subtaskCards.map((subtaskCard, i) => { |
||||
const shortenedName = subtaskCard.name.substring(0, 70) + (subtaskCard.name.length >= 70 ? '...' : '') |
||||
return { |
||||
title: shortenedName, |
||||
value: { index: i, card: subtaskCard }, |
||||
short: shortenedName |
||||
} |
||||
}) |
||||
subtaskChoices.push( |
||||
{ title: 'Play card here', value: 'create_here', short: 'new card' }, |
||||
{ title: 'Back Up', value: false, short: 'back' } |
||||
) |
||||
const cardName = taskId === memberId ? 'My Hand' : card.name.substring(0, 70).toTitleCase() |
||||
const answer = await promptMenu(subtaskChoices, 'Cards in ' + cardName, undefined, previousIndex) |
||||
switch(answer) { |
||||
case false: |
||||
return false |
||||
case 'create_here': |
||||
let previousCardCreatedText |
||||
do { |
||||
previousCardCreatedText = await createCardInteractive() |
||||
console.log('done with round') |
||||
} while(previousCardCreatedText != '\n') |
||||
console.log('returning true') |
||||
return answer.index |
||||
case 'Missing card, repair your database': |
||||
console.log('Database repair yet implemented, sorry.') |
||||
return answer.index |
||||
} |
||||
if(answer === false) { |
||||
return previousIndex |
||||
} |
||||
let chosenTask = answer.card |
||||
const chosenTaskId = chosenTask.taskId |
||||
let previousAnswer |
||||
do { |
||||
previousAnswer = await subtaskCardMenu(chosenTask, answer.index, taskId, priorityCards) // send priorities for echelon info in case they upboat
|
||||
if(previousAnswer) { |
||||
const fetchedCards = await getCard(chosenTaskId, false) |
||||
if(!fetchedCards || fetchedCards.length < 1) { |
||||
console.log('The card has disappeared. Maybe it was deleted, or cards held by no one are automatically cleaned up every five minutes.') |
||||
return false |
||||
} |
||||
chosenTask = fetchedCards[0] |
||||
} |
||||
} while(previousAnswer !== false) |
||||
return answer.index |
||||
} |
||||
|
||||
// Short action-oriented menu for cards in the subtasks list
|
||||
// Index is the position of the card in the list that it is in, used for fencepost case to display upboat contextually
|
||||
// inId is the taskId of the parent card that we are in contextually as we look at the given card in its list
|
||||
// allPriorities is an array of task objects for the other priorities in the .priorities for the card this card is in (adjacent to this card)
|
||||
async function subtaskCardMenu(card, index, inId, allPriorities) { |
||||
if(!card) { |
||||
console.log('subtaskCardMenu: card is required.') |
||||
return false |
||||
} |
||||
const taskId = card.taskId |
||||
const memberId = aoEnv('AO_CLI_SESSION_MEMBERID') |
||||
if(!memberId) { |
||||
console.log('Not logged in.') |
||||
return false |
||||
} |
||||
const isChecked = card.claimed.includes(memberId) |
||||
const guild = card.guild === true ? card.name.toTitleCase() : card.guild || false |
||||
let subtaskChoices = [] |
||||
const isInDeck = card.deck.includes(memberId) |
||||
if(!isInDeck) { |
||||
subtaskChoices.push({ title: 'Grab card (add to my deck)', value: 'grab', short: 'grab card' }) |
||||
} |
||||
subtaskChoices.push( |
||||
{ title: 'Discard from hand (downboat)', value: 'downboat', short: 'downboat' }, |
||||
{ title: 'Prioritize (upboat)', value: 'upboat', short: 'upboat' }, |
||||
{ title: isChecked ? 'Uncheck' : 'Check off', value: 'check', short: 'check!' }, |
||||
{ title: guild ? 'Tag: ' + guild : 'Tag', value: 'tag', short: 'tag' } |
||||
) |
||||
if(guild) { |
||||
subtaskChoices.push({ title: 'View tag', value: 'view_tag', short: 'view tag' }) |
||||
} |
||||
if(isInDeck) { |
||||
subtaskChoices.push({ title: 'Remove from my deck', value: 'drop', short: 'drop card' }) |
||||
} |
||||
subtaskChoices.push( |
||||
{ title: 'Browse within', value: 'browse', short: 'browse' }, |
||||
{ title: 'Back Up', value: false, short: 'back' } |
||||
) |
||||
const answer = await promptMenu(subtaskChoices, 'Card: ' + card.name) |
||||
switch(answer) { |
||||
case 'grab': |
||||
await grabCard(taskId) |
||||
break |
||||
case 'drop': |
||||
await dropCard(taskId) |
||||
break |
||||
case 'check': |
||||
if(isChecked) { |
||||
await uncheckCard(taskId) |
||||
} else { |
||||
await completeCard(taskId) |
||||
} |
||||
break |
||||
case 'tag': |
||||
await tagCardInteractive(card) |
||||
break |
||||
case 'view_tag': |
||||
while(await viewTagMenu(card.guild)) |
||||
break |
||||
case 'upboat': |
||||
console.log('upboat') |
||||
const { newPosition, newEchelonScore } = getNewHighestEchelonScore(card.echelon, allPriorities) |
||||
//console.log('newPosition is', newPosition, 'and newEchelonScore is', newEchelonScore)
|
||||
await prioritizeCard(taskId, inId, newPosition, newEchelonScore) |
||||
return false |
||||
case 'downboat': |
||||
console.log(taskId, inId, 'discard') |
||||
await discardCardFromCard(taskId, inId) |
||||
return false |
||||
case 'browse': |
||||
let previousAnswer |
||||
do { |
||||
previousAnswer = await subcardsMenu(taskId, previousAnswer) // undefined taskId defaults to member card
|
||||
} while(previousAnswer !== false) {} |
||||
break |
||||
break |
||||
default: |
||||
return false |
||||
} |
||||
return true |
||||
} |
@ -1,120 +0,0 @@
|
||||
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) { |
||||
|
||||
} |
@ -0,0 +1,157 @@
|
||||
// Also called guilds, groups, or the title of a card, the .guild field of a card can hold a string that is 'about' the card, or true (and the tag is then the card .name itself)
|
||||
// Right now one tag can be added to each card. Maybe it should be multiple, with tag #1 being the main/title tag
|
||||
// When viewing a card, it should show the tag in the menu, or if not tagged, the option to tag it (with setTagInteractive)
|
||||
// If the tag is set, an option should appear to view the tag (menu)
|
||||
// When viewing a tag, it should be possible tag the tag. This creates the tag as a card with the new tag as its tag
|
||||
// When viewing a tag, it should be possible to view all the cards tagged by that tag
|
||||
// Because tags are implemented as part of cards, to put a tag in another tag, we make a card for that tag and tag it (give it a .guild)
|
||||
import { askQuestionText, promptMenu } from './welcome.js' |
||||
import { tagCard, getCardsForTag, setCardProperty, getCardByName, createCard } from '../ao-lib/api.js' |
||||
|
||||
// Tells the user the current guild/tag of the specified card, and allows them to set a new one (or blank to leave unchanged)
|
||||
export async function tagCardInteractive(card) { |
||||
if(card.guild === true) { |
||||
} else if(card.guild) { |
||||
console.log('This card\'s tag is:') |
||||
console.log(card.guild) |
||||
} else { |
||||
console.log('This card is not yet tagged.') |
||||
} |
||||
const answer = await askQuestionText('Type new tag (or Enter/ESC to cancel):') |
||||
if(!answer || answer === 'ESC' || answer == '') { |
||||
return |
||||
} |
||||
await tagCard(card.taskId, answer) |
||||
} |
||||
|
||||
// The tags index displays the tag of every card with a tag, sorted by echelon, then by we'll see!
|
||||
export async function tagsMenu() { |
||||
const allTaggedCards = await getCardsForTag('*') |
||||
|
||||
// Build a pure list of tags (since we don't do this in the database)
|
||||
const tags = {} |
||||
allTaggedCards.forEach(card => { |
||||
const guild = card.guild === true ? card.name.toTitleCase() : card.guild |
||||
if(guild.includes('test')) console.log('guild:', guild, 'card is', card) |
||||
//console.log('guild is', guild)
|
||||
// Skip any card that is within another tagged card or tagged with the name of another tag
|
||||
if(allTaggedCards.some(t => { |
||||
if(t.taskId === card.taskId) return false |
||||
if(t.priorities.concat(t.subTasks, t.completed).includes(card.taskId)) { |
||||
if(guild.includes('test')) console.log('reason 1, t is', t) |
||||
|
||||
return true |
||||
} |
||||
if(t.pins && t.pins.some(pin => pin.taskId === card.taskId)) { |
||||
if(guild.includes('test')) console.log('reason 2') |
||||
|
||||
return true |
||||
} |
||||
if(card.taskId !== t.taskId && guild === card.name && card.guild && (card.echelon || 0) < (t.echelon || 0)) { |
||||
if(guild.includes('test')) console.log('reason 3') |
||||
|
||||
return true |
||||
} |
||||
return false |
||||
})) { |
||||
return |
||||
} |
||||
|
||||
if(!tags.hasOwnProperty(guild)) { |
||||
tags[guild] = card.echelon |
||||
return |
||||
} |
||||
if(!tags[guild] || tags[guild] < card.echelon) { |
||||
tags[guild] = card.echelon |
||||
} |
||||
}) |
||||
|
||||
let tagChoices = Object.entries(tags).sort((entryA, entryB) => { |
||||
return (entryB[1] || 0) - (entryA[1] || 0) |
||||
}) |
||||
|
||||
tagChoices = tagChoices.map((entry, i) => { |
||||
const [tag, echelon] = entry |
||||
const paddedEchelon = echelon >= 0 ? echelon.toString().padEnd(4) : ' ' |
||||
return { title: paddedEchelon + tag, value: tag, short: 'view tag' } |
||||
}) |
||||
tagChoices.push({ title: 'Back to Deck Menu', value: false, short: 'back' }) |
||||
const answer = await promptMenu(tagChoices, 'All Tags') |
||||
console.log('answer:', answer) |
||||
if(!answer) { |
||||
return false |
||||
} |
||||
await viewTagMenu(answer, allTaggedCards) |
||||
return true |
||||
} |
||||
|
||||
// Prints the menu for viewing a single tag, with options to view cards tagged by the tag, or to tag the tag
|
||||
export async function viewTagMenu(tag, allTaggedCards) { |
||||
const tagChoices = [ |
||||
{ title: 'Prioritize tag', value: 'upboat', short: 'prioritize tag' }, |
||||
{ title: 'Cards tagged \'' + tag + '\'', value: 'tagged_cards', short: 'view tagged cards' }, // (current) deck? (60)
|
||||
{ title: 'Tag this tag', value: 'tagtag' }, // hand? (7) (add #s in parens)
|
||||
'Back to Tags Index' |
||||
] |
||||
const answer = await promptMenu(tagChoices, 'My Deck') |
||||
switch(answer) { |
||||
case 'upboat': |
||||
await tagToTopEchelon(tag, allTaggedCards) |
||||
return false |
||||
case 'tagged_cards': |
||||
while(await cardsTaggedByMenu(tag)) {} |
||||
break |
||||
case 'tagtag': |
||||
await tagTagInteractive(tag, allTaggedCards) |
||||
break |
||||
default: |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// Finds the first card with the given tag and sets its echelon score to the highest + 1 of all tagged cards
|
||||
async function tagToTopEchelon(tag, allTaggedCards) { |
||||
let highestEchelonScore = 0 |
||||
allTaggedCards.forEach(card => { |
||||
if(card.echelon > highestEchelonScore) { |
||||
highestEchelonScore = card.echelon |
||||
} |
||||
}) |
||||
highestEchelonScore++ |
||||
const firstCardWithTag = allTaggedCards.find(card => card.guild === tag || (card.guild === true && card.name.toTitleCase() === tag)) |
||||
if(!firstCardWithTag) { |
||||
console.log('Could not find the tag you just selected, this is bad') |
||||
return |
||||
} |
||||
await setCardProperty(firstCardWithTag.taskId, 'echelon', highestEchelonScore) |
||||
return |
||||
} |
||||
|
||||
// Prints an interactive list of all cards tagged by the given tag, that can be browsed just like in the priorities and deck browsers
|
||||
async function cardsTaggedByMenu(tag) { |
||||
} |
||||
|
||||
// Allows the user to tag a tag by making a new card for the tag that contains the tagged card
|
||||
// A card can have its .guild set to true, in which case the guild is the card .name
|
||||
// Returns true if the tag was tagged/categorized
|
||||
export async function tagTagInteractive(tag, allTaggedCards) { |
||||
console.log('Tag: ', tag) |
||||
const answer = await askQuestionText('Type a tag to categorize (or Enter/ESC to cancel):') |
||||
if(!answer || answer === 'ESC' || answer == '') { |
||||
return false |
||||
} |
||||
// Make a card for the tag if it doesn't already exist
|
||||
let guildCard = (await getCardByName(tag, false))[0] |
||||
if(!guildCard) { |
||||
await createCard(tag) |
||||
guildCard = (await getCardByName(tag, false))[0] |
||||
if(!guildCard) { |
||||
console.log('Failed to create tag card, sorry.') |
||||
return false |
||||
} |
||||
} |
||||
await setCardProperty(guildCard.taskId, 'guild', true) |
||||
await tagToTopEchelon(answer, allTaggedCards) |
||||
return await tagCard(guildCard.taskId, answer) |
||||
} |
@ -1,20 +0,0 @@
|
||||
// General helper functions
|
||||
|
||||
// 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)] |
||||
} |
||||
|
||||
// Waits for the given number of milliseconds (or a brief pause by default)
|
||||
export function sleep(ms = 550) { |
||||
return new Promise((r) => setTimeout(r, ms)) |
||||
} |
||||
|
||||
export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' |
Loading…
Reference in new issue