You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1549 lines
44 KiB
1549 lines
44 KiB
// Mutations are state builders. |
|
// The current state is the result of all the events in the system fed through the mutation functions. |
|
// `server/state.js` for server; `modules/*` for vuex. |
|
|
|
// const Vue = require('vue') |
|
import { createHash } from './crypto.js' |
|
import { |
|
blankCard, |
|
blankPinboard, |
|
getTask, |
|
getTaskBy, |
|
atomicCardPlay, |
|
unpinTasksOutOfBounds, |
|
discardTaskFromZone, |
|
taskExists, |
|
seeTask, |
|
clearPassesTo, |
|
changeGiftCount, |
|
grabTask, |
|
dropTask, |
|
addParent, |
|
removeParentIfNotParent, |
|
filterFromSubpiles, |
|
clearSeenExcept, |
|
addSubTask, |
|
putTaskInTask, |
|
addPriority, |
|
stashTask, |
|
unstashTask, |
|
addPotential, |
|
checkPotential, |
|
clearPotential, |
|
updateLastUsed, |
|
safeMerge, |
|
registerDuplicateTaskId, |
|
POTENTIALS_TO_EXECUTE, |
|
} from './cards.js' |
|
|
|
function aoMuts(aos, ev) { |
|
switch (ev.type) { |
|
//case 'ao-linked': |
|
// aos.forEach((ao, i) => { |
|
// if (ao.address === ev.address) { |
|
// ao.links.push(ev.taskId) |
|
// } |
|
// }) |
|
// break |
|
case 'ao-inbound-connected': |
|
let inAddressConnect = aos.some(a => { |
|
if (a.address === ev.address) { |
|
a.inboundSecret = ev.secret |
|
a.lastContact = Date.now() |
|
return true |
|
} |
|
}) |
|
if (!inAddressConnect) { |
|
let newEv = { |
|
address: ev.address, |
|
outboundSecret: false, |
|
inboundSecret: ev.secret, |
|
lastContact: Date.now(), |
|
} |
|
aos.push(newEv) |
|
} |
|
break |
|
case 'ao-outbound-connected': |
|
let outAddressConnect = aos.some(a => { |
|
if (a.address === ev.address) { |
|
a.outboundSecret = ev.secret |
|
a.lastContact = Date.now() |
|
return true |
|
} |
|
}) |
|
if (!outAddressConnect) { |
|
let newEv = { |
|
address: ev.address.trim(), |
|
outboundSecret: ev.secret, |
|
inboundSecret: false, |
|
lastContact: Date.now(), |
|
} |
|
aos.push(newEv) |
|
} |
|
break |
|
case 'ao-disconnected': |
|
aos.forEach((ao, i) => { |
|
if (ao.address.trim() === ev.address.trim()) { |
|
aos.splice(i, 1) |
|
} |
|
}) |
|
break |
|
} |
|
} |
|
|
|
function cashMuts(cash, ev) { |
|
switch (ev.type) { |
|
case 'ao-named': |
|
cash.alias = ev.alias |
|
break |
|
case 'spot-updated': |
|
cash.spot = ev.spot |
|
break |
|
case 'currency-switched': |
|
cash.currency = ev.currency |
|
break |
|
case 'rent-set': |
|
cash.rent = parseFloat(ev.amount) |
|
break |
|
case 'cap-set': |
|
cash.cap = ev.amount |
|
break |
|
case 'funds-set': |
|
cash.outputs = ev.outputs |
|
cash.channels = ev.channels |
|
break |
|
case 'quorum-set': |
|
cash.quorum = ev.quorum |
|
break |
|
case 'task-boosted': |
|
if (ev.txid) cash.usedTxIds.push(ev.txid) |
|
break |
|
case 'task-boosted-lightning': |
|
cash.pay_index = ev.pay_index |
|
break |
|
case 'get-node-info': |
|
cash.info = ev.info |
|
break |
|
} |
|
} |
|
|
|
function membersMuts(members, ev) { |
|
switch (ev.type) { |
|
case 'ao-outbound-connected': |
|
break |
|
case 'ao-disconnected': |
|
break |
|
case 'member-created': |
|
updateLastUsed(ev, ev.timestamp) |
|
ev.muted = true |
|
ev.p0wned = true |
|
members.push(ev) |
|
break |
|
case 'member-activated': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId && !member.banned) { |
|
if (member.active < 0) { |
|
member.active = -1 * member.active |
|
} else { |
|
member.active++ |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-boosted': |
|
members.forEach(member => { |
|
if (member.memberId === ev.taskId) { |
|
if (member.active < 0) { |
|
member.active = -1 * member.active |
|
} else { |
|
member.active++ |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-boosted-lightning': |
|
members.forEach(member => { |
|
if (member.memberId === ev.taskId) { |
|
if (member.active < 0) { |
|
member.active = -1 * member.active |
|
} else { |
|
member.active++ |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-visited': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId) { |
|
updateLastUsed(member, ev.timestamp) |
|
} |
|
}) |
|
break |
|
case 'member-deactivated': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId) { |
|
if (member.active >= 0) { |
|
member.active = -1 * member.active - 1 |
|
} |
|
} |
|
}) |
|
break |
|
case 'member-secret-reset': |
|
members.forEach(member => { |
|
if (member.memberId === ev.kohaiId) { |
|
const newSig = { |
|
memberId: ev.senpaiId, |
|
timestamp: ev.timestamp, |
|
opinion: ev.type, |
|
} |
|
|
|
addPotential(member, newSig) |
|
|
|
if (checkPotential(member, 'member-secret-reset')) { |
|
member.p0wned = true |
|
member.secret = createHash(member.name) |
|
clearPotential(member, 'member-secret-reset') |
|
} |
|
} |
|
}) |
|
break |
|
case 'member-promoted': |
|
let toIndex |
|
let fromIndex |
|
if ( |
|
members.some((member, i) => { |
|
if (member.memberId === ev.senpaiId) { |
|
toIndex = i |
|
return true |
|
} |
|
}) && |
|
members.some((member, i) => { |
|
if (member.memberId === ev.kohaiId) { |
|
fromIndex = i |
|
return true |
|
} |
|
}) |
|
) { |
|
members.splice(toIndex, 0, members.splice(fromIndex, 1)[0]) |
|
} |
|
case 'member-banned': |
|
members.forEach(member => { |
|
if (member.memberId === ev.kohaiId) { |
|
const newSig = { |
|
memberId: ev.senpaiId, |
|
timestamp: ev.timestamp, |
|
opinion: ev.type, |
|
} |
|
|
|
addPotential(member, newSig) |
|
|
|
if (checkPotential(member, 'member-banned')) { |
|
member.banned = true |
|
if (member.active >= 0) { |
|
member.active = -1 * member.active - 1 |
|
} |
|
} |
|
} |
|
}) |
|
break |
|
case 'member-unbanned': |
|
members.forEach(member => { |
|
if ( |
|
member.memberId === ev.kohaiId && |
|
member.hasOwnProperty('potentials') && |
|
member.potentials.length >= 1 |
|
) { |
|
const beforeBans = member.potentials.filter( |
|
p => p.opinion === 'member-banned' |
|
).length |
|
|
|
member.potentials = member.potentials.filter( |
|
p => !(p.opinion === 'member-banned' && p.memberId === ev.senpaiId) |
|
) |
|
|
|
const afterBans = member.potentials.filter( |
|
p => p.opinion === 'member-banned' |
|
).length |
|
|
|
if ( |
|
beforeBans >= POTENTIALS_TO_EXECUTE && |
|
afterBans < POTENTIALS_TO_EXECUTE |
|
) { |
|
member.banned = false |
|
} |
|
} |
|
}) |
|
break |
|
case 'member-purged': |
|
for (let i = members.length - 1; i >= 0; i--) { |
|
const member = members[i] |
|
if (member.memberId === ev.memberId) { |
|
const newSig = { |
|
memberId: ev.blame, |
|
timestamp: ev.timestamp, |
|
opinion: ev.type, |
|
} |
|
|
|
//addPotential(member, newSig) |
|
|
|
//if (testPotential(member, 'member-purged')) { |
|
members.splice(i, 1) |
|
//} |
|
} |
|
} |
|
|
|
break |
|
case 'resource-used': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId) { |
|
updateLastUsed(member, ev.timestamp) |
|
} |
|
}) |
|
break |
|
|
|
case 'member-field-updated': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId) { |
|
member[ev.field] = ev.newfield |
|
if (ev.field === 'secret') { |
|
member.p0wned = false |
|
} |
|
} |
|
}) |
|
break |
|
|
|
case 'member-ticker-set': |
|
members.forEach(member => { |
|
if (member.memberId === ev.memberId) { |
|
if (!member.tickers) { |
|
member.tickers = [] |
|
} |
|
if ( |
|
!ev.fromCoin || |
|
ev.fromCoin.trim().length < 1 || |
|
!ev.toCoin || |
|
ev.toCoin.trim().length < 1 |
|
) { |
|
member.tickers.splice(ev.index, 1) |
|
} else { |
|
member.tickers[ev.index] = { |
|
from: ev.fromCoin.trim().toLowerCase(), |
|
to: ev.toCoin.trim().toLowerCase(), |
|
} |
|
} |
|
} |
|
}) |
|
break |
|
|
|
case 'doge-barked': |
|
members.forEach(member => { |
|
// this should only bump up for mutual doges |
|
if (member.memberId === ev.memberId) { |
|
updateLastUsed(member, ev.timestamp) |
|
// then bark |
|
} |
|
}) |
|
break |
|
} |
|
} |
|
|
|
function resourcesMuts(resources, ev) { |
|
switch (ev.type) { |
|
case 'resource-created': |
|
let resourceIds = resources.map(r => r.resourceId) |
|
if (resourceIds.indexOf(ev.resourceId) === -1) { |
|
resources.push(ev) |
|
} else { |
|
console.log( |
|
'BAD data duplicate resource rejected in mutation, dup resource task likely created' |
|
) |
|
} |
|
break |
|
case 'resource-used': |
|
resources.forEach(resource => { |
|
if (resource.resourceId == ev.resourceId) { |
|
resource.stock -= parseInt(ev.amount) |
|
} |
|
}) |
|
break |
|
case 'resource-purged': |
|
resources.forEach((r, i) => { |
|
if (r.resourceId === ev.resourceId) { |
|
resources.splice(i, 1) |
|
} |
|
}) |
|
break |
|
case 'resource-stocked': |
|
resources.forEach(resource => { |
|
if (resource.resourceId == ev.resourceId) { |
|
resource.stock += parseInt(ev.amount) |
|
} |
|
}) |
|
break |
|
case 'channel-created': |
|
resources.forEach((r, i) => { |
|
if (r.resourceId == ev.resourceId) { |
|
r.pubkey = ev.pubkey |
|
} |
|
}) |
|
break |
|
} |
|
} |
|
|
|
function memesMuts(memes, ev) { |
|
switch (ev.type) { |
|
case 'meme-added': |
|
const fileHash = ev.data |
|
if ( |
|
!memes.some(file => { |
|
return file.hash === ev.hash |
|
}) |
|
) { |
|
memes.push({ |
|
memeId: ev.taskId, |
|
filename: ev.filename, |
|
hash: ev.hash, |
|
filetype: ev.filetype, |
|
}) |
|
// console.log('added meme file: ', ev.filename) |
|
} else { |
|
// console.log('meme file already in state: ', ev.filename) |
|
} |
|
break |
|
case 'task-removed': |
|
for (let i = memes.length - 1; i >= 0; i--) { |
|
const meme = memes[i] |
|
if (meme.memeId === ev.taskId) { |
|
memes.splice(i, 1) |
|
} |
|
} |
|
break |
|
} |
|
} |
|
|
|
function sessionsMuts(sessions, ev) { |
|
switch (ev.type) { |
|
case 'session-created': |
|
let idHasSession = sessions.some(session => { |
|
// replace that sessions creds, |
|
let match = false |
|
if (session.ownerId === ev.ownerId) { |
|
match = true |
|
Object.assign(session, session, ev) |
|
} |
|
return match // true terminates the some loop & idHasSession->true too |
|
}) |
|
|
|
if (idHasSession) { |
|
// edited in session |
|
} else { |
|
// id didn't previously have session |
|
sessions.push(ev) |
|
} |
|
break |
|
case 'session-killed': |
|
for (let i = sessions.length - 1; i >= 0; i--) { |
|
const s = sessions[i] |
|
if (s.session == ev.session) { |
|
sessions.splice(i, 1) |
|
} |
|
} |
|
break |
|
case 'ao-outbound-connected': |
|
sessions.push({ |
|
ownerId: ev.address, |
|
token: ev.secret, |
|
session: ev.address, |
|
}) |
|
break |
|
} |
|
} |
|
|
|
let missingTaskIds = [] |
|
function tasksMuts(tasks, ev) { |
|
let theTask |
|
let inTask |
|
let memberTask |
|
|
|
// Most tasks have a taskId and memberId, and many have an inId, so pull these out in a standard way |
|
const memberTaskId = ev.memberId || ev.blame |
|
if ( |
|
memberTaskId && |
|
memberTaskId !== 'cleanup' && |
|
typeof memberTaskId === 'string' && |
|
!memberTaskId.includes('.onion') |
|
) { |
|
memberTask = getTask(tasks, memberTaskId) |
|
if ( |
|
!memberTask && |
|
ev.type !== 'member-created' && |
|
ev.type !== 'member-purged' |
|
) { |
|
if (!missingTaskIds.includes(memberTaskId)) { |
|
missingTaskIds.push(memberTaskId) |
|
console.log( |
|
ev.type + ': first missing member task for memberId', |
|
memberTaskId, |
|
'(' + missingTaskIds.length + ')' |
|
) |
|
} |
|
return |
|
} |
|
} |
|
|
|
const theTaskId = |
|
ev.taskId || |
|
ev.subTask || |
|
ev.resourceId || |
|
ev?.from?.taskId || |
|
ev?.to?.taskId |
|
if (theTaskId) { |
|
theTask = getTask(tasks, theTaskId) |
|
if ( |
|
!theTask && |
|
ev.type !== 'task-created' && |
|
ev.type !== 'grid-created' && |
|
ev.type !== 'resource-created' && |
|
ev.type !== 'meme-added' |
|
) { |
|
if (!missingTaskIds.includes(theTaskId)) { |
|
missingTaskIds.push(theTaskId) |
|
//console.log(ev.type + ': first missing task event for taskId', theTaskId, '(' + missingTaskIds.length + ') ev:', ev) |
|
} |
|
return // continuing may crash, but returning may cause further inconsistencies going forward |
|
} |
|
} |
|
|
|
if (ev.inId) { |
|
inTask = getTask(tasks, ev.inId) |
|
if (!inTask && ev.type !== 'task-created') { |
|
if (!missingTaskIds.includes(ev.inId)) { |
|
missingTaskIds.push(ev.inId) |
|
//console.log(ev.type + ': first missing task event for inId', ev.inId, '(' + missingTaskIds.length + ') ev:', ev) |
|
} |
|
return // continuing may crash, but returning may cause further inconsistencies going forward |
|
} |
|
} |
|
|
|
switch (ev.type) { |
|
case 'highlighted': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
let didUpdateInline = false |
|
task.highlights.forEach((h, i) => { |
|
if (h.memberId === ev.memberId) { |
|
didUpdateInline = true |
|
if (h.valence === ev.valence) { |
|
task.highlights.splice(i, 1) |
|
} else { |
|
h.valence = ev.valence |
|
} |
|
} |
|
}) |
|
if (!didUpdateInline) { |
|
task.highlights.push({ |
|
memberId: ev.memberId, |
|
valence: ev.valence, |
|
}) |
|
} |
|
} |
|
}) |
|
break |
|
case 'ao-outbound-connected': |
|
tasks.push(blankCard(ev.address, ev.address, 'purple', ev.timestamp)) |
|
break |
|
case 'ao-disconnected': |
|
break |
|
case 'resource-created': |
|
tasks.push(blankCard(ev.resourceId, ev.resourceId, 'red', ev.timestamp)) |
|
break |
|
case 'member-created': |
|
tasks.push(blankCard(ev.memberId, ev.memberId, 'blue', ev.timestamp)) |
|
break |
|
case 'member-purged': |
|
// This is terribly redundant since the same potential builds up on the member. |
|
// Maybe the potentials system should be abstracted out to the spec or validation layer; |
|
// Attempts to call limited functions instead produce an action-potential event. |
|
// The original idea was potentials would only build up on members, not tasks. |
|
let purgedMemberCard = false |
|
for (let i = tasks.length - 1; i >= 0; i--) { |
|
const task = tasks[i] |
|
if (task.taskId === ev.memberId) { |
|
let newSig = { |
|
memberId: ev.blame, |
|
timestamp: ev.timestamp, |
|
opinion: ev.type, |
|
} |
|
|
|
addPotential(task, newSig) |
|
|
|
if (checkPotential(task, 'member-purged')) { |
|
tasks.splice(i, 1) |
|
purgedMemberCard = true |
|
} |
|
} |
|
} |
|
|
|
if (purgedMemberCard) { |
|
tasks.forEach((t, j) => { |
|
t.subTasks = t.subTasks.filter(st => st !== ev.memberId) |
|
t.priorities = t.priorities.filter(st => st !== ev.memberId) |
|
t.completed.filter(st => st !== ev.memberId) |
|
t.claimed = t.claimed.filter(st => st !== ev.memberId) |
|
t.deck = t.deck.filter(st => st !== ev.memberId) |
|
clearPassesTo(tasks, t, ev.memberId, true) |
|
if (t.pins && t.pins.length >= 1) { |
|
t.pins.forEach((pin, i) => { |
|
const cell = pin.taskId |
|
if (cell === ev.memberId) { |
|
tasks[j].pins.spice(i, 1) |
|
} |
|
}) |
|
} |
|
}) |
|
} |
|
break |
|
case 'meme-added': |
|
// console.log('meme-added taskId is', ev.taskId) |
|
if (!tasks.some(t => t.taskId === ev.taskId)) { |
|
// console.log('adding meme', ev.taskId) |
|
tasks.push(blankCard(ev.taskId, ev.filename, 'yellow', ev.timestamp)) |
|
} |
|
break |
|
case 'task-created': |
|
const foundExistingTask = getTaskBy(tasks, ev.name, 'name') |
|
if (foundExistingTask?.taskId) { |
|
registerDuplicateTaskId(foundExistingTask.taskId, ev.taskId) |
|
break |
|
} |
|
tasks.push( |
|
blankCard( |
|
ev.taskId, |
|
ev.name, |
|
ev.color, |
|
ev.timestamp, |
|
ev.deck, |
|
ev.inId ? [ev.inId] : [] |
|
) |
|
) |
|
if (inTask) { |
|
if (ev.prioritized) { |
|
addPriority(inTask, ev.taskId) |
|
} else { |
|
addSubTask(inTask, ev.taskId) |
|
} |
|
addParent(inTask, ev.inId) |
|
clearSeenExcept(inTask, ev.deck.length >= 1 ? [ev.deck[0]] : undefined) // The very font of novelty |
|
} |
|
break |
|
case 'address-updated': |
|
tasks.forEach(t => { |
|
if (t.taskId === ev.taskId) { |
|
t.address = ev.address |
|
} |
|
}) |
|
break |
|
case 'task-passed': |
|
let pass = [ev.fromMemberId, ev.toMemberId] |
|
|
|
if ( |
|
!theTask.passed.some(p => { |
|
if (p[0] === pass[0] && p[1] === pass[1]) { |
|
return true |
|
} |
|
}) |
|
) { |
|
theTask.passed.push(pass) |
|
const recipient = getTask(tasks, ev.toMemberId) |
|
if (recipient) { |
|
changeGiftCount(recipient, 1) |
|
} |
|
} |
|
break |
|
case 'task-grabbed': |
|
grabTask(tasks, theTask, ev.memberId) |
|
break |
|
case 'task-seen': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
if (!task.seen) { |
|
task.seen = [] |
|
} |
|
if ( |
|
!task.seen.some(t => { |
|
return t.memberId === ev.memberId |
|
}) |
|
) { |
|
task.seen.push({ memberId: ev.memberId, timestamp: Date.now() }) |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-started': |
|
const tsFound = theTask |
|
|
|
if (tsFound) { |
|
if (!tsFound.timelog) { |
|
tsFound.timelog = [] |
|
} |
|
// console.log('task-started pre timelog is', tsFound.timelog) |
|
tsFound.timelog.push({ |
|
memberId: ev.memberId, |
|
taskId: ev.taskId, |
|
inId: ev.inId, |
|
start: ev.timestamp, |
|
stop: null, |
|
}) |
|
} |
|
// console.log('task-started post timelog is', tsFound.timelog) |
|
break |
|
case 'task-stopped': |
|
// console.log('task-stopped 1') |
|
const tstFound = theTask |
|
// console.log('task-stopped 2') |
|
if (tstFound) { |
|
// console.log('task-stopped 3') |
|
// console.log('task-stopped pre timelog is', tstFound.timelog) |
|
if (!tstFound.timelog) { |
|
return |
|
} |
|
// console.log('task-stopped 4') |
|
for (var i = tstFound.timelog.length - 1; i >= 0; i--) { |
|
// console.log('task-stopped 5') |
|
if (tstFound.timelog[i].memberId === ev.memberId) { |
|
// console.log('task-stopped 6') |
|
if ( |
|
tstFound.timelog[i].stop && |
|
tstFound.timelog[i].stop > tstFound.timelog[i].start |
|
) { |
|
// console.log( |
|
// 'task-stopped 7 stop is', |
|
// tstFound.timelog[i].stop, |
|
// ' and start is', |
|
// tstFound.timelog[i].start |
|
// ) |
|
console.log( |
|
'Stop time already set for most recent start time, triggering this event should not be possible in the GUI' |
|
) |
|
} else { |
|
// console.log('task-stopped 8') |
|
tstFound.timelog[i].stop = ev.timestamp |
|
// tstFound.timelog.push({ |
|
// memberId: 'test', |
|
// taskId: 'testId', |
|
// inId: null, |
|
// start: null, |
|
// stop: null, |
|
// }) |
|
} |
|
} |
|
// console.log('task-stopped post timelog is', tstFound.timelog) |
|
|
|
break |
|
} |
|
} |
|
break |
|
case 'task-time-clocked': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
let found = task.time.find(t => { |
|
return t.memberId === ev.memberId |
|
}) |
|
if (!found) { |
|
task.time.push({ |
|
memberId: ev.memberId, |
|
timelog: [ev.seconds], |
|
date: [ev.date], |
|
}) |
|
} else { |
|
if (!found.timelog) { |
|
found.timelog = [] |
|
} |
|
if (!found.date) { |
|
found.date = [] |
|
if (found.timelog.length > found.date.length) { |
|
let count = found.timelog.length - found.date.length |
|
while (count > 0) { |
|
found.date.push(null) |
|
count-- |
|
} |
|
} |
|
} |
|
found.timelog.push(ev.seconds) |
|
found.date.push(ev.date) |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-signed': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
clearPassesTo(tasks, task, ev.memberId) |
|
if (task.deck.indexOf(ev.memberId) === -1) { |
|
task.deck.push(ev.memberId) |
|
} |
|
let newSig = { |
|
memberId: ev.memberId, |
|
timestamp: ev.timestamp, |
|
opinion: ev.opinion, |
|
} |
|
if (!task.signed) { |
|
task.signed = [] |
|
} |
|
task.signed.push(newSig) |
|
if (task.guild && task.guild.length >= 1) { |
|
if ( |
|
ev.opinion === 1 && |
|
(!task.hasOwnProperty('memberships') || |
|
!Array.isArray(task.memberships)) |
|
) { |
|
if (!task.memberships) { |
|
task.memberships = [] |
|
} |
|
task.memberships.push({ memberId: ev.memberId, level: 2 }) |
|
} else if ( |
|
ev.opinion === 0 && |
|
task.hasOwnProperty('memberships') && |
|
Array.isArray(task.memberships) |
|
) { |
|
task.memberships.filter(memb => memb.memberId !== ev.memberId) |
|
} |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-membership': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
if (task.guild && task.guild.length >= 1) { |
|
// The member must have signed the task affirmatively |
|
if (!task.signed || !task.signed.length || task.signed.length < 1) { |
|
return |
|
} |
|
|
|
let mostRecentOpinion |
|
for (let i = task.signed.length - 1; i--; i >= 0) { |
|
const signature = task.signed[i] |
|
if (signature.memberId === ev.memberId) { |
|
mostRecentOpinion = signature.opinion |
|
break |
|
} |
|
} |
|
|
|
if (mostRecentOpinion < 1) { |
|
return |
|
} |
|
|
|
if ( |
|
!task.memberships || |
|
!task.memberships.length || |
|
task.memberships.length < 1 |
|
) { |
|
return |
|
} |
|
|
|
const promoterLevel = task.memberships.find( |
|
membership => membership.memberId === ev.blame |
|
)?.level |
|
|
|
if (!promoterLevel || promoterLevel < 1) { |
|
return |
|
} |
|
|
|
const promotedLevel = |
|
task.memberships.find( |
|
membership => membership.memberId === ev.memberId |
|
)?.level || 0 |
|
|
|
let maxLevel = 0 |
|
task.memberships.forEach(membership => { |
|
maxLevel = Math.max(maxLevel, membership.level) |
|
}) |
|
|
|
// The promoter must be a member at least one level higher |
|
// Or, the highest-level member of a group can promote themselves |
|
const canPromote = |
|
(promoterLevel > promotedLevel && |
|
promoterLevel >= ev.level + 1) || |
|
(ev.memberId === ev.blame && |
|
promoterLevel === maxLevel && |
|
maxLevel >= 1) |
|
|
|
if (!canPromote) { |
|
return |
|
} |
|
|
|
task.memberships = task.memberships.filter( |
|
memb => memb.memberId !== ev.memberId |
|
) |
|
if (ev.level !== 0) { |
|
task.memberships.push({ |
|
memberId: ev.memberId, |
|
level: ev.level, |
|
}) |
|
} |
|
} |
|
} |
|
}) |
|
break |
|
case 'task-stashed': |
|
// I think the spec is only run on event creation, not load from database, |
|
// so make sure the task exists before linking to it from another card |
|
const toStash = theTask |
|
if (toStash) { |
|
grabTask(tasks, toStash, ev.blame) |
|
addParent(toStash, ev.inId) |
|
|
|
tasks.forEach(task => { |
|
if (task.taskId === ev.inId) { |
|
stashTask(task, ev.taskId, ev.level) |
|
} |
|
}) |
|
} |
|
break |
|
case 'task-unstashed': |
|
// I think the spec is only run on event creation, not load from database, |
|
// so make sure the task exists before linking to it from another card |
|
const toUnstash = theTask |
|
const unstashParentCard = inTask |
|
if (toUnstash && unstashParentCard) { |
|
grabTask(tasks, toUnstash, ev.blame) |
|
|
|
tasks.forEach(task => { |
|
if (task.taskId === ev.inId) { |
|
unstashTask(task, ev.taskId, ev.level) |
|
removeParentIfNotParent(task, unstashParentCard) |
|
} |
|
}) |
|
} |
|
break |
|
case 'pile-grabbed': |
|
if (!ev.memberId) { |
|
break |
|
} |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
clearPassesTo(tasks, task, ev.memberId) |
|
let crawler = [ev.taskId] |
|
let history = [] |
|
let newCards = [] |
|
do { |
|
newCards = [] |
|
crawler.forEach(t => { |
|
if (history.indexOf(t) >= 0) return |
|
let subTask = tasks.filter(pst => pst.taskId === t) |
|
if (subTask.length < 1) { |
|
// console.log( |
|
// 'missing subtask, this is messy. parent task name: ', |
|
// task.name |
|
// ) |
|
return |
|
} |
|
if (subTask.length > 1) { |
|
console.log('duplicate task found, this is very bad') |
|
} |
|
subTask = subTask[0] |
|
if ( |
|
subTask === undefined || |
|
subTask.subTasks === undefined || |
|
subTask.priorities === undefined || |
|
subTask.completed === undefined |
|
) { |
|
console.log('invalid task data found, this is very bad') |
|
return |
|
} |
|
|
|
history.push(t) |
|
|
|
if ( |
|
subTask.deck.indexOf(ev.memberId) === -1 && |
|
ev.taskId !== ev.memberId |
|
) { |
|
clearPassesTo(tasks, subTask, ev.memberId) |
|
subTask.deck.push(ev.memberId) |
|
} |
|
newCards = newCards |
|
.concat(subTask.subTasks) |
|
.concat(subTask.priorities) |
|
.concat(subTask.completed) |
|
}) |
|
crawler = newCards |
|
} while (crawler.length > 0) |
|
} |
|
}) |
|
break |
|
case 'task-dropped': |
|
theTask.deck = theTask.deck.filter(d => d !== ev.memberId) |
|
clearPassesTo(tasks, theTask, ev.memberId) |
|
break |
|
case 'pile-dropped': |
|
if (!ev.memberId) { |
|
break |
|
} |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.taskId) { |
|
clearPassesTo(tasks, task, ev.memberId) |
|
let crawler = [ev.taskId] |
|
let history = [] |
|
let newCards = [] |
|
do { |
|
newCards = [] |
|
crawler.forEach(t => { |
|
if (history.indexOf(t) >= 0) return |
|
let subTask = tasks.filter(pst => pst.taskId === t) |
|
if (subTask.length < 1) { |
|
console.log('missing subtask, this is messy') |
|
return |
|
} |
|
if (subTask.length > 1) { |
|
console.log('duplicate task found, this is very bad') |
|
} |
|
subTask = subTask[0] |
|
if ( |
|
subTask === undefined || |
|
subTask.subTasks === undefined || |
|
subTask.priorities === undefined || |
|
subTask.completed === undefined |
|
) { |
|
console.log('invalid task data found, this is very bad') |
|
return |
|
} |
|
|
|
history.push(t) |
|
|
|
if ( |
|
subTask.deck.indexOf(ev.memberId) >= 0 && |
|
ev.taskId !== ev.memberId |
|
) { |
|
clearPassesTo(tasks, subTask, ev.memberId) |
|
dropTask(subTask, ev.memberId) |
|
} |
|
newCards = newCards |
|
.concat(subTask.subTasks) |
|
.concat(subTask.priorities) |
|
.concat(subTask.completed) |
|
}) |
|
crawler = newCards |
|
} while (crawler.length > 0) |
|
} |
|
}) |
|
break |
|
case 'task-removed': |
|
for (let i = tasks.length - 1; i >= 0; i--) { |
|
const task = tasks[i] |
|
if (task.taskId === ev.taskId) { |
|
tasks.splice(i, 1) |
|
} |
|
} |
|
tasks.forEach((t, i) => { |
|
t.subTasks = t.subTasks.filter(st => st !== ev.taskId) |
|
t.priorities = t.priorities.filter(st => st !== ev.taskId) |
|
t.completed = t.completed.filter(st => st !== ev.taskId) |
|
t.pins = t.pins.filter(pin => pin.taskId !== ev.taskId) |
|
}) |
|
break |
|
case 'tasks-removed': |
|
for (let i = tasks.length - 1; i >= 0; i--) { |
|
const task = tasks[i] |
|
if (ev.taskIds.includes(task.taskId)) { |
|
tasks.splice(i, 1) |
|
} |
|
} |
|
tasks.forEach((t, i) => { |
|
t.subTasks = t.subTasks.filter(st => !ev.taskIds.includes(st)) |
|
t.priorities = t.priorities.filter(st => !ev.taskIds.includes(st)) |
|
t.completed = t.completed.filter(st => !ev.taskIds.includes(st)) |
|
t.pins = |
|
t.pins && Array.isArray(t.pins) |
|
? t.pins.filter(pin => !ev.taskIds.includes(pin.taskId)) |
|
: [] |
|
}) |
|
break |
|
case 'pile-prioritized': |
|
inTask.priorities = inTask.priorities.concat(inTask.subTasks) |
|
inTask.subTasks = [] |
|
break |
|
case 'task-refocused': |
|
atomicCardPlay( |
|
tasks, |
|
{ taskId: ev.taskId, inId: ev.inId, zone: 'priorities' }, |
|
{ taskId: ev.taskId, inId: ev.inId, zone: 'subTasks' }, |
|
ev.blame |
|
) |
|
break |
|
case 'pile-refocused': |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.inId) { |
|
task.priorities.forEach(stId => { |
|
tasks.forEach(st => { |
|
if (st.taskId === stId) { |
|
if (st.claimed && st.claimed.length >= 1) { |
|
task.completed.push(stId) |
|
} else { |
|
task.subTasks.push(stId) |
|
} |
|
} |
|
}) |
|
task.priorities = [] |
|
if (task.allocations && Array.isArray(task.allocations)) { |
|
task.allocations.forEach(allocation => { |
|
task.boost += allocation.amount |
|
}) |
|
task.allocations = [] |
|
} |
|
}) |
|
} |
|
}) |
|
break |
|
case 'task-played': |
|
atomicCardPlay(tasks, ev.from, ev.to, ev.memberId) |
|
break |
|
case 'task-sub-tasked': |
|
atomicCardPlay( |
|
tasks, |
|
{ taskId: ev.subTask }, |
|
{ taskId: ev.subTask, inId: ev.taskId, zone: 'subTasks' }, |
|
ev.memberId |
|
) |
|
break |
|
case 'task-de-sub-tasked': |
|
atomicCardPlay( |
|
tasks, |
|
{ taskId: ev.subTask, inId: ev.taskId, zone: 'subTasks' }, |
|
{ taskId: ev.subTask, zone: 'discard' }, |
|
ev.memberId |
|
) |
|
break |
|
case 'task-prioritized': |
|
atomicCardPlay( |
|
tasks, |
|
{ taskId: ev.taskId, inId: ev.inId, zone: 'card' }, |
|
{ taskId: ev.taskId, inId: ev.inId, zone: 'priorities' }, |
|
ev.memberId |
|
) |
|
break |
|
case 'grid-pin': |
|
if (typeof ev.y === 'string') { |
|
ev.y = parseInt(ev.y) |
|
} |
|
if (typeof ev.x === 'string') { |
|
ev.x = parseInt(ev.x) |
|
} |
|
atomicCardPlay( |
|
tasks, |
|
{ taskId: ev.taskId, inId: ev.inId, zone: 'subTasks' }, |
|
{ |
|
taskId: ev.taskId, |
|
inId: ev.inId, |
|
zone: 'grid', |
|
coords: { y: parseInt(ev.y), x: parseInt(ev.x) }, |
|
}, |
|
ev?.memberId |
|
) |
|
break |
|
case 'grid-unpin': |
|
//console.log("event type is", ev.type) |
|
atomicCardPlay( |
|
tasks, |
|
{ |
|
taskId: ev.taskId, |
|
inId: ev.inId, |
|
zone: 'grid', |
|
coords: { y: parseInt(ev.y), x: parseInt(ev.x) }, |
|
}, |
|
{ |
|
taskId: ev.taskId, |
|
inId: ev.inId, |
|
zone: 'subTasks', |
|
coords: { y: 0 }, |
|
}, |
|
ev.memberId |
|
) |
|
break |
|
case 'task-emptied': |
|
let updateParents = [] |
|
const emptiedParent = theTask |
|
updateParents = [...theTask.priorities, ...theTask.subTasks] |
|
theTask.priorities = [] |
|
theTask.subTasks = [] |
|
tasks.forEach(task => { |
|
if (updateParents.indexOf(task.taskId) >= 0) { |
|
removeParentIfNotParent(task, emptiedParent) |
|
} |
|
}) |
|
break |
|
case 'task-guilded': |
|
theTask.guild = ev.guild |
|
break |
|
case 'task-property-set': |
|
let properties = ev.property.split('.') |
|
|
|
if (properties?.length >= 2) { |
|
if ( |
|
!theTask.hasOwnProperty(properties[0]) || |
|
typeof theTask[properties[0]] != 'object' |
|
) { |
|
theTask[properties[0]] = {} |
|
} |
|
theTask[properties[0]][properties[1]] = ev.value |
|
} else { |
|
theTask[ev.property] = ev.value |
|
} |
|
if (ev.property === 'pinboard.spread') { |
|
unpinTasksOutOfBounds(tasks, theTask) |
|
} |
|
break |
|
case 'task-colored': |
|
theTask.color = ev.color |
|
|
|
if (ev.inId) { |
|
addSubTask(inTask, ev.taskId) |
|
} |
|
break |
|
case 'task-claimed': |
|
let paid = parseFloat(ev.paid) > 0 ? parseFloat(ev.paid) : 0 |
|
let bounty = 0 |
|
tasks.forEach(task => { |
|
let found = false |
|
task.priorities.some(taskId => { |
|
if (taskId !== ev.taskId) { |
|
return false |
|
} else { |
|
found = true |
|
return true |
|
} |
|
}) |
|
|
|
task.subTasks.some(taskId => { |
|
if (taskId !== ev.taskId) { |
|
return false |
|
} else { |
|
found = true |
|
return true |
|
} |
|
}) |
|
|
|
if (found) { |
|
if (task.priorities.indexOf(ev.taskId) === -1) { |
|
task.subTasks = task.subTasks.filter(tId => tId !== ev.subTask) |
|
task.completed = task.completed.filter(tId => tId !== ev.subTask) |
|
task.completed.push(ev.taskId) |
|
} |
|
// let alloc = false |
|
if (task.allocations && Array.isArray(task.allocations)) { |
|
task.allocations = task.allocations.filter(al => { |
|
if (al.allocatedId === ev.taskId) { |
|
bounty += al.amount |
|
return false |
|
} |
|
return true |
|
}) |
|
} |
|
} |
|
if (task.taskId === ev.taskId) { |
|
clearPassesTo(tasks, task, ev.memberId) |
|
if (task.deck.indexOf(ev.memberId) === -1) { |
|
if (ev.taskId !== ev.memberId && ev.memberId) { |
|
task.deck.push(ev.memberId) |
|
} |
|
} |
|
if (task.claimed.indexOf(ev.memberId) === -1) { |
|
task.claimed.push(ev.memberId) |
|
} |
|
task.lastClaimed = ev.timestamp |
|
} |
|
}) |
|
tasks.forEach(task => { |
|
if (task.taskId === ev.memberId) { |
|
task.boost += paid + bounty |
|
} |
|
}) |
|
break |
|
case 'task-unclaimed': |
|
theTask.claimed = theTask.claimed.filter(mId => mId !== ev.memberId) |
|
if (theTask.claimed.length < 1) { |
|
tasks.forEach(p => { |
|
if ( |
|
p.priorities.indexOf(ev.taskId) === -1 && |
|
p.completed.indexOf(ev.taskId) > -1 |
|
) { |
|
p.completed = p.completed.filter(taskId => taskId !== ev.taskId) |
|
addSubTask(p, ev.taskId) |
|
} |
|
}) |
|
} |
|
break |
|
case 'task-reset': |
|
const clearAllCheckmarksFromTask = taskToReset => { |
|
if (!taskToReset) return |
|
taskToReset.claimed = [] |
|
taskToReset.lastClaimed = ev.timestamp |
|
} |
|
if (theTask.uncheckThisCard) { |
|
clearAllCheckmarksFromTask(theTask) |
|
} |
|
if ( |
|
theTask.uncheckPriorities && |
|
theTask.priorities && |
|
theTask.priorities.length >= 1 |
|
) { |
|
theTask.priorities.forEach(tId => { |
|
const priorityCard = getTask(tasks, tId) |
|
clearAllCheckmarksFromTask(priorityCard) |
|
}) |
|
} |
|
if (theTask.uncheckPinned && theTask.pins && theTask.pins.length >= 1) { |
|
theTask.pins.forEach(pin => { |
|
const pinnedCard = getTask(tasks, pin.taskId) |
|
clearAllCheckmarksFromTask(pinnedCard) |
|
}) |
|
} |
|
break |
|
case 'task-boosted': |
|
let amount = parseFloat(ev.amount) |
|
let boost = parseFloat(theTask.boost) |
|
if (amount > 0) { |
|
theTask.boost = amount + boost |
|
theTask.address = '' |
|
} |
|
break |
|
case 'task-boosted-lightning': |
|
const taskToBoostLightning = getTaskBy( |
|
tasks, |
|
ev.payment_hash, |
|
'payment_hash' |
|
) |
|
let amountToBoost = parseFloat(ev.amount) |
|
let boostLightning = parseFloat(taskToBoostLightning.boost) |
|
if (amountToBoost > 0) { |
|
taskToBoostLightning.boost = amountToBoost + boostLightning |
|
taskToBoostLightning.bolt11 = '' |
|
taskToBoostLightning.payment_hash = '' |
|
} |
|
break |
|
case 'task-allocated': |
|
if (!Number.isInteger(ev.amount) || ev.amount < 0) { |
|
break |
|
} |
|
if ( |
|
!theTask.hasOwnProperty('allocations') || |
|
!Array.isArray(theTask.allocations) |
|
) { |
|
theTask.allocations = [] |
|
} |
|
if (ev.amount === 0) { |
|
theTask.allocations = theTask.allocations.filter(als => { |
|
if (als.allocatedId == ev.allocatedId) { |
|
theTask.boost += als.amount |
|
return false |
|
} |
|
return true |
|
}) |
|
break |
|
} |
|
const alreadyPointed = theTask.allocations.some(als => { |
|
const diff = ev.amount - als.amount |
|
if (als.allocatedId !== ev.allocatedId) return false |
|
if (diff > theTask.boost) return true |
|
theTask.boost -= diff |
|
als.amount = ev.amount |
|
return true |
|
}) |
|
if (!alreadyPointed) { |
|
theTask.allocations.push(ev) |
|
theTask.boost -= ev.amount |
|
} |
|
addPriority(theTask, ev.allocatedId) |
|
break |
|
case 'resource-booked': |
|
theTask.book = ev |
|
break |
|
case 'resource-used': |
|
const charged = parseFloat(ev.charged) |
|
if (charged > 0) { |
|
memberTask.boost -= charged |
|
|
|
const resourceCard = theTask |
|
resourceCard.boost += charged |
|
} |
|
break |
|
case 'invoice-created': |
|
theTask.payment_hash = ev.payment_hash |
|
theTask.bolt11 = ev.bolt11 |
|
break |
|
case 'task-swapped': |
|
let task |
|
tasks.forEach(t => { |
|
if (t.taskId === ev.taskId) { |
|
task = t |
|
} |
|
}) |
|
|
|
if (task) { |
|
let originalIndex = task.subTasks.indexOf(ev.swapId1) |
|
let swapIndex = task.subTasks.indexOf(ev.swapId2) |
|
|
|
let originalIndexCompleted = task.completed.indexOf(ev.swapId1) |
|
let swapIndexCompleted = task.completed.indexOf(ev.swapId2) |
|
|
|
if (originalIndex > -1 && swapIndex > -1) { |
|
let newST = task.subTasks.slice() |
|
newST[originalIndex] = ev.swapId2 |
|
newST[swapIndex] = ev.swapId1 |
|
task.subTasks = newST |
|
} |
|
|
|
if (originalIndexCompleted > -1 && swapIndexCompleted > -1) { |
|
let newCompleted = task.completed.slice() |
|
newCompleted[originalIndexCompleted] = ev.swapId2 |
|
newCompleted[swapIndexCompleted] = ev.swapId1 |
|
task.completed = newCompleted |
|
} |
|
} |
|
break |
|
case 'task-bumped': |
|
let taskB |
|
tasks.forEach(t => { |
|
if (t.taskId === ev.taskId) { |
|
taskB = t |
|
} |
|
}) |
|
|
|
if (taskB) { |
|
let originalIndex = taskB.subTasks.indexOf(ev.bumpId) |
|
let originalIndexCompleted = taskB.completed.indexOf(ev.bumpId) |
|
if ( |
|
originalIndex === taskB.subTasks.length - 1 && |
|
ev.direction === -1 |
|
) { |
|
let newST = [ev.bumpId] |
|
newST = newST.concat( |
|
taskB.subTasks.slice(0, taskB.subTasks.length - 1) |
|
) |
|
taskB.subTasks = newST |
|
} |
|
|
|
if (originalIndex === 0 && ev.direction === 1) { |
|
let newST = taskB.subTasks.slice(1) |
|
newST.push(ev.bumpId) |
|
taskB.subTasks = newST |
|
} |
|
} |
|
break |
|
case 'tasks-received': |
|
ev.tasks.forEach(newT => { |
|
let existingTask = getTask(tasks, newT.taskId) |
|
if (!existingTask) { |
|
existingTask = blankCard( |
|
newT.taskId, |
|
newT.name, |
|
newT.color, |
|
newT.timestamp, |
|
newT.parents, |
|
newT.height, |
|
newT.width |
|
) |
|
tasks.push(existingTask) |
|
} |
|
safeMerge(existingTask, newT) |
|
}) |
|
|
|
// Loop through the new cards and remove invalid references to cards that don't exist on this server |
|
/*changedIndexes.forEach(tId => { |
|
const t = tasks[tId] |
|
let beforeLength = t.subTasks.length |
|
t.subTasks = t.subTasks.filter(stId => taskExists(tasks, stId)) |
|
t.priorities = t.priorities.filter(stId => taskExists(tasks, stId)) |
|
t.completed = t.completed.filter(stId => taskExists(tasks, stId)) |
|
t.deck = t.deck.filter(stId => |
|
tasks.some(sst => sst.taskId === stId && sst.taskId === sst.name) |
|
) |
|
if (t?.grid?.rows && Object.keys(t.grid.rows).length >= 1) { |
|
let filteredRows = {} |
|
Object.entries(t.grid.rows).forEach(([x, row]) => { |
|
let filteredRow = {} |
|
|
|
if (row) { |
|
Object.entries(row).forEach(([y, stId]) => { |
|
if (taskExists(tasks, stId)) { |
|
filteredRow[y] = stId |
|
} |
|
}) |
|
if (Object.keys(filteredRow).length < 1) { |
|
filteredRows[x] = {} |
|
} else { |
|
filteredRows[x] = filteredRow |
|
} |
|
} |
|
}) |
|
t.grid.rows = filteredRows |
|
} |
|
})*/ |
|
break |
|
case 'task-visited': |
|
// Remove the avatar from everywhere else |
|
tasks.forEach(task => { |
|
if (task.hasOwnProperty('avatars')) { |
|
task.avatars = task.avatars.filter( |
|
avatarLocation => avatarLocation.memberId !== ev.memberId |
|
) |
|
} |
|
}) |
|
if (!theTask.hasOwnProperty('avatars')) { |
|
theTask.avatars = [] |
|
} |
|
theTask.avatars.push({ |
|
memberId: ev.memberId, |
|
timestamp: ev.timestamp, |
|
area: ev.area, |
|
}) |
|
theTask.lastClaimed = ev.timestamp |
|
break |
|
case 'member-charged': |
|
memberTask.boost -= parseFloat(ev.charged) |
|
if (memberTask.boost < 0) { |
|
memberTask.boost = 0 |
|
} |
|
break |
|
case 'grid-created': |
|
tasks.push( |
|
blankCard( |
|
ev.taskId, |
|
ev.name, |
|
ev.color, |
|
ev.timestamp, |
|
ev.deck, |
|
ev.height, |
|
ev.width |
|
) |
|
) |
|
break |
|
case 'grid-added': |
|
theTask.pinboard = blankPinboard(ev.height, ev.width, ev.spread) |
|
break |
|
case 'grid-removed': |
|
theTask.pins = [] |
|
theTask.pinboard = false |
|
break |
|
case 'grid-resized': |
|
if (!theTask.pinboard) { |
|
theTask.pinboard = blankPinboard(ev.height, ev.width) |
|
} |
|
theTask.pinboard.height = ev.height |
|
theTask.pinboard.width = ev.width |
|
theTask.pinboard.size = ev.size || 9 |
|
unpinTasksOutOfBounds(tasks, theTask) |
|
break |
|
} |
|
} |
|
|
|
export default { |
|
aoMuts, |
|
cashMuts, |
|
membersMuts, |
|
resourcesMuts, |
|
memesMuts, |
|
sessionsMuts, |
|
tasksMuts, |
|
POTENTIALS_TO_EXECUTE, |
|
}
|
|
|