Browse Source

switched common files to separate git project ao\-lib included via subtree

main
deicidus 2 years ago
parent
commit
deb372409c
  1. 122
      src/calculations.ts
  2. 798
      src/cards.ts
  3. 30
      src/crypto.ts
  4. 100
      src/members.ts
  5. 2
      src/modules/ao.ts
  6. 2
      src/modules/cash.ts
  7. 2
      src/modules/members.ts
  8. 2
      src/modules/memes.ts
  9. 2
      src/modules/resources.ts
  10. 2
      src/modules/sessions.ts
  11. 2
      src/modules/tasks.ts
  12. 1549
      src/mutations.ts
  13. 132
      src/semantics.ts
  14. 2
      src/server/auth.ts
  15. 6
      src/server/database.ts
  16. 2
      src/server/files.ts
  17. 2
      src/server/link.ts
  18. 6
      src/server/router.ts
  19. 4
      src/server/spec.ts
  20. 2
      src/server/state.ts
  21. 2
      src/server/validators.ts
  22. 351
      src/types.ts
  23. 44
      src/utils.ts

122
src/calculations.ts

@ -1,122 +0,0 @@
const satsPerBtc = 100000000 // one hundred million per btc
import { createHash } from './crypto.js'
import { Task } from './types.js'
export function crawlerHash(tasks: Task[], taskId: string) {
const crawlerResults = crawler(tasks, taskId)
let buffers: number[] = []
crawlerResults.forEach(tId => {
buffers.push(...Buffer.from(tId))
})
const bufferResult = Buffer.from(buffers)
return createHash(bufferResult)
}
export function crawler(tasks: Task[], taskId: string): string[] {
let history: string[] = []
tasks.forEach(task => {
if (task.taskId === taskId) {
let crawler: string[] = [taskId]
do {
let newCards: string[] = []
crawler.forEach(t => {
if (history.indexOf(t) >= 0) return
history.push(t)
let subTask = tasks.filter(pst => pst.taskId === t)[0]
if (subTask) {
let gridCells =
subTask.pins && subTask.pins.length >= 1
? subTask.pins.map(pin => pin.taskId)
: []
newCards = newCards
.concat(subTask.subTasks)
.concat(subTask.priorities)
.concat(subTask.completed)
.concat(gridCells)
}
})
crawler = newCards
} while (crawler.length > 0)
}
})
return history
}
export function shortName(name) {
let limit = 280
let shortened = name.substring(0, limit)
if (name.length > limit) {
shortened += 'โ€ฆ'
}
return shortened
}
export function cardColorCSS(color) {
return {
redwx: color == 'red',
bluewx: color == 'blue',
greenwx: color == 'green',
yellowwx: color == 'yellow',
purplewx: color == 'purple',
blackwx: color == 'black',
}
}
export function isString(x) {
return Object.prototype.toString.call(x) === '[object String]'
}
export function cadToSats(cadAmt, spot) {
let sats = (parseFloat(cadAmt) / parseFloat(spot)) * satsPerBtc
return Math.floor(sats)
}
export function satsToCad(sats, spot) {
let cad = sats * (spot / satsPerBtc)
return cad.toFixed(2)
}
export function calculateMsThisMonth() {
let today = new Date()
let daysThisMonth = new Date(
today.getFullYear(),
today.getMonth(),
0
).getDate()
return daysThisMonth * 24 * 60 * 60 * 1000
}
export function getMeridienTime(ts) {
let d = new Date(parseInt(ts))
let hour24 = d.getHours()
let rollover = 0
if (hour24 >= 24) {
rollover = 1
hour24 %= 24
}
let hour, meridien
if (hour24 > 12) {
meridien = 'pm'
hour = hour24 - 12
} else {
meridien = 'am'
hour = hour24
}
let date = d.getDate() + rollover
let month = d.getMonth() + 1
let minute = d.getMinutes()
let year = d.getFullYear()
let weekday = d.toString().slice(0, 3)
return { weekday, year, month, date, hour, minute, meridien }
}
export function toTitleCase(str) {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
})
}

798
src/cards.ts

