deicidus
2 years ago
23 changed files with 20 additions and 3146 deletions
@ -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() |
||||
}) |
||||
} |
@ -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 |
||||
} |
@ -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') |
||||
} |
@ -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 |
||||
} |
@ -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 } |
||||
} |
@ -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
|