@ -1,798 +0,0 @@
import { isString } from './calculations.js'
import { v1 } from 'uuid'
import {
Task,
Signature,
UserSeen,
CardZone,
Coords,
CardLocation,
Pinboard,
PinboardStyle,
CardPass,
} from './types.js'
// With this set to 1, actions will occur immediately, as if they were not potential-based actions
export const POTENTIALS_TO_EXECUTE = 1
// Default style when a new pinboard is created without specifiying the style
const defaultPinboardStyle: PinboardStyle = 'pyramid'
// Grid squares have a width measure in ems. The default number for this is 9 and the +/- buttons increase or decrease this by 1.
const defaultSquareSizeInEms = 9
// The one and only function to create a new blank card, please use it everywhere cards are created for standardization and searchability.
export function blankCard(
taskId,
name,
color,
created,
deck = [],
parents = [],
height = undefined,
width = undefined
): Task {
let newCard = {
taskId,
color,
deck,
name: typeof name !== 'string' ? 'invalid filename' : name.trim(),
address: '', // Could be left undefined by default?
bolt11: '', // Could be left undefined by default?
book: undefined, // Could be left undefined by default?
boost: 0,
priorities: [],
subTasks: [],
completed: [],
pinboard:
height && width && height >= 1 && width >= 1
? blankPinboard(height, width)
: null,
pins: [],
parents: parents,
claimed: [],
passed: [],
signed: [], // Could be left undefined by default?
guild: false,
created: created,
lastClaimed: 0,
payment_hash: '', // Could be left undefined by default?
highlights: [],
seen: deck.length >= 1 ? [{ memberId: deck[0], timestamp: created }] : [],
time: [],
allocations: [],
}
return newCard
}
// Returns a blank pinboard of the default style
export function blankPinboard(
height = 3,
width = 3,
spread = defaultPinboardStyle
): Pinboard {
const newGrid = {
spread: spread,
height: height,
width: width,
size: defaultSquareSizeInEms,
}
return newGrid
}
// Version of this function for the server, merge with above
export function allReachableHeldParentsServer(tasks, origin, memberId) {
if (!origin?.hasOwnProperty('taskId')) {
return []
}
let queue = [origin]
let reachableCards = []
let visited = {}
visited[origin.taskId] = true
let i = 0
while (queue.length >= 1) {
let task = queue.pop()
if (
task === undefined ||
task.subTasks === undefined ||
task.priorities === undefined ||
task.completed === undefined
) {
console.log('Invalid task found during returned cards search, skipping.')
continue
}
if (task.deck.indexOf(memberId) < 0 && task.taskId !== memberId) {
continue
}
reachableCards.push(task)
if (task.hasOwnProperty('parents') && task.parents.length >= 1) {
let parents = tasks.filter(taskItem =>
task.parents.includes(taskItem.taskId)
)
parents.forEach(st => {
if (!st.hasOwnProperty('taskId')) {
console.log('Missing parent found during returned cards search.')
return
}
if (!visited.hasOwnProperty(st.taskId)) {
visited[st.taskId] = true
queue.push(st)
}
})
}
}
return reachableCards
}
let dupesGlossary = {}
// Adds a synonym taskId for another taskId to the duplicates glossary
export function registerDuplicateTaskId(originalTaskId, duplicateTaskId) {
if (!dupesGlossary[duplicateTaskId]) {
dupesGlossary[duplicateTaskId] = originalTaskId
console.log(Object.keys(dupesGlossary).length, 'cards with duplicates')
}
}
// Returns the task with the given taskId from the given list of tasks, or null
// Uses the duplicates glossary to return synonymous tasks that were created by junk task-created events
export function getTask(tasks, taskId) {
// Look up duplicate tasks in the duplicates glossary to politely overlook duplicate task-created mutations
let loops = 0
while (dupesGlossary[taskId] && loops < 4) {
taskId = dupesGlossary[taskId]
//console.log("Looked up duplicate task:", taskId, " (", Object.keys(dupesGlossary).length, " duplicated)")
loops++
}
if (loops >= 4) {
console.log(
'Woah, four or more redirects in the duplicates glossary, something weird'
)
}
return tasks.find(task => {
if (task.taskId === taskId) {
return task
}
})
}
// Returns the first task that exactly matches the given value of the given property
// Todo: move property to be the first argument
export function getTaskBy(tasks, value, property) {
return tasks.find(task => {
if (task[property] === value) {
return task
}
})
}
// Returns true if the given taskId matches an existing task/card in the state/database
// The state should match the database exactly,
// because it is sourced from the database via the deterministic event mutations in mutations.js
export function taskExists(tasks, taskId) {
return tasks.some(task => task.taskId === taskId)
}
// Marks the task as seen by the given memberId
export function seeTask(task, memberId) {
if (!task.seen) {
task.seen = []
}
task.seen = task.seen.filter(seenObject => seenObject.memberId !== memberId)
task.seen.push({ memberId: memberId, timestamp: Date.now() })
}
// Clears any pending passes to the specified memberId from the task
// This is done whenever a member accepts a pass or grabs or handles a card
export function clearPassesTo(tasks, task, memberId, alsoClearFrom = false) {
const lengthBefore = task.passed.length
task.passed = task.passed.filter(
d => d[1] !== memberId || (alsoClearFrom ? d[0] !== memberId : false)
)
if (lengthBefore != task.passed.length) {
const memberCard = getTask(tasks, memberId)
changeGiftCount(memberCard, task.passed.length - lengthBefore)
}
}
// Takes a member card and increases or decreases its .giftCount property, adding it if necessary
export function changeGiftCount(memberCard, amount) {
if (!memberCard) {
return
}
if (!memberCard.hasOwnProperty('giftCount')) {
memberCard.giftCount = 0
}
memberCard.giftCount += amount
if (memberCard.giftCount < 0) {
memberCard.giftCount = 0
}
}
// Grabs a task, adding it to the member's deck
// The associated pending pass on the card, if any, will be removed
// You cannot grab your own member card
export function grabTask(tasks, task, memberId) {
clearPassesTo(tasks, task, memberId)
if (memberId && task.deck.indexOf(memberId) === -1) {
if (task.taskId !== memberId) {
task.deck.push(memberId)
}
}
}
// Drops the task, removing it from the member's deck
export function dropTask(task, memberId) {
task.deck = task.deck.filter(d => d !== memberId)
}
// Adds the given taskId to list of the card's parents (this is a cache)
export function addParent(task, parentId) {
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) {
task.parents = []
}
if (!task.parents.some(pId => pId === parentId)) {
task.parents.push(parentId)
}
}
// Removes the given taskId from the list of the card's parents
// This function seems to make no sense
export function removeParent(task, parentId) {
if (!task.hasOwnProperty('parents') || !Array.isArray(task.parents)) {
return
}
let gridCells = []
if (task.pins && task.pins.length >= 1) {
gridCells = task.pins.map(pin => pin.taskId)
}
let stashItems: string[] = []
if (task.stash && Object.keys(task.stash).length >= 1) {
stashItems = [...Object.values<string>(task.stash)]
}
const allSubTasks = [
...task.priorities,
...task.subTasks,
...gridCells,
...task.completed,
...stashItems,
]
if (!allSubTasks.some(stId => stId === parentId)) {
task.parents = task.parents.filter(tId => tId !== parentId)
}
}
// Removes the second card from the first card's list of parents,
// unless the card is actuall still a parent
export function removeParentIfNotParent(task, parent) {
if (
!task.hasOwnProperty('parents') ||
!Array.isArray(task.parents) ||
task.parents.length < 1
) {
return
}
let gridCells = []
if (parent.pins && parent.pins.length >= 1) {
gridCells = parent.pins.map(pin => pin.taskId)
}
let stashItems: string[] = []
if (parent.stash && Object.keys(parent.stash).length >= 1) {
stashItems = [...Object.values<string>(parent.stash)]
}
const allSubTasks = [
...parent.priorities,
...parent.subTasks,
...gridCells,
...parent.completed,
...stashItems,
]
if (!allSubTasks.some(stId => stId === task.taskId)) {
task.parents = task.parents.filter(tId => tId !== parent.taskId)
}
}
// Removes the given taskId from the priorities, subTasks, and completed of the given task
// Does NOT remove the taskId from the grid
export function filterFromSubpiles(task, taskId) {
const start = [
task?.priorities?.length || null,
task?.subTask?.length || null,
task?.completed?.length || null,
]
discardPriority(task, taskId)
discardSubTask(task, taskId)
discardCompletedTask(task, taskId)
if (
(start[0] !== null && start[0] - task?.priorities?.length > 0) ||
(start[1] !== null && start[1] - task?.subTasks?.length > 0) ||
(start[2] !== null && start[2] - task?.completed?.length > 0)
) {
return true
}
return false
}
// Marks as unseen (clears seen from) the given task, unless it's on the list
// Unseens bubble-up one level, but if you've seen the child, it doesn't affect you
export function clearSeenExcept(task, exceptionMemberIds: UserSeen[] = []) {
if (task?.seen?.length >= 1) {
task.seen = task.seen.filter(userseen =>
exceptionMemberIds.includes(userseen.memberId)
)
}
}
// Re-adds the given taskId to the given card's subTasks (moving it to the end)
// This will move it to the top/front of the list of cards in the GUI
// Precondition: The subtask referred to by subTaskId exists (otherwise it will create a broken card link / missing reference)
export function addSubTask(task, subTaskId) {
if (!task) {
console.log(
'Attempting to add a subtask to a missing task, this should never happen'
)
return
}
discardSubTask(task, subTaskId)
task.subTasks.push(subTaskId)
}
// Removes the given discardTaskId from the given task's subtasks
function discardSubTask(task, discardTaskId) {
if (!task || !discardTaskId || !task.subTasks || task.subTasks.length <= 0)
return
task.subTasks = task.subTasks.filter(stId => stId !== discardTaskId)
}
// Removes the given discardTaskId from the given task's completed tasks list
function discardCompletedTask(task, discardTaskId) {
if (!task || !discardTaskId || !task.completed || task.completed.length <= 0)
return
task.completed = task.completed.filter(stId => stId !== discardTaskId)
}
// Adds a completed task to the completed list in a card or moves it to the top of the list
function addCompletedTask(task, completedTaskId) {
discardCompletedTask(task, completedTaskId)
task.completed.push(completedTaskId)
}
// Adds the subTask to the given new parent task and adds the parent as a parent to the new subTask
export function putTaskInTask(subTask, inTask) {
addSubTask(inTask, subTask.taskId)
addParent(subTask, inTask.taskId)
}
// Re-adds the given taskId to the given card's priorities (moving it to the end)
// This will move it to the top/front of the list of cards in the GUI
export function addPriority(task, taskId) {
discardPriority(task, taskId)
task.priorities.push(taskId)
}
// Removes the given discardTaskId from the given task's subtasks
function discardPriority(task, discardTaskId) {
if (
!task ||
!discardTaskId ||
!task.priorities ||
task.priorities.length <= 0
)
return
task.priorities = task.priorities.filter(stId => stId !== discardTaskId)
}
export function unpinTasksOutOfBounds(tasks, task) {
if (!tasks || !task || !task.pins || task.pins.length <= 0) {
return
}
const vertLimit = task.pinboard.spread === 'rune' ? 1 : task.pinboard.height
for (let i = task.pins.length - 1; i >= 0; i--) {
const pin = task.pins[i]
const { taskId, y, x } = pin
const horizLimit =
task.pinboard.spread === 'pyramid' ? y + 1 : task.pinboard.width
if (x >= horizLimit || y >= vertLimit) {
const theSubTask = getTask(tasks, taskId)
unpinTaskFromTask(task, { y: y, x: x })
if (theSubTask) {
putTaskInTask(theSubTask, task)
} else {
console.log('A missing card was removed from the pinboard:', taskId)
}
}
}
}
// Unpins the card from the given coordinates in a card and returns its taskId
function unpinTaskFromTask(task, coords) {
let result
if (!task.pins || task.pins.length <= 0) {
return null
}
task.pins.some((pin, i) => {
const { pinId, y, x } = pin
if (y == coords.y && x == coords.x) {
result = task.pins.splice(i, 1)[0]
}
})
return result
}
// Precondition: The spec should validate whether this is a legal move based upon the current gridStyle of the card
// In other words, this function does not check if the coordinates are off the side of the pinboard
// Unlike the functions to add subtasks, this function does NOT attempt to filter the pinboard before adding a card,
// so duplicates will occur unless you unpin first from the origin coords
// However, it WILL check where the card is going to be placed, and if a card is already there, that card will drop into .subTasks
function pinTaskToTask(task, taskId, coords) {
if (!task.hasOwnProperty('pins') || !Array.isArray(task.pins)) {
task.pins = []
}
// If this taskId is already at this location, do nothing
if (
task.pins.some(
pin => pin.taskId === taskId && pin.y === coords.y && pin.x === coords.x
)
) {
return
}
// If there is already something pinned there, drop it into subTasks
const previousPinnedTaskId = unpinTaskFromTask(task, coords)?.taskId
if (previousPinnedTaskId) {
addSubTask(task, previousPinnedTaskId)
}
task.pins.push({ taskId, y: coords.y, x: coords.x })
}
function putTaskInTaskZone(task, inTask, toLocation) {
switch (toLocation.zone) {
case 'priorities':
// Move the card to the .priorities
//filterFromSubpiles(inTask, task.taskId)
addPriority(inTask, task.taskId)
addParent(task, inTask.taskId)
break
case 'grid':
// Move the card to the .pins using coordinates in the current gridStyle, or fail if not possible
// If there isn't a grid on this card, add a grid large enough for the new coordinates to fit on
if (!inTask.pinboard) {
inTask.pinboard = blankPinboard(
Math.max(toLocation.coords.y, 3),
Math.max(toLocation.coords.x, 3)
)
}
pinTaskToTask(inTask, task.taskId, toLocation.coords)
addParent(task, inTask.taskId)
break
case 'completed':
// Move the card to the .completed
addCompletedTask(inTask, task.taskId)
break
case 'discard':
// Remove the card from its inId, or save in .completed if it's
// Could replace task-de-sub-tasked
filterFromSubpiles(inTask, task.taskId)
break
case 'context':
case 'panel':
// These don't do anything on the server, it's a zone only on the client
break
case 'gifts':
// Deprecated?
break
case 'stash':
// the .level on the toLocation.level tells what stash level to put the card in
// Rethink this
break
case 'card':
case 'subTasks':
default:
// Move the card to the .subTasks (replaces task-sub-tasked)
putTaskInTask(task, inTask)
break
}
}
// Removes the specified discardTaskId from the specified zone of the given task
// If zone argument is 'card' or empty, tries to discard from priorities, subTasks, and completed (but not grid)
export function discardTaskFromZone(task, fromLocation) {
switch (fromLocation.zone) {
case 'grid':
unpinTaskFromTask(task, fromLocation.coords)
return
case 'priorities':
discardPriority(task, fromLocation.taskId)
return
case 'completed':
discardCompletedTask(task, fromLocation.taskId)
return
case 'subTasks':
discardSubTask(task, fromLocation.taskId)
return
}
}
// Moves a card from one location to another location.
// fromLocation defines the card to be unplayed from somewhere, and toLocation defines a card to be placed somewhere.
// fromLocation and toLocation are CardLocation objects defining a taskId in a location.
// The fromLocation is an optional CardLocation that, if provided, requires a taskId and zone at minimum
// If null, no card will be unplayed.
// fromLocation.taskId and toLocation.taskId can be different,
// so it is possible to play a different card than was unplayed in one move (i.e., swap out a card)
// Right now the card being played must exist; card creation and modification is separate since it includes color etc.
// Maybe toLocation should be option also, simplifying discards and further decomposing a play.
export function atomicCardPlay(
tasks,
fromLocation: CardLocation,
toLocation: CardLocation,
memberId
) {
const taskId =
fromLocation && fromLocation.taskId
? fromLocation.taskId
: toLocation.taskId
const theCard = getTask(tasks, taskId)
if (!theCard && fromLocation?.zone !== 'grid') {
return
}
const theCardMovedTo = getTask(tasks, toLocation.inId)
if (
!theCardMovedTo &&
!['discard', 'context', 'panel'].includes(toLocation.zone)
) {
console.log(
'Attempting to move a card to a missing card, this should never happen. Missing card:',
toLocation.inId,
'and zone:',
toLocation.zone
)
return
}
if (theCard && memberId) {
// memberId should be required, but temporarily for debugging
// You cannot play a card without having seen it
seeTask(theCard, memberId)
// You cannot play a card without first grabbing it
grabTask(tasks, theCard, memberId)
}
// Remove the card from wherever it was moved from
const fromInId = fromLocation?.inId
const theCardMovedFrom = getTask(tasks, fromInId)
if (theCardMovedFrom) {
discardTaskFromZone(theCardMovedFrom, fromLocation)
if (fromLocation.inId !== toLocation.inId) {
removeParentIfNotParent(theCard, theCardMovedFrom)
}
// Save the card to the completed cards if it has at least one checkmark
if (
fromLocation.zone != 'completed' &&
toLocation.zone === 'discard' &&
theCard &&
theCard.claimed &&
theCard.claimed.length >= 1
) {
addCompletedTask(theCardMovedFrom, taskId)
}
// If removing from priorities, remove allocations that were on the card from theCardMovedFrom
if (
fromLocation?.zone === 'priorities' &&
toLocation.zone !== 'priorities' &&
theCardMovedFrom.allocations &&
Array.isArray(theCardMovedFrom.allocations)
) {
theCardMovedFrom.allocations = theCardMovedFrom.allocations.filter(al => {
if (al.allocatedId === taskId) {
theCardMovedFrom.boost += al.amount
return false
}
return true
})
}
}
// Move card to wherever it was moved to
if (theCard) {
putTaskInTaskZone(theCard, theCardMovedTo, toLocation)
}
}
// Adds the given taskId to the given card's stash of the specified level
// Each membership level added to a card has a corresponding stash level
export function stashTask(task, taskId, level) {
if (
!task.hasOwnProperty('stash') ||
!(
typeof task.stash === 'object' &&
task.stash !== null &&
!Array.isArray(task.stash)
)
) {
task.stash = {}
}
if (!task.stash.hasOwnProperty('level')) {
task.stash[level] = []
}
task.stash[level] = task.stash[level].filter(tId => tId !== taskId)
task.stash[level].push(taskId)
}
// Removes the given taskId from the given card's stash of the specified level
export function unstashTask(task, taskId, level) {
if (
!task.hasOwnProperty('stash') ||
!(
typeof task.stash === 'object' &&
task.stash !== null &&
!Array.isArray(task.stash)
)
) {
return
}
if (!task.stash.hasOwnProperty('level')) {
return
}
task.stash[level] = task.stash[level].filter(tId => tId !== taskId)
}
// A potentials list is a list of signatures, each signature endorsing a specific task event-type
// When POTENTIALS_TO_EXECUTE potentials accrue for a given event-type it is executed, like an action potential
// This allows built-in AO mutations to be voted upon by members before execution
// Because it is a vote, duplicate potentials for the same event-type are prevented
export function addPotential(member, signature) {
if (!member.potentials) {
member.potentials = []
}
member.potentials = member.potentials.filter(
pot =>
!(
pot.opinion === signature.opinion && pot.memberId === signature.memberId
)
)
member.potentials.push(signature)
}
// Returns true if there are POTENTIALS_TO_EXECUTE or more potentials of the specified event-type on the object
export function checkPotential(member, eventType) {
return (
member.potentials.filter(pot => pot.opinion === eventType).length >=
POTENTIALS_TO_EXECUTE
)
}
// Clears all potentials of the specified event-type from the given card
export function clearPotential(member, eventType) {
member.potentials = member.potentials.filter(pot => pot.opinion !== eventType)
}
// Sets the lastUsed property of the given object to the given timestamp
export function updateLastUsed(member, timestamp) {
member.lastUsed = timestamp
}
export function safeMerge(cardA, cardZ) {
if (!cardA || !cardZ) {
console.log('attempt to merge nonexistent card')
return
}
if (!cardZ.taskId || !isString(cardZ.taskId)) {
console.log('attempt to merge card with a missing or invalid taskId')
return
}
if (!cardZ.color) {
console.log('attempt to merge card without a color')
return
}
if (isString(cardZ.color) && cardZ.color.trim().length >= 1) {
cardA.color = cardZ.color
}
if (isString(cardZ.guild) && cardZ.color.trim().length >= 1) {
cardA.guild = cardZ.guild
}
const filterNull = tasks => {
return tasks.filter(task => task !== null && task !== undefined)
}
cardA.book = cardZ.book
cardA.address = cardZ.address
cardA.bolt11 = cardZ.bolt11
cardA.priorities = [
...new Set(cardA.priorities.concat(filterNull(cardZ.priorities))),
]
cardA.subTasks = [
...new Set(cardA.subTasks.concat(filterNull(cardZ.subTasks))),
]
cardA.completed = [
...new Set(cardA.completed.concat(filterNull(cardZ.completed))),
]
// Replace the pinboard (maybe they could merge? or at least drop existing pins down to subTasks)
if (
cardZ.pinboard &&
cardZ.pinboard.height >= 1 &&
cardZ.pinboard.width >= 1
) {
if (!cardA.pinboard) {
cardA.pinboard = blankPinboard()
}
cardA.pinboard.height = Math.max(
cardA.pinboard.height,
cardZ.pinboard.height
)
cardA.pinboard.width = Math.max(cardA.pinboard.width, cardZ.pinboard.width)
cardA.pinboard.spread = cardZ.pinboard.spread
if (cardZ.pins && Array.isArray(cardZ.pins)) {
cardA.pins = cardZ.pins
}
}
cardA.passed = [...new Set([...cardA.passed, ...filterNull(cardZ.passed)])]
// Remove duplicate passes
let passesNoDuplicates: CardPass[] = []
cardA.passed.forEach(pass => {
if (
!passesNoDuplicates.some(
pass2 => pass[0] === pass2[0] && pass[1] === pass2[1]
)
) {
passesNoDuplicates.push(pass)
}
})
cardA.passed = passesNoDuplicates
// XXX only add in merge for now
// XXX bolt11 / address need to clearly indicate origin ao
// XXX book should be a list?
}
// A card's .signed is an append-only list of all signing events.
// This function reduces it to just each member's current opinion
// signed is type Signature[]
export function mostRecentSignaturesOnly(signed) {
let mostRecentSignaturesOnly = signed.filter((signature, index) => {
let lastIndex
for (let i = signed.length - 1; i >= 0; i--) {
if (signed[i].memberId === signature.memberId) {
lastIndex = i
break
}
}
return lastIndex === index
})
return mostRecentSignaturesOnly
}
// Signed is type Signature[]
export function countCurrentSignatures(signed) {
return mostRecentSignaturesOnly(signed).filter(
signature => signature.opinion >= 1
).length
}

30
src/crypto.ts

@ -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')
}

100
src/members.ts

@ -1,100 +0,0 @@
import { getTask } from './cards.js'
// DUPLICATED function from client/cardActions.ts
// Returns the number of vouches a member has. Member card must exist for each voucher. Members do not have to be active to vouch.
// This function can be used either on the client or the server. If on the server, server state must be passed as the second arg.
export function countVouches(memberId, state): number | null {
let card
card = getTask(state.tasks, memberId)
if (!card || !card.hasOwnProperty('deck')) return null
let count = 0
const memberCards = card.deck
.map(memberId => state.members.find(m => m.memberId === memberId))
.forEach(memberCard => {
if (memberCard !== undefined) {
count++
}
})
return count
}
// Returns true if the senpai memberId is ahead of the kohai memberId in the members list order
export function isAheadOf(senpaiId, kohaiId, state, errRes?) {
if (errRes === undefined) {
errRes = []
}
let senpaiRank = state.members.findIndex(m => m.memberId === senpaiId)
let kohaiRank = state.members.findIndex(m => m.memberId === kohaiId)
if (senpaiRank < kohaiRank) {
return 1
} else if (kohaiRank < senpaiRank) {
return -1
}
errRes.push('member is not ahead of other member in order of member list')
return 0
}
// Returns true if the senpai has more attack than the kohai has defense
// A member's defense is their number of vouches, or the highest attack score out of anyone who vouches for them (whichever is greater)
// This method does not check if vouchers exist, therefore it depends on the mutations being perfect
// and there not being any invalid members leftover in the .deck / vouchers list of the other member
export function isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes?) {
if (errRes === undefined) {
errRes = []
}
const senpaiCard = state.tasks.find(t => t.taskId === senpaiId)
if (!senpaiCard) {
errRes.push('invalid member detected')
return null
}
const kohaiCard = state.tasks.find(t => t.taskId === kohaiId)
if (!kohaiCard) {
errRes.push('invalid member detected')
return null
}
const senpaiVouches = countVouches(senpaiId, state)
let kohaiVouchCards = state.tasks.filter(
t => kohaiCard.deck.indexOf(t.taskId) >= 0
)
let kohaiVouches = kohaiVouchCards.length
kohaiVouchCards.forEach(card => {
if (card.taskId !== senpaiCard.taskId) {
kohaiVouches = Math.max(kohaiVouches, countVouches(card.taskId, state))
}
})
if (senpaiVouches > kohaiVouches) {
return 1
} else if (kohaiVouches > senpaiVouches) {
return -1
}
errRes.push('member does not have more vouches than other member')
return 0
}
// Returns true if the senpaiId member is both isAheadOf and isDecidedlyMorePopularThan the kohaiId member
export function isSenpaiOf(senpaiId, kohaiId, state, errRes?) {
if (errRes === undefined) {
errRes = []
}
const rank = isAheadOf(senpaiId, kohaiId, state, errRes)
const vouches = isDecidedlyMorePopularThan(senpaiId, kohaiId, state, errRes)
if (rank === 1 && vouches === 1) {
return 1
} else if (rank === -1 && vouches === -1) {
return -1
}
errRes.push('member is not a senpai of the other member')
return 0
}

2
src/modules/ao.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = []

2
src/modules/cash.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = {
alias: '',

2
src/modules/members.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = [] // aka members (in this file):

2
src/modules/memes.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = [] // aka files (in this file):

2
src/modules/resources.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = [] // aka resources (in this file):

2
src/modules/sessions.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = []

2
src/modules/tasks.ts

@ -1,4 +1,4 @@
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
const state = []

1549
src/mutations.ts

File diff suppressed because it is too large Load Diff

132
src/semantics.ts

@ -1,132 +0,0 @@
const defaultSemantics = {
glossary: {
card: 'card',
user: 'member',
username: 'hackername',
proposal: 'proposition',
avatar: 'avatar',
},
levels: {
0: 'guest',
1: 'member',
2: 'elite member',
},
}
let loadedGlossary = {}
/* Too complex for .env, need a new solution, maybe use fs here to import custom JSON or YAML file
if (config.semantics && config.semantics.glossary) {
loadedGlossary = config.semantics.glossary
}*/
const serverGlossary = { ...defaultSemantics.glossary, ...loadedGlossary }
function pluralize(word) {
let plural = word
if (Array.isArray(plural)) {
plural = plural[1]
} else {
if (plural[plural.length - 1] === 's') {
plural = plural + 'es'
} else {
plural = plural + 's'
}
}
return plural
}
export function capitalize(word) {
if (word.length < 1) {
return ''
}
return word[0].toUpperCase() + word.substring(1)
}
// Returns the given word or string, with all instances of words in the glossary in configuration.js replaced with their gloss.
// In a multi-word string, it will correctly distinguish between all-lowercase keywords and those
// with their first letter capitalized, and replace them correctly. Original hardcoded keywords must be typed (in this codebase)
// in either all lowercase or with the first letter capitalized to be caught be the word replacement.
export function gloss(wordOrSentence, plural = false) {
let result
if (wordOrSentence.indexOf(' ') < 0) {
const word = wordOrSentence
result = word
const lowercase = word.toLowerCase()
const pluralEntry = Object.entries(serverGlossary).find(
([keyword, synonym]) => {
return (
(Array.isArray(keyword) && keyword[1] === lowercase) ||
pluralize(keyword) === lowercase
)
}
)
const singularEntry = Object.entries(serverGlossary).find(
([keyword, synonym]) =>
(Array.isArray(keyword) && keyword[0] === lowercase) ||
keyword === lowercase
)
if (pluralEntry || singularEntry) {
result = pluralEntry ? pluralize(pluralEntry[1]) : singularEntry[1]
if (Array.isArray(result)) {
result = result[0]
}
if (word[0].toLowerCase() !== word[0]) {
result = result[0].toUpperCase() + result.substring(1)
}
}
} else {
result = wordOrSentence
Object.entries(serverGlossary).forEach(([keyword, synonym]) => {
// replace lowercase plural version of the keyword
const pluralKeyword = pluralize(keyword)
const pluralSynonym = pluralize(synonym)
let regexp = new RegExp('\\b' + pluralKeyword + '\\b', 'g')
result = result.replace(regexp, pluralSynonym)
// replace capitalized plural version of the keyword
const pluralKeywordUppercase = capitalize(pluralKeyword)
const pluralSynonymUppercase = capitalize(pluralSynonym)
regexp = new RegExp('\\b' + pluralKeywordUppercase + '\\b', 'g')
result = result.replace(regexp, pluralSynonymUppercase)
// replace lowercase singular version of the keyword
regexp = new RegExp('\\b' + keyword + '\\b', 'g')
const singularSynonym = Array.isArray(synonym) ? synonym[0] : synonym
result = result.replace(regexp, singularSynonym)
// replace capitalized singular version of the keyword
const singularKeywordUppercase = capitalize(keyword)
const singularSynonymUppercase = capitalize(singularSynonym)
regexp = new RegExp('\\b' + singularKeywordUppercase + '\\b', 'g')
result = result.replace(regexp, singularSynonymUppercase)
})
}
return result
}
let loadedLevels = {}
/*if (config.semantics && config.semantics.levels) {
loadedLevels = config.semantics.levels
}*/
const serverLevels = { ...defaultSemantics.levels, ...loadedLevels }
export function glossLevel(level) {
if (level < 0) {
return null
}
let highestMatchingWord
Object.entries(serverLevels).some(([index, word]) => {
if (index <= level) {
highestMatchingWord = word
}
if (index >= level) {
return true
}
})
return highestMatchingWord || 'member'
}
export function getSemantics() {
return { glossary: serverGlossary, levels: serverLevels }
}

2
src/server/auth.ts

@ -1,6 +1,6 @@
import { buildResCallback } from './utils.js'
import events from './events.js'
import { createHash, hmacHex } from '../crypto.js'
import { createHash, hmacHex } from '../ao-lib/crypto.js'
import state from './state.js'
const getIdSecret = function (identifier) {

6
src/server/database.ts

@ -1,9 +1,9 @@
import Kefir from 'kefir'
import { v1 } from 'uuid'
import dbengine from 'better-sqlite3'
import { createHash } from '../crypto.js'
import { blankCard } from '../cards.js'
import { Task } from '../types.js'
import { createHash } from '../ao-lib/crypto.js'
import { blankCard } from '../ao-lib/cards.js'
import { Task } from '../ao-lib/types.js'
interface TaskCreatedEvent extends Task {
type?: string

2
src/server/files.ts

@ -1,7 +1,7 @@
import path from 'path'
import fs from 'fs'
import events from './events.js'
import { createHash } from '../crypto.js'
import { createHash } from '../ao-lib/crypto.js'
import state from './state.js'
import { v1 } from 'uuid'
const serverState = state.serverState

2
src/server/link.ts

@ -3,7 +3,7 @@ import events from './events.js'
import state from './state.js'
const serverState = state.serverState
import { checkHash, postEvent } from './connector.js'
import { crawler, crawlerHash } from '../calculations.js'
import { crawler, crawlerHash } from '../ao-lib/calculations.js'
import Rsync from 'rsync'
const syncLink = new cron.CronJob({

6
src/server/router.ts

@ -8,18 +8,18 @@ import { serverAuth } from './auth.js'
import lightningRouter from './lightning.js'
import fs from 'fs'
import multer from 'multer'
import { Task } from '../types.js'
import { Task } from '../ao-lib/types.js'
import { addMeme } from './files.js'
import events from './events.js'
import { crawlerHash } from '../calculations.js'
import { crawlerHash } from '../ao-lib/calculations.js'
import { allReachableHeldParentsServer } from '../ao-lib/cards.js'
import validators from './validators.js'
import { fileURLToPath } from 'url'
import util from 'util'
import { allReachableHeldParentsServer } from '../cards.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

4
src/server/spec.ts

@ -3,12 +3,12 @@ import { v1 } from 'uuid'
import state from './state.js'
import { buildResCallback } from './utils.js'
import validators from './validators.js'
import { blankCard, getTask } from '../cards.js'
import { blankCard, getTask } from '../ao-lib/cards.js'
import { createHash } from '../ao-lib/crypto.js'
import events from './events.js'
import { postEvent } from './connector.js'
import { newAddress, createInvoice } from './lightning.js'
import { sendNotification } from './signal.js'
import { createHash } from '../crypto.js'
//import getUrls from 'get-urls'
import { cache } from './cache.js'

2
src/server/state.ts

@ -1,5 +1,5 @@
import { recover, getAll, insertBackup, insertEvent } from './database.js'
import M from '../mutations.js'
import M from '../ao-lib/mutations.js'
import { formatDistanceToNow } from 'date-fns'
import cron from 'cron'
import torControl from './torControl.js'

2
src/server/validators.ts

@ -1,5 +1,5 @@
import state from './state.js'
import { isAheadOf, isDecidedlyMorePopularThan, isSenpaiOf } from '../members.js'
import { isAheadOf, isDecidedlyMorePopularThan, isSenpaiOf } from '../ao-lib/members.js'
export default {
isAmount(val, errRes) {

351
src/types.ts

@ -1,351 +0,0 @@
// Every card has a color, right now only five preset colors exist (they can be renamed with the glossary and recolored with CSS)
export type Color = 'red' | 'yellow' | 'green' | 'purple' | 'blue' | 'black'
// The regions or areas within a card
// There are four main zones within a card: the priorities,
// the optional pinboard (grid/pyramid/rune),the subTasks (main pile), and completed cards