From ce828945be9a7c61f1192c5a61d6b9a42b066e36 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 20 Feb 2024 14:28:50 +0100 Subject: [PATCH 01/21] feat: Add game to post --- Helpers.js | 10 +- migration-updates/add-game-to-posts.js | 24 + migrations/create-post.js | 3 + models/Post.js | 1 + routes/Auth.js | 2 +- routes/Post.js | 1062 ++++++++++++------------ 6 files changed, 573 insertions(+), 529 deletions(-) create mode 100644 migration-updates/add-game-to-posts.js diff --git a/Helpers.js b/Helpers.js index a01f688..58cac79 100644 --- a/Helpers.js +++ b/Helpers.js @@ -965,6 +965,7 @@ const fullPostAttributes = [ 'totalReposts', 'totalRatings', 'totalLinks', + 'game' ] // todo: replace all use cases with const fullPostAttributes above @@ -984,6 +985,7 @@ function findFullPostAttributes(model, accountId) { 'totalReposts', 'totalRatings', 'totalLinks', + 'game', // accountLike('post', model, accountId), // accountComment('post', model, accountId), // accountLink('post', model, accountId), @@ -1630,12 +1632,10 @@ function sendGBGInvite(player, postId, creator, settings) {
Allowed bead types: ${allowedBeadTypes.join(',')}
- Time window for moves: ${ - moveTimeWindow ? `${moveTimeWindow} minutes` : 'Off' + Time window for moves: ${moveTimeWindow ? `${moveTimeWindow} minutes` : 'Off' }
- Character limit: ${ - characterLimit ? `${characterLimit} characters` : 'Off' + Character limit: ${characterLimit ? `${characterLimit} characters` : 'Off' }
Audio time limit: ${moveDuration ? `${moveDuration} seconds` : 'Off'} @@ -1700,6 +1700,7 @@ function createPost(data, files, accountId) { event, poll, glassBeadGame, + game, card, color, watermark, @@ -1721,6 +1722,7 @@ function createPost(data, files, accountId) { color: color || null, watermark: !!watermark, lastActivity: new Date(), + game, }) // todo: add the correct notification type diff --git a/migration-updates/add-game-to-posts.js b/migration-updates/add-game-to-posts.js new file mode 100644 index 0000000..03ac257 --- /dev/null +++ b/migration-updates/add-game-to-posts.js @@ -0,0 +1,24 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.addColumn( + 'Posts', + 'game', + { + type: Sequelize.DataTypes.JSON, + }, + { transaction: t } + ), + ]) + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.removeColumn('Posts', 'game', { transaction: t }), + ]) + }) + }, +} diff --git a/migrations/create-post.js b/migrations/create-post.js index 42e91fa..9172eee 100644 --- a/migrations/create-post.js +++ b/migrations/create-post.js @@ -59,6 +59,9 @@ module.exports = { totalGlassBeadGames: { type: Sequelize.INTEGER, }, + game: { + type: Sequelize.JSON, + }, lastActivity: { type: Sequelize.DATE, }, diff --git a/models/Post.js b/models/Post.js index c27f000..cce0993 100644 --- a/models/Post.js +++ b/models/Post.js @@ -25,6 +25,7 @@ module.exports = (sequelize, DataTypes) => { totalReposts: DataTypes.INTEGER, totalRatings: DataTypes.INTEGER, totalGlassBeadGames: DataTypes.INTEGER, + game: DataTypes.JSON, lastActivity: DataTypes.DATE, }, {} diff --git a/routes/Auth.js b/routes/Auth.js index 0abd291..dece6b5 100644 --- a/routes/Auth.js +++ b/routes/Auth.js @@ -12,7 +12,7 @@ const jwt = require('jsonwebtoken') const sgMail = require('@sendgrid/mail') const webpush = require('web-push') // webpush.setGCMAPIKey('') -webpush.setVapidDetails('https://weco.io', vapidPublicKey, vapidPrivateKey) +// webpush.setVapidDetails('https://weco.io', vapidPublicKey, vapidPrivateKey) sgMail.setApiKey(process.env.SENDGRID_API_KEY) const authenticateToken = require('../middleware/authenticateToken') diff --git a/routes/Post.js b/routes/Post.js index be14a45..514628f 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -1267,15 +1267,15 @@ router.get('/card-faces', async (req, res) => { }) const linkToImage = linkToImageBlock ? await Link.findOne({ - where: { - itemAType: 'image-block', - itemAId: linkToImageBlock.itemBId, - itemBType: 'image', - state: 'active', - }, - attributes: [], - include: { model: Image, attributes: ['url'] }, - }) + where: { + itemAType: 'image-block', + itemAId: linkToImageBlock.itemBId, + itemBType: 'image', + state: 'active', + }, + attributes: [], + include: { model: Image, attributes: ['url'] }, + }) : null blocks.push({ ...link.Post.dataValues, @@ -1304,71 +1304,71 @@ router.post('/create-post', authenticateToken, async (req, res) => { // add spaces and increment space stats const addSpaces = spaceIds ? await new Promise(async (resolve) => { - const addDirectSpaces = await Promise.all( - spaceIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'direct') - ) - ) - // gather direct spaces ancestor ids - const spaces = await Space.findAll({ - where: { id: spaceIds, state: 'active' }, - attributes: ['id'], - include: { - model: Space, - as: 'SpaceAncestors', - attributes: ['id'], - through: { where: { state: 'open' }, attributes: [] }, - }, - }) - let ancestorIds = [] - spaces.forEach((space) => - ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) - ) - // remove duplicates and direct spaces - ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) - // store ancestor ids for response - allSpaceIds.push(...ancestorIds) - const addIndirectSpaces = await Promise.all( - ancestorIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') - ) - ) - // increment space stats - const incrementSpaceStats = await Space.increment('totalPosts', { - where: { id: allSpaceIds }, - silent: true, - }) - Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const addDirectSpaces = await Promise.all( + spaceIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'direct') + ) + ) + // gather direct spaces ancestor ids + const spaces = await Space.findAll({ + where: { id: spaceIds, state: 'active' }, + attributes: ['id'], + include: { + model: Space, + as: 'SpaceAncestors', + attributes: ['id'], + through: { where: { state: 'open' }, attributes: [] }, + }, + }) + let ancestorIds = [] + spaces.forEach((space) => + ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) + ) + // remove duplicates and direct spaces + ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) + // store ancestor ids for response + allSpaceIds.push(...ancestorIds) + const addIndirectSpaces = await Promise.all( + ancestorIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') + ) + ) + // increment space stats + const incrementSpaceStats = await Space.increment('totalPosts', { + where: { id: allSpaceIds }, + silent: true, + }) + Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null // todo: notify source creator const addLink = source ? await new Promise(async (resolve) => { - const createNewLink = await Link.create({ - state: 'active', - creatorId: accountId, - relationship: 'link', - itemAType: source.type, - itemBType: 'post', - itemAId: source.id, - itemBId: post.id, - description: source.linkDescription, - totalLikes: 0, - totalComments: 0, - totalRatings: 0, - }) - const updateSourceLinks = await Post.increment('totalLinks', { - where: { id: source.id }, - silent: true, - }) - const updateTargetLinks = await post.update({ totalLinks: 1 }, { silent: true }) - Promise.all([createNewLink, updateSourceLinks, updateTargetLinks]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const createNewLink = await Link.create({ + state: 'active', + creatorId: accountId, + relationship: 'link', + itemAType: source.type, + itemBType: 'post', + itemAId: source.id, + itemBId: post.id, + description: source.linkDescription, + totalLikes: 0, + totalComments: 0, + totalRatings: 0, + }) + const updateSourceLinks = await Post.increment('totalLinks', { + where: { id: source.id }, + silent: true, + }) + const updateTargetLinks = await post.update({ totalLinks: 1 }, { silent: true }) + Promise.all([createNewLink, updateSourceLinks, updateTargetLinks]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null Promise.all([addSpaces, addLink]) @@ -1404,14 +1404,14 @@ router.post('/create-comment', authenticateToken, async (req, res) => { const createNotification = isOwnPost ? null : await Notification.create({ - ownerId: parentPost.Creator.id, - type: parentPost.type === 'comment' ? 'comment-reply' : 'post-comment', - seen: false, - spaceAId: postData.originSpaceId, - userId: accountId, - postId: parent.id, - commentId: post.id, - }) + ownerId: parentPost.Creator.id, + type: parentPost.type === 'comment' ? 'comment-reply' : 'post-comment', + seen: false, + spaceAId: postData.originSpaceId, + userId: accountId, + postId: parent.id, + commentId: post.id, + }) const muted = await accountMuted(accountId, parentPost.Creator) const skipEmail = isOwnPost || muted || parentPost.Creator.emailsDisabled const messageText = @@ -1419,14 +1419,14 @@ router.post('/create-comment', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: parentPost.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: parentPost.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${parentPost.Creator.name}, ${account.name} just ${messageText} ${parentPost.type} on weco: http://${appURL}/p/${post.id} `, - html: ` + html: `

Hi ${parentPost.Creator.name},
@@ -1436,7 +1436,7 @@ router.post('/create-comment', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) @@ -1468,44 +1468,44 @@ router.post('/create-chat-message', authenticateToken, async (req, res) => { // add spaces and increment space stats const addSpaces = spaceIds ? await new Promise(async (resolve) => { - const addDirectSpaces = await Promise.all( - spaceIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'direct') - ) - ) - // gather direct spaces ancestor ids - const spaces = await Space.findAll({ - where: { id: spaceIds, state: 'active' }, - attributes: ['id'], - include: { - model: Space, - as: 'SpaceAncestors', - attributes: ['id'], - through: { where: { state: 'open' }, attributes: [] }, - }, - }) - let ancestorIds = [] - spaces.forEach((space) => - ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) - ) - // remove duplicates and direct spaces - ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) - // store ancestor ids for response - allSpaceIds.push(...ancestorIds) - const addIndirectSpaces = await Promise.all( - ancestorIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') - ) - ) - // increment space stats - const incrementSpaceStats = await Space.increment('totalPosts', { - where: { id: allSpaceIds }, - silent: true, - }) - Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const addDirectSpaces = await Promise.all( + spaceIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'direct') + ) + ) + // gather direct spaces ancestor ids + const spaces = await Space.findAll({ + where: { id: spaceIds, state: 'active' }, + attributes: ['id'], + include: { + model: Space, + as: 'SpaceAncestors', + attributes: ['id'], + through: { where: { state: 'open' }, attributes: [] }, + }, + }) + let ancestorIds = [] + spaces.forEach((space) => + ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) + ) + // remove duplicates and direct spaces + ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) + // store ancestor ids for response + allSpaceIds.push(...ancestorIds) + const addIndirectSpaces = await Promise.all( + ancestorIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') + ) + ) + // increment space stats + const incrementSpaceStats = await Space.increment('totalPosts', { + where: { id: allSpaceIds }, + silent: true, + }) + Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null // attach parent comment const linkComment = parent ? await attachComment(post, parent, accountId) : null @@ -1631,48 +1631,48 @@ router.post('/create-bead', authenticateToken, async (req, res) => { const notifyPlayers = !synchronous && multiplayer ? await new Promise(async (resolve) => { - // find other players to notify - const otherPlayers = [] - if (gamePost.Players.length) { - // if restricted game, use linked Players - otherPlayers.push(...gamePost.Players.filter((p) => p.id !== accountId)) - } else { - // if open game, use linked Bead Creators - gamePost.Beads.forEach((bead) => { - // filter out game creator and existing records - if ( - bead.Creator.id !== accountId && - !otherPlayers.find((p) => p.id === bead.Creator.id) - ) - otherPlayers.push(bead.Creator) - }) - } - // notify players - const sendNotifications = await Promise.all( - otherPlayers.map( - (p) => - new Promise(async (resolve2) => { - const notifyPlayer = await Notification.create({ - type: 'gbg-move-from-other-player', - ownerId: p.id, - postId: parent.id, - userId: accountId, - seen: false, - }) - const emailPlayer = p.emailsDisabled - ? null - : await sgMail.send({ - to: p.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + // find other players to notify + const otherPlayers = [] + if (gamePost.Players.length) { + // if restricted game, use linked Players + otherPlayers.push(...gamePost.Players.filter((p) => p.id !== accountId)) + } else { + // if open game, use linked Bead Creators + gamePost.Beads.forEach((bead) => { + // filter out game creator and existing records + if ( + bead.Creator.id !== accountId && + !otherPlayers.find((p) => p.id === bead.Creator.id) + ) + otherPlayers.push(bead.Creator) + }) + } + // notify players + const sendNotifications = await Promise.all( + otherPlayers.map( + (p) => + new Promise(async (resolve2) => { + const notifyPlayer = await Notification.create({ + type: 'gbg-move-from-other-player', + ownerId: p.id, + postId: parent.id, + userId: accountId, + seen: false, + }) + const emailPlayer = p.emailsDisabled + ? null + : await sgMail.send({ + to: p.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${p.name}, ${creator.name} just added a new bead. https://${appURL}/p/${parent.id} `, - html: ` + html: `

Hi ${p.name},
@@ -1681,26 +1681,26 @@ router.post('/create-bead', authenticateToken, async (req, res) => { bead.

`, - }) - Promise.all([notifyPlayer, emailPlayer]) - .then(() => resolve2()) - .catch((error) => resolve2(error)) - }) - ) - ) - // schedule next deadline - const scheduleNewDeadline = moveTimeWindow - ? await scheduleNextBeadDeadline( - parent.id, - gamePost.GlassBeadGame, - gamePost.Players - ) - : null + }) + Promise.all([notifyPlayer, emailPlayer]) + .then(() => resolve2()) + .catch((error) => resolve2(error)) + }) + ) + ) + // schedule next deadline + const scheduleNewDeadline = moveTimeWindow + ? await scheduleNextBeadDeadline( + parent.id, + gamePost.GlassBeadGame, + gamePost.Players + ) + : null - Promise.all([sendNotifications, scheduleNewDeadline]) - .then((data) => resolve(data[1])) - .catch((error) => resolve(error)) - }) + Promise.all([sendNotifications, scheduleNewDeadline]) + .then((data) => resolve(data[1])) + .catch((error) => resolve(error)) + }) : null const incrementTotalBeads = await GlassBeadGame.increment('totalBeads', { @@ -1721,7 +1721,7 @@ router.post('/create-bead', authenticateToken, async (req, res) => { // test router.post('/update-post', authenticateToken, async (req, res) => { const accountId = req.user ? req.user.id : null - const { id, mediaTypes, title, text, searchableText, mentions, urls: newUrls } = req.body + const id = req.body.id; const post = await Post.findOne({ where: { id, creatorId: accountId }, attributes: ['id', 'type', 'mediaTypes'], @@ -1733,71 +1733,83 @@ router.post('/update-post', authenticateToken, async (req, res) => { }) if (!post) res.status(401).json({ message: 'Unauthorized' }) else { + const toUpdate = {}; + for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game']) { + if (key in req.body) { + toUpdate[key] = req.body[key] + } + } + const promises = [] const updatePost = await Post.update( - { mediaTypes, title, text, searchableText }, + toUpdate, { where: { id, creatorId: accountId } } ) - // update urls - const oldUrlBlockLinks = await Link.findAll({ - where: { - itemAId: post.id, - itemAType: post.type, - itemBType: 'url-block', - state: 'active', - }, - attributes: ['id', 'itemBId'], - }) - const oldUrlLinks = await Promise.all( - oldUrlBlockLinks.map( - (oldUrlBlockLink) => - new Promise(async (resolve) => { - const oldUrlLink = await Link.findOne({ - where: { - itemAId: oldUrlBlockLink.itemBId, - itemAType: 'url-block', - itemBType: 'url', - state: 'active', - }, - attributes: [], - include: { model: Url, attributes: ['url'] }, + promises.push(updatePost) + if ('urls' in req.body) { + const newUrls = req.body.urls; + // update urls + const oldUrlBlockLinks = await Link.findAll({ + where: { + itemAId: post.id, + itemAType: post.type, + itemBType: 'url-block', + state: 'active', + }, + attributes: ['id', 'itemBId'], + }) + const oldUrlLinks = await Promise.all( + oldUrlBlockLinks.map( + (oldUrlBlockLink) => + new Promise(async (resolve) => { + const oldUrlLink = await Link.findOne({ + where: { + itemAId: oldUrlBlockLink.itemBId, + itemAType: 'url-block', + itemBType: 'url', + state: 'active', + }, + attributes: [], + include: { model: Url, attributes: ['url'] }, + }) + resolve({ id: oldUrlBlockLink.id, url: oldUrlLink.Url.url }) }) - resolve({ id: oldUrlBlockLink.id, url: oldUrlLink.Url.url }) - }) + ) ) - ) - const removeOldUrls = await Promise.all( - oldUrlLinks.map( - (oldUrlLink) => - new Promise(async (resolve) => { - const match = newUrls.find((newUrl) => newUrl.url === oldUrlLink.url) - if (match) resolve() - else { - Link.update({ state: 'deleted' }, { where: { id: oldUrlLink.id } }) - .then(() => resolve()) - .catch((error) => resolve(error)) - } - }) + const removeOldUrls = await Promise.all( + oldUrlLinks.map( + (oldUrlLink) => + new Promise(async (resolve) => { + const match = newUrls.find((newUrl) => newUrl.url === oldUrlLink.url) + if (match) resolve() + else { + Link.update({ state: 'deleted' }, { where: { id: oldUrlLink.id } }) + .then(() => resolve()) + .catch((error) => resolve(error)) + } + }) + ) ) - ) - const addNewUrls = await Promise.all( - newUrls.map( - (newUrl, index) => - new Promise((resolve) => { - const match = oldUrlLinks.find( - (oldUrlLink) => oldUrlLink.url === newUrl.url - ) - if (match) { - Link.update({ index }, { where: { id: match.id } }) - .then(() => resolve()) - .catch((error) => resolve(error)) - } else { - createUrl(accountId, id, post.type, newUrl, index) - .then(() => resolve()) - .catch((error) => resolve(error)) - } - }) + const addNewUrls = await Promise.all( + newUrls.map( + (newUrl, index) => + new Promise((resolve) => { + const match = oldUrlLinks.find( + (oldUrlLink) => oldUrlLink.url === newUrl.url + ) + if (match) { + Link.update({ index }, { where: { id: match.id } }) + .then(() => resolve()) + .catch((error) => resolve(error)) + } else { + createUrl(accountId, id, post.type, newUrl, index) + .then(() => resolve()) + .catch((error) => resolve(error)) + } + }) + ) ) - ) + promises.push(removeOldUrls, addNewUrls) + } // const oldUrls = await post.getBlocks({ // attributes: ['id'], @@ -1845,46 +1857,47 @@ router.post('/update-post', authenticateToken, async (req, res) => { // ) // notify mentions - const mentionedUsers = await User.findAll({ - where: { handle: mentions, state: 'active' }, - attributes: ['id', 'name', 'email', 'emailsDisabled'], - }) + if ('mentions' in req.body) { + const mentionedUsers = await User.findAll({ + where: { handle: req.body.mentions, state: 'active' }, + attributes: ['id', 'name', 'email', 'emailsDisabled'], + }) - const notifyMentions = await Promise.all( - mentionedUsers.map( - (user) => - new Promise(async (resolve) => { - const alreadySent = await Notification.findOne({ - where: { - ownerId: user.id, - type: `${post.type}-mention`, // post, comment, or bead (todo: poll-answer) - userId: accountId, - postId: id, - }, - }) - if (alreadySent) resolve() - else { - const sendNotification = await Notification.create({ - ownerId: user.id, - type: `${post.type}-mention`, - seen: false, - userId: accountId, - postId: id, + const notifyMentions = await Promise.all( + mentionedUsers.map( + (user) => + new Promise(async (resolve) => { + const alreadySent = await Notification.findOne({ + where: { + ownerId: user.id, + type: `${post.type}-mention`, // post, comment, or bead (todo: poll-answer) + userId: accountId, + postId: id, + }, }) - const sendEmail = user.emailsDisabled - ? null - : await sgMail.send({ - to: user.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + if (alreadySent) resolve() + else { + const sendNotification = await Notification.create({ + ownerId: user.id, + type: `${post.type}-mention`, + seen: false, + userId: accountId, + postId: id, + }) + const sendEmail = user.emailsDisabled + ? null + : await sgMail.send({ + to: user.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${user.name}, ${post.Creator.name} just mentioned you in a ${post.type} on weco: http://${appURL}/p/${id} `, - html: ` + html: `

Hi ${user.name},
@@ -1894,18 +1907,23 @@ router.post('/update-post', authenticateToken, async (req, res) => { on weco

`, - }) - Promise.all([sendNotification, sendEmail]) - .then(() => resolve()) - .catch((error) => resolve(error)) - } - }) + }) + Promise.all([sendNotification, sendEmail]) + .then(() => resolve()) + .catch((error) => resolve(error)) + } + }) + ) ) - ) + promises.push(notifyMentions) + } - Promise.all([updatePost, removeOldUrls, addNewUrls, notifyMentions]) + Promise.all(promises) .then(() => res.status(200).json(updatePost)) - .catch((error) => res.status(500).json({ message: 'Error', error })) + .catch((error) => { + console.error(error) + res.status(500).json({ message: 'Error', error }) + }) } }) @@ -1940,28 +1958,28 @@ router.post('/repost-post', authenticateToken, async (req, res) => { const sendNotification = skipNotification ? null : await Notification.create({ - ownerId: post.Creator.id, - type: 'post-repost', - seen: false, - spaceAId: spaceId, - userId: accountId, - postId, - }) + ownerId: post.Creator.id, + type: 'post-repost', + seen: false, + spaceAId: spaceId, + userId: accountId, + postId, + }) const sendEmail = skipEmail ? null : await sgMail.send({ - to: post.Creator.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: post.Creator.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${post.Creator.name}, ${accountName} just reposted your post on weco: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${post.Creator.name},
@@ -1971,7 +1989,7 @@ router.post('/repost-post', authenticateToken, async (req, res) => { on weco

`, - }) + }) const createReactions = await Promise.all( spaceIds.map((id) => @@ -2017,14 +2035,14 @@ router.post('/repost-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.increment('totalPostLikes', { - by: post.totalLikes, - }) + by: post.totalLikes, + }) : await SpaceUserStat.create({ - spaceId: id, - userId: post.Creator.id, - totalPostLikes: post.totalLikes, - totalUnseenMessages: 0, - }) + spaceId: id, + userId: post.Creator.id, + totalPostLikes: post.totalLikes, + totalUnseenMessages: 0, + }) Promise.all([ createSpacePost, incrementTotalPostLikes, @@ -2094,14 +2112,14 @@ router.post('/repost-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.increment('totalPostLikes', { - by: post.totalLikes, - }) + by: post.totalLikes, + }) : await SpaceUserStat.create({ - spaceId: id, - userId: post.Creator.id, - totalPostLikes: post.totalLikes, - totalUnseenMessages: 0, - }) + spaceId: id, + userId: post.Creator.id, + totalPostLikes: post.totalLikes, + totalUnseenMessages: 0, + }) Promise.all([ createSpacePost, updateSpaceStats, @@ -2165,30 +2183,30 @@ router.post('/add-like', authenticateToken, async (req, res) => { const updateSpaceStats = type !== 'link' ? Promise.all( - item.AllPostSpaces.map( - (space) => - new Promise(async (resolve) => { - const updateSpaceStat = await space.increment('totalPostLikes', { - silent: true, - }) - const spaceUserStat = await SpaceUserStat.findOne({ - where: { spaceId: space.id, userId: item.Creator.id }, - attributes: ['id'], - }) - const updateSpaceUserStat = spaceUserStat - ? await spaceUserStat.increment('totalPostLikes') - : await SpaceUserStat.create({ - spaceId: space.id, - userId: item.Creator.id, - totalPostLikes: 1, - totalUnseenMessages: 0, - }) - Promise.all([updateSpaceStat, updateSpaceUserStat]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) - ) - ) + item.AllPostSpaces.map( + (space) => + new Promise(async (resolve) => { + const updateSpaceStat = await space.increment('totalPostLikes', { + silent: true, + }) + const spaceUserStat = await SpaceUserStat.findOne({ + where: { spaceId: space.id, userId: item.Creator.id }, + attributes: ['id'], + }) + const updateSpaceUserStat = spaceUserStat + ? await spaceUserStat.increment('totalPostLikes') + : await SpaceUserStat.create({ + spaceId: space.id, + userId: item.Creator.id, + totalPostLikes: 1, + totalUnseenMessages: 0, + }) + Promise.all([updateSpaceStat, updateSpaceUserStat]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) + ) + ) : null const createReaction = await Reaction.create({ @@ -2220,14 +2238,14 @@ router.post('/add-like', authenticateToken, async (req, res) => { const createNotification = skipNotification ? null : await Notification.create({ - ownerId: item.Creator.id, - type: `${type}-like`, - seen: false, - userId: accountId, - spaceAId, - postId, - commentId, - }) + ownerId: item.Creator.id, + type: `${type}-like`, + seen: false, + userId: accountId, + spaceAId, + postId, + commentId, + }) const { handle, name, flagImagePath } = await User.findOne({ where: { id: accountId }, @@ -2249,14 +2267,14 @@ router.post('/add-like', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: item.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: item.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${item.Creator.name}, ${name} just liked your ${type} on weco: http://${itemUrl} `, - html: ` + html: `

Hi ${item.Creator.name},
@@ -2266,7 +2284,7 @@ router.post('/add-like', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([ updateTotalLikes, @@ -2305,22 +2323,22 @@ router.post('/remove-like', authenticateToken, async (req, res) => { const updateSpaceStats = type === 'post' ? Promise.all( - item.AllPostSpaces.map( - (space) => - new Promise(async (resolve) => { - const updateSpaceStat = await space.decrement('totalPostLikes', { - silent: true, - }) - const updateSpaceUserStat = await SpaceUserStat.decrement( - 'totalPostLikes', - { where: { spaceId: space.id, userId: item.Creator.id } } - ) - Promise.all([updateSpaceStat, updateSpaceUserStat]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) - ) - ) + item.AllPostSpaces.map( + (space) => + new Promise(async (resolve) => { + const updateSpaceStat = await space.decrement('totalPostLikes', { + silent: true, + }) + const updateSpaceUserStat = await SpaceUserStat.decrement( + 'totalPostLikes', + { where: { spaceId: space.id, userId: item.Creator.id } } + ) + Promise.all([updateSpaceStat, updateSpaceUserStat]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) + ) + ) : null const removeReaction = await Reaction.update( @@ -2384,27 +2402,27 @@ router.post('/add-rating', authenticateToken, async (req, res) => { const sendNotification = skipNotification ? null : await Notification.create({ - ownerId: item.Creator.id, - type: `${type}-rating`, - seen: false, - spaceAId: spaceId, - userId: accountId, - postId, - commentId, - }) + ownerId: item.Creator.id, + type: `${type}-rating`, + seen: false, + spaceAId: spaceId, + userId: accountId, + postId, + commentId, + }) const itemUrl = `${appURL}/p/${id}` const sendEmail = skipEmail ? null : await sgMail.send({ - to: item.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: item.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${item.Creator.name}, ${accountName} just rated your ${type} on weco: http://${itemUrl} `, - html: ` + html: `

Hi ${item.Creator.name},
@@ -2414,7 +2432,7 @@ router.post('/add-rating', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([updateTotalRatings, createReaction, sendNotification, sendEmail]) .then(() => res.status(200).json({ message: 'Success' })) @@ -2572,33 +2590,29 @@ router.post('/add-link', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` - Hi ${name}, ${accountName} just linked ${ - type === 'user' ? 'you' : `your ${type}` - } to another ${ - location === 'source' ? sourceType : targetType - } on weco: + to: email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` + Hi ${name}, ${accountName} just linked ${type === 'user' ? 'you' : `your ${type}` + } to another ${location === 'source' ? sourceType : targetType + } on weco: http://${url} `, - html: ` + html: `

Hi ${name},
${accountName} - just linked ${ - type === 'user' - ? `you` - : `your ${type}` + just linked ${type === 'user' + ? `you` + : `your ${type}` } - to another ${ - location === 'source' ? sourceType : targetType + to another ${location === 'source' ? sourceType : targetType } on weco

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) @@ -2672,34 +2686,34 @@ router.post('/respond-to-event', authenticateToken, async (req, res) => { const updateStatus = previousResponse ? UserEvent.update({ state: 'removed' }, { where: { id: previousResponse.id } }) : new Promise(async (resolve) => { - const removeOtherResponseTypes = await UserEvent.update( - { state: 'removed' }, - { where: { userId: accountId, eventId, state: 'active' } } - ) - - const newResponse = await UserEvent.create({ - userId: accountId, - eventId, - relationship: response, - state: 'active', - }) - - const scheduleReminder = await scheduleEventNotification({ - type: response, - postId, - eventId, - userEventId: newResponse.id, - startTime, - userId: accountId, - userName: user.name, - userEmail: user.email, - emailsDisabled: user.emailsDisabled, - }) - - Promise.all([removeOtherResponseTypes, scheduleReminder]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const removeOtherResponseTypes = await UserEvent.update( + { state: 'removed' }, + { where: { userId: accountId, eventId, state: 'active' } } + ) + + const newResponse = await UserEvent.create({ + userId: accountId, + eventId, + relationship: response, + state: 'active', + }) + + const scheduleReminder = await scheduleEventNotification({ + type: response, + postId, + eventId, + userEventId: newResponse.id, + startTime, + userId: accountId, + userName: user.name, + userEmail: user.email, + emailsDisabled: user.emailsDisabled, + }) + + Promise.all([removeOtherResponseTypes, scheduleReminder]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) updateStatus .then(() => res.status(200).json({ message: 'Success' })) @@ -2752,89 +2766,89 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { const { type, action, threshold } = post.Poll const executeAction = action ? Promise.all( - voteData.map( - (answer) => - new Promise(async (resolve1) => { - // find poll answer - const pollAnswer = await Post.findOne({ - where: { id: answer.id }, - attributes: ['id', 'text'], - include: { - model: Reaction, - where: { type: 'vote', state: 'active' }, - required: false, - attributes: ['value'], - }, - }) - const answerLink = await Link.findOne({ - where: { - itemAId: postId, - itemAType: 'post', - itemBId: answer.id, - itemBType: 'poll-answer', - }, - attributes: ['id', 'state'], - }) - const { text, Reactions } = pollAnswer - let totalVotes - if (type === 'weighted-choice') - totalVotes = - Reactions.map((r) => +r.value).reduce((a, b) => a + b, 0) / - 100 - else totalVotes = Reactions.length - const createSpace = - action === 'Create spaces' && - answerLink.state !== 'done' && - totalVotes >= threshold - ? new Promise(async (resolve2) => { - const markAnswerDone = await answerLink.update({ - state: 'done', - }) - const newSpace = await Space.create({ - creatorId: post.Creator.id, - handle: uuidv4().substring(0, 15), - name: text, - description: null, - state: 'active', - privacy: 'public', - totalPostLikes: 0, - totalPosts: 0, - totalComments: 0, - totalFollowers: 1, - }) - const createModRelationship = SpaceUser.create({ - relationship: 'moderator', - state: 'active', - spaceId: newSpace.id, - userId: post.Creator.id, - }) - const createFollowerRelationship = SpaceUser.create({ - relationship: 'follower', - state: 'active', - spaceId: newSpace.id, - userId: post.Creator.id, - }) - const attachToParent = await attachParentSpace( - newSpace.id, - post.Poll.spaceId - ) - Promise.all([ - markAnswerDone, - createModRelationship, - createFollowerRelationship, - attachToParent, - ]) - .then(() => resolve2()) - .catch((error) => resolve2(error)) + voteData.map( + (answer) => + new Promise(async (resolve1) => { + // find poll answer + const pollAnswer = await Post.findOne({ + where: { id: answer.id }, + attributes: ['id', 'text'], + include: { + model: Reaction, + where: { type: 'vote', state: 'active' }, + required: false, + attributes: ['value'], + }, + }) + const answerLink = await Link.findOne({ + where: { + itemAId: postId, + itemAType: 'post', + itemBId: answer.id, + itemBType: 'poll-answer', + }, + attributes: ['id', 'state'], + }) + const { text, Reactions } = pollAnswer + let totalVotes + if (type === 'weighted-choice') + totalVotes = + Reactions.map((r) => +r.value).reduce((a, b) => a + b, 0) / + 100 + else totalVotes = Reactions.length + const createSpace = + action === 'Create spaces' && + answerLink.state !== 'done' && + totalVotes >= threshold + ? new Promise(async (resolve2) => { + const markAnswerDone = await answerLink.update({ + state: 'done', }) - : null - - Promise.all([createSpace]) - .then(() => resolve1()) - .catch((error) => resolve1(error)) - }) - ) - ) + const newSpace = await Space.create({ + creatorId: post.Creator.id, + handle: uuidv4().substring(0, 15), + name: text, + description: null, + state: 'active', + privacy: 'public', + totalPostLikes: 0, + totalPosts: 0, + totalComments: 0, + totalFollowers: 1, + }) + const createModRelationship = SpaceUser.create({ + relationship: 'moderator', + state: 'active', + spaceId: newSpace.id, + userId: post.Creator.id, + }) + const createFollowerRelationship = SpaceUser.create({ + relationship: 'follower', + state: 'active', + spaceId: newSpace.id, + userId: post.Creator.id, + }) + const attachToParent = await attachParentSpace( + newSpace.id, + post.Poll.spaceId + ) + Promise.all([ + markAnswerDone, + createModRelationship, + createFollowerRelationship, + attachToParent, + ]) + .then(() => resolve2()) + .catch((error) => resolve2(error)) + }) + : null + + Promise.all([createSpace]) + .then(() => resolve1()) + .catch((error) => resolve1(error)) + }) + ) + ) : null const skipNotification = post.Creator.id === accountId @@ -2843,27 +2857,27 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { const createNotification = skipNotification ? null : await Notification.create({ - ownerId: post.Creator.id, - type: 'poll-vote', - seen: false, - userId: accountId, - postId, - }) + ownerId: post.Creator.id, + type: 'poll-vote', + seen: false, + userId: accountId, + postId, + }) const sendEmail = skipEmail ? null : await sgMail.send({ - to: post.Creator.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: post.Creator.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${post.Creator.name}, ${userName} just voted on your Poll: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${post.Creator.name},
@@ -2872,7 +2886,7 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { Poll

`, - }) + }) const updateLastPostActivity = await Post.update( { lastActivity: new Date() }, @@ -3062,8 +3076,8 @@ router.post('/delete-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.update({ - totalPostLikes: spaceUserStat.totalPostLikes - post.totalLikes, - }) + totalPostLikes: spaceUserStat.totalPostLikes - post.totalLikes, + }) : null Promise.all([updateSpace, updateSpaceUserStat]) .then(() => resolve()) From 1042920bde597b746ab9fb79a061f6c7df721000 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 20 Feb 2024 14:29:21 +0100 Subject: [PATCH 02/21] fix --- routes/Auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/Auth.js b/routes/Auth.js index dece6b5..0abd291 100644 --- a/routes/Auth.js +++ b/routes/Auth.js @@ -12,7 +12,7 @@ const jwt = require('jsonwebtoken') const sgMail = require('@sendgrid/mail') const webpush = require('web-push') // webpush.setGCMAPIKey('') -// webpush.setVapidDetails('https://weco.io', vapidPublicKey, vapidPrivateKey) +webpush.setVapidDetails('https://weco.io', vapidPublicKey, vapidPrivateKey) sgMail.setApiKey(process.env.SENDGRID_API_KEY) const authenticateToken = require('../middleware/authenticateToken') From 410a057f4986f7e0964a47d4fd268cdbb847d9b2 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Thu, 29 Feb 2024 16:49:16 +0100 Subject: [PATCH 03/21] add play too --- Helpers.js | 340 +++++++++++++------------ migration-updates/add-play-to-posts.js | 24 ++ models/Post.js | 1 + routes/Post.js | 10 +- 4 files changed, 202 insertions(+), 173 deletions(-) create mode 100644 migration-updates/add-play-to-posts.js diff --git a/Helpers.js b/Helpers.js index 58cac79..3c79644 100644 --- a/Helpers.js +++ b/Helpers.js @@ -301,14 +301,14 @@ function notifyMention(creator, user, postId, type) { const sendEmail = skipEmail ? null : await sgMail.send({ - to: user.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: user.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${user.name}, ${creator.name} just mentioned you in a ${type} on weco: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${user.name},
@@ -318,7 +318,7 @@ function notifyMention(creator, user, postId, type) { on weco

`, - }) + }) Promise.all([sendNotification, sendEmail]) .then(() => resolve()) @@ -862,7 +862,7 @@ function totalSpaceResults(filters) { const endDate = createSQLDate(new Date()) return depth === 'Deep' ? [ - literal(`( + literal(`( SELECT COUNT(*) FROM Spaces s WHERE s.id != Space.id @@ -880,10 +880,10 @@ function totalSpaceResults(filters) { OR s.description LIKE '%${search}%' ) AND s.createdAt BETWEEN '${startDate}' AND '${endDate}' )`), - 'totalResults', - ] + 'totalResults', + ] : [ - literal(`( + literal(`( SELECT COUNT(*) FROM Spaces s WHERE s.state = 'active' @@ -899,8 +899,8 @@ function totalSpaceResults(filters) { OR s.description LIKE '%${search}%' ) AND s.createdAt BETWEEN '${startDate}' AND '${endDate}' )`), - 'totalResults', - ] + 'totalResults', + ] } } @@ -965,7 +965,8 @@ const fullPostAttributes = [ 'totalReposts', 'totalRatings', 'totalLinks', - 'game' + 'game', + 'play' ] // todo: replace all use cases with const fullPostAttributes above @@ -986,6 +987,7 @@ function findFullPostAttributes(model, accountId) { 'totalRatings', 'totalLinks', 'game', + 'play', // accountLike('post', model, accountId), // accountComment('post', model, accountId), // accountLink('post', model, accountId), @@ -1607,14 +1609,14 @@ function sendGBGInvite(player, postId, creator, settings) { const sendEmail = player.emailsDisabled ? null : await sgMail.send({ - to: player.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: player.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${player.name}, ${creator.name} just invited you to join a game on weco: https://${appURL}/p/${postId} Log in and go to your notifications to accept or reject the invitation. `, - html: ` + html: `

Hi ${player.name},
@@ -1633,16 +1635,16 @@ function sendGBGInvite(player, postId, creator, settings) { Allowed bead types: ${allowedBeadTypes.join(',')}
Time window for moves: ${moveTimeWindow ? `${moveTimeWindow} minutes` : 'Off' - } + }
Character limit: ${characterLimit ? `${characterLimit} characters` : 'Off' - } + }
Audio time limit: ${moveDuration ? `${moveDuration} seconds` : 'Off'}

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) @@ -1701,6 +1703,7 @@ function createPost(data, files, accountId) { poll, glassBeadGame, game, + play, card, color, watermark, @@ -1723,19 +1726,20 @@ function createPost(data, files, accountId) { watermark: !!watermark, lastActivity: new Date(), game, + play, }) // todo: add the correct notification type - const notifyMentions = mentions.length + const notifyMentions = mentions?.length ? await new Promise(async (resolve) => { - const users = await User.findAll({ - where: { id: mentions, state: 'active' }, - attributes: ['id', 'name', 'email', 'emailsDisabled'], - }) - Promise.all(users.map((user) => notifyMention(creator, user, post.id, type))) - .then(() => resolve()) - .catch((error) => resolve(data, error)) - }) + const users = await User.findAll({ + where: { id: mentions, state: 'active' }, + attributes: ['id', 'name', 'email', 'emailsDisabled'], + }) + Promise.all(users.map((user) => notifyMention(creator, user, post.id, type))) + .then(() => resolve()) + .catch((error) => resolve(data, error)) + }) : null const createUrls = urls @@ -1744,158 +1748,158 @@ function createPost(data, files, accountId) { const createImages = images ? await Promise.all( - images.map((image, i) => createImage(accountId, post.id, type, image, i, files)) - ) + images.map((image, i) => createImage(accountId, post.id, type, image, i, files)) + ) : null const createAudios = audios ? await Promise.all( - audios.map((audio, i) => createAudio(accountId, post.id, type, audio, i, files)) - ) + audios.map((audio, i) => createAudio(accountId, post.id, type, audio, i, files)) + ) : null const createEvent = event ? await Event.create({ - postId: post.id, - state: 'active', - startTime: event.startTime, - endTime: event.endTime, - }) + postId: post.id, + state: 'active', + startTime: event.startTime, + endTime: event.endTime, + }) : null const createPoll = poll ? await new Promise(async (resolve) => { - const { type, answers, locked, governance, action, threshold } = poll - const createPoll = await Poll.create({ - postId: post.id, - type, - answersLocked: locked, - spaceId: governance ? spaceIds[0] : null, - action: action || null, - threshold: threshold || null, - }) - const creatAnswers = await Promise.all( - answers.map((a) => createPollAnswer(a, accountId, post.id, files)) - ) - Promise.all([createPoll, creatAnswers]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const { type, answers, locked, governance, action, threshold } = poll + const createPoll = await Poll.create({ + postId: post.id, + type, + answersLocked: locked, + spaceId: governance ? spaceIds[0] : null, + action: action || null, + threshold: threshold || null, + }) + const creatAnswers = await Promise.all( + answers.map((a) => createPollAnswer(a, accountId, post.id, files)) + ) + Promise.all([createPoll, creatAnswers]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null const createGBG = glassBeadGame ? await new Promise(async (resolve) => { - const { settings, topicImage, topicGroup, beads, sourcePostId } = glassBeadGame - const imageFile = files.find((file) => file.originalname === topicImage.id) - const { players } = settings - const createGame = await GlassBeadGame.create({ - postId: post.id, - state: 'active', - locked: false, - topicGroup, - topicImage: imageFile ? imageFile.url : topicImage.Image.url || null, - synchronous: settings.synchronous, - multiplayer: settings.multiplayer, - allowedBeadTypes: settings.allowedBeadTypes.join(',').toLowerCase(), - playerOrder: players.length ? players.map((p) => p.id).join(',') : null, - totalMoves: settings.totalMoves || null, - movesPerPlayer: settings.movesPerPlayer || null, - moveDuration: settings.moveDuration || null, - moveTimeWindow: settings.moveTimeWindow || null, - characterLimit: settings.characterLimit || null, - introDuration: settings.introDuration || null, - outroDuration: settings.outroDuration || null, - intervalDuration: settings.intervalDuration || null, - nextMoveDeadline: settings.nextMoveDeadline || null, - totalBeads: beads.length + (sourcePostId ? 1 : 0), - }) - - // const linkSourceBead = sourcePostId - // ? await Link.create({ - // state: 'active', - // // type: 'gbg-post', - // index: 0, - // relationship: 'source', - // creatorId: accountId, - // itemAId: post.id, - // itemBId: sourcePostId, - // totalLikes: 0, - // totalComments: 0, - // totalRatings: 0, - // }) - // : null - - // const notifySourceCreator = - // sourcePostId && sourceCreatorId !== accountId - // ? await new Promise(async (Resolve) => { - // const sourceCreator = await User.findOne({ - // where: { id: sourceCreatorId }, - // attributes: ['name', 'email', 'emailsDisabled'], - // }) - // const notifyCreator = await Notification.create({ - // type: 'new-gbg-from-your-post', - // ownerId: sourceCreatorId, - // userId: accountId, - // postId: post.id, - // seen: false, - // }) - // const skipEmail = - // sourceCreator.emailsDisabled || - // (await accountMuted(accountId, sourceCreator)) - // const emailCreator = skipEmail - // ? null - // : await sgMail.send({ - // to: sourceCreator.email, - // from: { - // email: 'admin@weco.io', - // name: 'we { collective }', - // }, - // subject: 'New notification', - // text: ` - // Hi ${sourceCreator.name}, ${creatorName} just created a new glass bead game from your post on weco: https://${appURL}/p/${post.id} - // `, - // html: ` - //

- // Hi ${sourceCreator.name}, - //
- // ${creatorName} - // just created a new glass bead game from your post on weco. - //

- // `, - // }) - // Promise.all([notifyCreator, emailCreator]) - // .then(() => Resolve()) - // .catch((error) => Resolve(error)) - // }) - // : null - - const createBeads = await Promise.all( - beads.map((bead, index) => createBead(bead, index, accountId, post.id, files)) - ) - - const addPlayers = - settings.multiplayer && !!players.length - ? await addGBGPlayers(post.id, creator, settings) - : null - - Promise.all([ - createGame, - // linkSourceBead, - // notifySourceCreator, - createBeads, - addPlayers, - ]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const { settings, topicImage, topicGroup, beads, sourcePostId } = glassBeadGame + const imageFile = files.find((file) => file.originalname === topicImage.id) + const { players } = settings + const createGame = await GlassBeadGame.create({ + postId: post.id, + state: 'active', + locked: false, + topicGroup, + topicImage: imageFile ? imageFile.url : topicImage.Image.url || null, + synchronous: settings.synchronous, + multiplayer: settings.multiplayer, + allowedBeadTypes: settings.allowedBeadTypes.join(',').toLowerCase(), + playerOrder: players.length ? players.map((p) => p.id).join(',') : null, + totalMoves: settings.totalMoves || null, + movesPerPlayer: settings.movesPerPlayer || null, + moveDuration: settings.moveDuration || null, + moveTimeWindow: settings.moveTimeWindow || null, + characterLimit: settings.characterLimit || null, + introDuration: settings.introDuration || null, + outroDuration: settings.outroDuration || null, + intervalDuration: settings.intervalDuration || null, + nextMoveDeadline: settings.nextMoveDeadline || null, + totalBeads: beads.length + (sourcePostId ? 1 : 0), + }) + + // const linkSourceBead = sourcePostId + // ? await Link.create({ + // state: 'active', + // // type: 'gbg-post', + // index: 0, + // relationship: 'source', + // creatorId: accountId, + // itemAId: post.id, + // itemBId: sourcePostId, + // totalLikes: 0, + // totalComments: 0, + // totalRatings: 0, + // }) + // : null + + // const notifySourceCreator = + // sourcePostId && sourceCreatorId !== accountId + // ? await new Promise(async (Resolve) => { + // const sourceCreator = await User.findOne({ + // where: { id: sourceCreatorId }, + // attributes: ['name', 'email', 'emailsDisabled'], + // }) + // const notifyCreator = await Notification.create({ + // type: 'new-gbg-from-your-post', + // ownerId: sourceCreatorId, + // userId: accountId, + // postId: post.id, + // seen: false, + // }) + // const skipEmail = + // sourceCreator.emailsDisabled || + // (await accountMuted(accountId, sourceCreator)) + // const emailCreator = skipEmail + // ? null + // : await sgMail.send({ + // to: sourceCreator.email, + // from: { + // email: 'admin@weco.io', + // name: 'we { collective }', + // }, + // subject: 'New notification', + // text: ` + // Hi ${sourceCreator.name}, ${creatorName} just created a new glass bead game from your post on weco: https://${appURL}/p/${post.id} + // `, + // html: ` + //

+ // Hi ${sourceCreator.name}, + //
+ // ${creatorName} + // just created a new glass bead game from your post on weco. + //

+ // `, + // }) + // Promise.all([notifyCreator, emailCreator]) + // .then(() => Resolve()) + // .catch((error) => Resolve(error)) + // }) + // : null + + const createBeads = await Promise.all( + beads.map((bead, index) => createBead(bead, index, accountId, post.id, files)) + ) + + const addPlayers = + settings.multiplayer && !!players.length + ? await addGBGPlayers(post.id, creator, settings) + : null + + Promise.all([ + createGame, + // linkSourceBead, + // notifySourceCreator, + createBeads, + addPlayers, + ]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null const createCard = card ? await Promise.all( - [card.front, card.back].map((cardFace, index) => - createCardFace(cardFace, index, accountId, post.id, files) - ) - ) + [card.front, card.back].map((cardFace, index) => + createCardFace(cardFace, index, accountId, post.id, files) + ) + ) : null Promise.all([ @@ -2051,14 +2055,14 @@ function scheduleNextBeadDeadline(postId, settings, players) { const sendMoveEmail = nextPlayer.emailsDisabled ? null : await sgMail.send({ - to: nextPlayer.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: nextPlayer.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${nextPlayer.name}, it's your move! Add a new bead to the glass bead game: https://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${nextPlayer.name},
@@ -2067,7 +2071,7 @@ function scheduleNextBeadDeadline(postId, settings, players) { Add a new bead to the glass bead game.

`, - }) + }) const scheduleReminders = await scheduleGBGMoveJobs( postId, nextPlayer, diff --git a/migration-updates/add-play-to-posts.js b/migration-updates/add-play-to-posts.js new file mode 100644 index 0000000..e07a28a --- /dev/null +++ b/migration-updates/add-play-to-posts.js @@ -0,0 +1,24 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.addColumn( + 'Posts', + 'play', + { + type: Sequelize.DataTypes.JSON, + }, + { transaction: t } + ), + ]) + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.removeColumn('Posts', 'play', { transaction: t }), + ]) + }) + }, +} diff --git a/models/Post.js b/models/Post.js index cce0993..425c608 100644 --- a/models/Post.js +++ b/models/Post.js @@ -26,6 +26,7 @@ module.exports = (sequelize, DataTypes) => { totalRatings: DataTypes.INTEGER, totalGlassBeadGames: DataTypes.INTEGER, game: DataTypes.JSON, + play: DataTypes.JSON, lastActivity: DataTypes.DATE, }, {} diff --git a/routes/Post.js b/routes/Post.js index 514628f..c30b155 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -685,7 +685,7 @@ router.get('/post-comments', async (req, res) => { // failed approaches: // + full nested include with no recursive promises (doesn't allow limit beyond first generation) // + get links first instead of using getBlocks function ~1.5s - const { postId, offset, filter } = req.query + const { postId, offset, filter, limit } = req.query const limits = [5, 4, 3, 2, 1] // number of comments to inlcude per generation (length of array determines max depth) const post = await Post.findOne({ where: { id: postId }, @@ -708,7 +708,7 @@ router.get('/post-comments', async (req, res) => { ['id', 'ASC'], ] - async function getChildComments(parent, depth) { + async function getChildComments(parent, depth, limit) { return new Promise(async (resolve) => { const comments = await parent.getBlocks({ attributes: [...fullPostAttributes, 'totalChildComments'], @@ -727,7 +727,7 @@ router.get('/post-comments', async (req, res) => { attributes: ['id', 'handle', 'name', 'flagImagePath'], }, ], - limit: limits[depth], + limit: limit || limits[depth], offset: depth ? 0 : +offset, order, }) @@ -748,7 +748,7 @@ router.get('/post-comments', async (req, res) => { }) } - getChildComments(post, 0) + getChildComments(post, 0, +limit) .then(() => res.status(200).json({ totalChildren: post.totalChildComments, @@ -1734,7 +1734,7 @@ router.post('/update-post', authenticateToken, async (req, res) => { if (!post) res.status(401).json({ message: 'Unauthorized' }) else { const toUpdate = {}; - for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game']) { + for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'play']) { if (key in req.body) { toUpdate[key] = req.body[key] } From 0387de490673cdc7ac4a2a8e95e2802c7c363d65 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 5 Mar 2024 12:42:33 +0100 Subject: [PATCH 04/21] links --- Helpers.js | 11 +++++++++++ models/Post.js | 1 + routes/Post.js | 21 ++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Helpers.js b/Helpers.js index 3c79644..e285e54 100644 --- a/Helpers.js +++ b/Helpers.js @@ -1146,6 +1146,17 @@ function findPostInclude(accountId) { }, }, }, + { + model: Link, + as: 'Plays', + separate: true, + where: { relationship: 'play', state: 'active' }, + order: [['index', 'ASC']], + include: { + model: Post, + attributes: ['id', 'title', 'play', 'state'] + } + }, { model: Reaction, where: { creatorId: accountId, state: 'active' }, diff --git a/models/Post.js b/models/Post.js index 425c608..8c37f02 100644 --- a/models/Post.js +++ b/models/Post.js @@ -50,6 +50,7 @@ module.exports = (sequelize, DataTypes) => { Post.hasMany(models.Link, { as: 'UrlBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'ImageBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'AudioBlocks', foreignKey: 'itemAId' }) + Post.hasMany(models.Link, { as: 'Plays', foreignKey: 'itemAId' }) Post.hasOne(models.Link, { as: 'MediaLink', foreignKey: 'itemAId' }) // used for post map (todo: rethink...) Post.hasMany(models.Link, { as: 'OutgoingPostLinks', foreignKey: 'itemAId' }) diff --git a/routes/Post.js b/routes/Post.js index c30b155..f74d981 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -1350,7 +1350,7 @@ router.post('/create-post', authenticateToken, async (req, res) => { const createNewLink = await Link.create({ state: 'active', creatorId: accountId, - relationship: 'link', + relationship: source.relationship ?? 'link', itemAType: source.type, itemBType: 'post', itemAId: source.id, @@ -3058,6 +3058,25 @@ router.post('/delete-post', authenticateToken, async (req, res) => { { where: { id: postId, creatorId: accountId } } ) + await Link.update( + { state: 'deleted', }, + { + where: { + state: 'active', + [Op.or]: [ + { + itemAType: 'post', + itemAId: postId + }, + { + itemBType: 'post', + itemBId: postId + } + ] + } + } + ); + const updateSpaceStats = await Promise.all( post.AllPostSpaces.map( (space) => From 0df50b74af1dcad4f6dc5b0815e083d6f4397f2c Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 5 Mar 2024 16:32:07 +0100 Subject: [PATCH 05/21] play service --- Play.js | 358 ++++++++++++++++++++++++++++++++++++++++++++++ ScheduledTasks.js | 37 +++-- Socket.js | 3 + 3 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 Play.js diff --git a/Play.js b/Play.js new file mode 100644 index 0000000..6361c4b --- /dev/null +++ b/Play.js @@ -0,0 +1,358 @@ +const sequelize = require('sequelize') +const { Op } = sequelize +const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') +const schedule = require('node-schedule') + +function getFirstLeafStep( + step, + variables, + playerIds +) { + if (!step) { + return undefined + } + switch (step.type) { + case 'game': + throw new Error('TODO') + case 'post': + return { step, variables } + case 'rounds': { + const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) + if (!firstStep) { + return undefined + } + return { + step: firstStep.step, + variables: { + ...firstStep.variables, + [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1, + }, + } + } + case 'turns': { + const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) + if (!firstStep) { + return undefined + } + return { + step: firstStep.step, + variables: { + ...firstStep.variables, + [`${step.id}_player`]: firstStep.variables[`${step.id}_player`] ?? playerIds[0], + }, + } + } + default: { + const exhaustivenessCheck = step + throw exhaustivenessCheck + } + } +} + +const getNextLeafStep = ( + steps, + stepId, + variables, + playerIds +) => { + let currentFound = false + let currentVariables = variables + for (let i = 0; i < steps.length; i++) { + const step = steps[i] + + if (currentFound) { + const nextStep = getFirstLeafStep(step, currentVariables, playerIds) + if (nextStep) { + return nextStep + } + } else { + switch (step.type) { + case 'game': + throw new Error('TODO') + case 'post': + if (step.id === stepId) { + currentFound = true + } + break + case 'rounds': { + const result = getNextLeafStep(step.steps, stepId, currentVariables, playerIds) + if (!result) { + break + } + + if (result.step) { + return result + } + + const roundKey = `${step.id}_round` + const currentRound = currentVariables[roundKey] + if (currentRound < +step.amount) { + const firstStep = getFirstLeafStep( + step, + { + ...result.variables, + [roundKey]: currentRound + 1, + }, + playerIds + ) + if (firstStep) { + return firstStep + } + } + + currentFound = true + currentVariables = omit(currentVariables, roundKey) + break + } + case 'turns': { + const result = getNextLeafStep(step.steps, stepId, currentVariables, playerIds) + if (!result) { + break + } + + if (result.step) { + return result + } + + const playerKey = `${step.id}_player` + const currentPlayerId = result.variables[playerKey] + const currentPlayerIndex = playerIds.indexOf(currentPlayerId) + if (currentPlayerIndex < playerIds.length - 1) { + const firstStep = getFirstLeafStep( + step, + { + ...result.variables, + [playerKey]: playerIds[currentPlayerIndex + 1], + }, + playerIds + ) + if (firstStep) { + return firstStep + } + } + + currentFound = true + currentVariables = omit(currentVariables, playerKey) + break + } + default: { + const exhaustivenessCheck = step + throw exhaustivenessCheck + } + } + } + } + + if (currentFound) { + return { variables: currentVariables } + } + + return undefined +} + +async function endStep(post) { + // TODO +} + +function scheduleEndStep(post) { + schedule.scheduleJob(post.play.stepTimeout, async () => { + const currentPost = await Post.findOne({ + where: { + state: 'active', + id: post.id + } + }) + if (!deepEquals(currentPost?.play, post.play)) { + // The state of the play has changed, we assume a new scheduled job has been triggered. + return + } + endStep(currentPost) + }) +} + +async function initializePlayTasks() { + const plays = await Post.findAll({ + where: { + state: 'active', + 'play': { [Op.not]: null } + } + }) + + for (const post of plays) { + const play = post.play + if (play.status !== 'started') { + continue + } + + const stepTimeout = new Date(play.stepTimeout); + if (stepTimeout < new Date()) { + endStep(post) + } else { + scheduleEndStep(post) + } + } +} + +const EVENTS = { + outgoing: { + updateGame: 'play:outgoing-update-game', + start: 'play:outgoing-start', + next: 'play:outgoing-next', + pause: 'play:outgoing-pause', + stop: 'play:outgoing-stop' + }, + incoming: { + updated: 'play:incoming-updated' + } +} + +function registerPlaySocketEvents(socket, io) { + socket.on(EVENTS.outgoing.updateGame, async ({ id, game }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + const newPlay = { + ...post.play, + game + } + + await Post.update({ + play: { + ...post.play, + game + } + }, { + where: { + id + } + }) + + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.start, async ({ id }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + let newPlay; + if (post.play.status === 'paused') { + newPlay = { + ...post.play, + status: 'started', + stepTimeout: +new Date() + post.play.step.post.timeout + } + } else { + const firstStep = getFirstLeafStep(post.play.game.steps[0]); + + newPlay = { + ...post.play, + status: 'started', + step: firstStep.step, + variables: firstStep.variables, + stepTimeout: + +new Date() + firstStep.step.post.timeout, + } + + } + + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.next, async ({ id }) => { + const post = await Post.findOne({ + where: { + id + } + }) + const play = post.play; + const nextStep = getNextLeafStep( + play.game.steps, + play.step.id, + play.variables, + play.playerIds + ) + const newPlay = nextStep?.step ? { + ...play, + step: nextStep.step, + variables: nextStep.variables + } : { + game: play.game, + gameId: play.gameId, + playerIds: play.playerIds, + status: 'ended', + variables: {} + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.pause, async ({ id }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + const newPlay = { + ...post.play, + status: 'paused' + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.stop, async ({ id }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + const newPlay = { + ...post.play, + status: 'stopped' + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + }) +} + +module.exports = { + registerPlaySocketEvents, + initializePlayTasks +} diff --git a/ScheduledTasks.js b/ScheduledTasks.js index 4671631..b540178 100644 --- a/ScheduledTasks.js +++ b/ScheduledTasks.js @@ -5,6 +5,7 @@ const { Op } = sequelize const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) +const { initializePlayTasks } = require('./Play') function scheduleEventNotification(data) { const { @@ -187,37 +188,33 @@ async function scheduleGBGMoveJobs(postId, player, moveNumber, deadline) { const sendEmail = p.emailsDisabled ? null : await sgMail.send({ - to: p.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` - Hi ${p.name}, ${ - you ? 'You' : player.name - } failed to make ${ - you ? 'your' : 'their' - } move in time on this glass bead game: + to: p.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` + Hi ${p.name}, ${you ? 'You' : player.name + } failed to make ${you ? 'your' : 'their' + } move in time on this glass bead game: http://${config.appURL}/p/${postId} The game has now ended! `, - html: ` + html: `

Hi ${p.name},

- ${you ? 'You' : player.name} failed to make ${ - you ? 'your' : 'their' - } move in time on this glass bead game. + ${you ? 'You' : player.name} failed to make ${you ? 'your' : 'their' + } move in time on this glass bead game.

The game has now ended!

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) @@ -344,6 +341,8 @@ async function initializeScheduledTasks() { if (nextPlayer) scheduleGBGMoveJobs(id, nextPlayer, moveNumber, nextMoveDeadline) } }) + + initializePlayTasks() } module.exports = { diff --git a/Socket.js b/Socket.js index 9f23168..f90db7f 100644 --- a/Socket.js +++ b/Socket.js @@ -6,6 +6,7 @@ const socketServer = require('http').createServer() const socketIo = require('socket.io') const io = socketIo(socketServer, { cors: { origin: whitelist } }) // socket.io cheatsheet: https://socket.io/docs/v3/emit-cheatsheet/ +const { registerPlaySocketEvents } = require('./Play') const sockets = [] const rooms = [] // space, chat, post, or game + id: `space-58` @@ -200,6 +201,8 @@ io.on('connection', (socket) => { // gameRooms[roomId] = gameRooms[roomId].filter((users) => users.socketId !== socket.id) // } // }) + + registerPlaySocketEvents(socket, io) }) socketServer.listen(5001) From 4687f2df54ed388dd6adcf088c392fa53572df93 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 12 Mar 2024 13:59:00 +0100 Subject: [PATCH 06/21] hm --- Play.js | 133 +++++++++++++++++++++++++++++----------------- package-lock.json | 11 ++++ package.json | 1 + 3 files changed, 96 insertions(+), 49 deletions(-) diff --git a/Play.js b/Play.js index 6361c4b..3d94edf 100644 --- a/Play.js +++ b/Play.js @@ -2,21 +2,19 @@ const sequelize = require('sequelize') const { Op } = sequelize const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') const schedule = require('node-schedule') +const { createPost } = require('./Helpers') +const parseDuration = require('parse-duration') -function getFirstLeafStep( - step, - variables, - playerIds -) { +const getFirstLeafStep = (step, variables, playerIds) => { if (!step) { return undefined } switch (step.type) { - case 'game': - throw new Error('TODO') - case 'post': + case "game": + throw new Error("TODO") + case "post": return { step, variables } - case 'rounds': { + case "rounds": { const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) if (!firstStep) { return undefined @@ -25,11 +23,11 @@ function getFirstLeafStep( step: firstStep.step, variables: { ...firstStep.variables, - [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1, - }, + [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1 + } } } - case 'turns': { + case "turns": { const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) if (!firstStep) { return undefined @@ -38,8 +36,9 @@ function getFirstLeafStep( step: firstStep.step, variables: { ...firstStep.variables, - [`${step.id}_player`]: firstStep.variables[`${step.id}_player`] ?? playerIds[0], - }, + [`${step.id}_player`]: + firstStep.variables[`${step.id}_player`] ?? playerIds[0] + } } } default: { @@ -49,38 +48,38 @@ function getFirstLeafStep( } } -const getNextLeafStep = ( - steps, - stepId, - variables, - playerIds -) => { - let currentFound = false +const getTransition = (steps, stepId, variables, playerIds) => { + let current let currentVariables = variables for (let i = 0; i < steps.length; i++) { const step = steps[i] - if (currentFound) { + if (current) { const nextStep = getFirstLeafStep(step, currentVariables, playerIds) if (nextStep) { - return nextStep + return { current, next: nextStep.step, variables: nextStep.variables } } } else { switch (step.type) { - case 'game': - throw new Error('TODO') - case 'post': + case "game": + throw new Error("TODO") + case "post": if (step.id === stepId) { - currentFound = true + current = step } break - case 'rounds': { - const result = getNextLeafStep(step.steps, stepId, currentVariables, playerIds) + case "rounds": { + const result = getTransition( + step.steps, + stepId, + currentVariables, + playerIds + ) if (!result) { break } - if (result.step) { + if (result.next) { return result } @@ -91,26 +90,35 @@ const getNextLeafStep = ( step, { ...result.variables, - [roundKey]: currentRound + 1, + [roundKey]: currentRound + 1 }, playerIds ) if (firstStep) { - return firstStep + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables + } } } - currentFound = true - currentVariables = omit(currentVariables, roundKey) + current = result.current + currentVariables = omit(result.variables, roundKey) break } - case 'turns': { - const result = getNextLeafStep(step.steps, stepId, currentVariables, playerIds) + case "turns": { + const result = getTransition( + step.steps, + stepId, + currentVariables, + playerIds + ) if (!result) { break } - if (result.step) { + if (result.next) { return result } @@ -122,17 +130,21 @@ const getNextLeafStep = ( step, { ...result.variables, - [playerKey]: playerIds[currentPlayerIndex + 1], + [playerKey]: playerIds[currentPlayerIndex + 1] }, playerIds ) if (firstStep) { - return firstStep + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables + } } } - currentFound = true - currentVariables = omit(currentVariables, playerKey) + current = result.current + currentVariables = omit(result.variables, playerKey) break } default: { @@ -143,13 +155,34 @@ const getNextLeafStep = ( } } - if (currentFound) { - return { variables: currentVariables } + if (current) { + return { current, variables: currentVariables } } return undefined } +async function startStep(post) { + const { current, next, variables } = getTransition(post) + + switch (current.type) { + case 'post': + const timeout = parseDuration(current.post.timeout) + await createPost({ + type: 'post', + title: current.post.title, + text: current.post.text, + }, [], post.creatorId) + scheduleEndStep() + break; + default: + throw new Error('TODO') + } + + const newPost = {}; + scheduleEndStep(post); +} + async function endStep(post) { // TODO } @@ -163,7 +196,7 @@ function scheduleEndStep(post) { } }) if (!deepEquals(currentPost?.play, post.play)) { - // The state of the play has changed, we assume a new scheduled job has been triggered. + // The state of the play has changed, this job is outdated. return } endStep(currentPost) @@ -261,7 +294,6 @@ function registerPlaySocketEvents(socket, io) { } - await Post.update({ play: newPlay }, { @@ -270,6 +302,9 @@ function registerPlaySocketEvents(socket, io) { } }) io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + + const newPost = { ...post, play: newPlay } + await startStep(newPost) }) socket.on(EVENTS.outgoing.next, async ({ id }) => { @@ -279,16 +314,16 @@ function registerPlaySocketEvents(socket, io) { } }) const play = post.play; - const nextStep = getNextLeafStep( + const transition = getTransition( play.game.steps, play.step.id, play.variables, play.playerIds ) - const newPlay = nextStep?.step ? { + const newPlay = transition?.next ? { ...play, - step: nextStep.step, - variables: nextStep.variables + step: transition.next, + variables: transition.variables } : { game: play.game, gameId: play.gameId, diff --git a/package-lock.json b/package-lock.json index bf54eb1..e1119db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "mysql2": "^3.8.0", "node-schedule": "^2.1.1", "nodemon": "^3.0.3", + "parse-duration": "^1.1.0", "pg": "^8.11.3", "puppeteer": "^20.3.0", "sequelize": "^6.35.2", @@ -6717,6 +6718,11 @@ "node": ">=6" } }, + "node_modules/parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -13948,6 +13954,11 @@ "callsites": "^3.0.0" } }, + "parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", diff --git a/package.json b/package.json index b03271e..5991543 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "mysql2": "^3.8.0", "node-schedule": "^2.1.1", "nodemon": "^3.0.3", + "parse-duration": "^1.1.0", "pg": "^8.11.3", "puppeteer": "^20.3.0", "sequelize": "^6.35.2", From 3cff216f95dab5716047be794d840de8511d34a5 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 12 Mar 2024 13:59:07 +0100 Subject: [PATCH 07/21] hmm --- Play.js => Play.ts | 279 +++++++++++++++++++++++++++++---------------- package-lock.json | 51 +++++++-- package.json | 5 +- tsconfig.json | 20 ++++ 4 files changed, 247 insertions(+), 108 deletions(-) rename Play.js => Play.ts (56%) create mode 100644 tsconfig.json diff --git a/Play.js b/Play.ts similarity index 56% rename from Play.js rename to Play.ts index 3d94edf..e37d802 100644 --- a/Play.js +++ b/Play.ts @@ -1,20 +1,114 @@ -const sequelize = require('sequelize') +import { isEqual, omit } from 'lodash' +import schedule from 'node-schedule' +import sequelize from 'sequelize' +import { Server, Socket } from 'socket.io' + +const { Post } = require('./models') + const { Op } = sequelize -const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') -const schedule = require('node-schedule') -const { createPost } = require('./Helpers') -const parseDuration = require('parse-duration') -const getFirstLeafStep = (step, variables, playerIds) => { +export const POST_TYPE = [ +'post', + 'play', + 'comment', + 'bead', + 'poll-answer', + 'card-face', + 'gbg-room-comment', + 'url-block', + 'image-block', + 'audio-block', +] as const + +export type PostType = (typeof POST_TYPE)[number] + +export type Post = { + id: number + type: PostType + mediaTypes: string + title: string + text: string + createdAt: string + updatedAt: string + totalComments: number + totalLikes: number + totalRatings: number + totalReposts: number + totalLinks: number + creatorId: number + game?: Game + play?: Play +} + +export type Step = { + id: string +} & ( + | { + type: 'post' + post: { + title: string + text: string + timeout: number + } + } + | { + type: 'game' + gameId: number + } + | { + type: 'rounds' + amount: string + steps: Step[] + } + | { + type: 'turns' + steps: Step[] + } +) + +export type LeafStep = Extract + +export type Game = { + steps: Step[] +} + +export type PlayVariables = Record + +export type Play = { + game: Game + gameId: number + playerIds: number[] + status: 'waiting' | 'started' | 'paused' | 'stopped' | 'ended' + variables: PlayVariables +} & ( + | { status: 'waiting' | 'stopped' | 'ended' } + | { status: 'paused'; step: LeafStep } + | { status: 'started'; step: LeafStep; stepTimeout: number } +) + +async function getPost(id: number) { + return await Post.findOne({ + where: { + state: 'active', + id + } + }) +} + +const getFirstLeafStep = ( + step: Step | undefined, + variables: PlayVariables, + playerIds: number[] +): undefined | { step: LeafStep; variables: PlayVariables } => { if (!step) { return undefined } switch (step.type) { - case "game": - throw new Error("TODO") - case "post": + case 'game': + throw new Error('TODO') + case 'post': return { step, variables } - case "rounds": { + case 'rounds': { const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) if (!firstStep) { return undefined @@ -23,11 +117,11 @@ const getFirstLeafStep = (step, variables, playerIds) => { step: firstStep.step, variables: { ...firstStep.variables, - [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1 - } + [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1, + }, } } - case "turns": { + case 'turns': { const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) if (!firstStep) { return undefined @@ -36,20 +130,24 @@ const getFirstLeafStep = (step, variables, playerIds) => { step: firstStep.step, variables: { ...firstStep.variables, - [`${step.id}_player`]: - firstStep.variables[`${step.id}_player`] ?? playerIds[0] - } + [`${step.id}_player`]: firstStep.variables[`${step.id}_player`] ?? playerIds[0], + }, } } default: { - const exhaustivenessCheck = step + const exhaustivenessCheck: never = step throw exhaustivenessCheck } } } -const getTransition = (steps, stepId, variables, playerIds) => { - let current +const getTransition = ( + steps: Step[], + stepId: string, + variables: PlayVariables, + playerIds: number[] +): undefined | { current: LeafStep; next?: LeafStep; variables: PlayVariables } => { + let current: LeafStep | undefined let currentVariables = variables for (let i = 0; i < steps.length; i++) { const step = steps[i] @@ -61,20 +159,15 @@ const getTransition = (steps, stepId, variables, playerIds) => { } } else { switch (step.type) { - case "game": - throw new Error("TODO") - case "post": + case 'game': + throw new Error('TODO') + case 'post': if (step.id === stepId) { current = step } break - case "rounds": { - const result = getTransition( - step.steps, - stepId, - currentVariables, - playerIds - ) + case 'rounds': { + const result = getTransition(step.steps, stepId, currentVariables, playerIds) if (!result) { break } @@ -84,13 +177,13 @@ const getTransition = (steps, stepId, variables, playerIds) => { } const roundKey = `${step.id}_round` - const currentRound = currentVariables[roundKey] + const currentRound = currentVariables[roundKey] as number if (currentRound < +step.amount) { const firstStep = getFirstLeafStep( step, { ...result.variables, - [roundKey]: currentRound + 1 + [roundKey]: currentRound + 1, }, playerIds ) @@ -98,7 +191,7 @@ const getTransition = (steps, stepId, variables, playerIds) => { return { current: result.current, next: firstStep.step, - variables: firstStep.variables + variables: firstStep.variables, } } } @@ -107,13 +200,8 @@ const getTransition = (steps, stepId, variables, playerIds) => { currentVariables = omit(result.variables, roundKey) break } - case "turns": { - const result = getTransition( - step.steps, - stepId, - currentVariables, - playerIds - ) + case 'turns': { + const result = getTransition(step.steps, stepId, currentVariables, playerIds) if (!result) { break } @@ -123,14 +211,14 @@ const getTransition = (steps, stepId, variables, playerIds) => { } const playerKey = `${step.id}_player` - const currentPlayerId = result.variables[playerKey] + const currentPlayerId = result.variables[playerKey] as number const currentPlayerIndex = playerIds.indexOf(currentPlayerId) if (currentPlayerIndex < playerIds.length - 1) { const firstStep = getFirstLeafStep( step, { ...result.variables, - [playerKey]: playerIds[currentPlayerIndex + 1] + [playerKey]: playerIds[currentPlayerIndex + 1], }, playerIds ) @@ -138,7 +226,7 @@ const getTransition = (steps, stepId, variables, playerIds) => { return { current: result.current, next: firstStep.step, - variables: firstStep.variables + variables: firstStep.variables, } } } @@ -148,7 +236,7 @@ const getTransition = (steps, stepId, variables, playerIds) => { break } default: { - const exhaustivenessCheck = step + const exhaustivenessCheck: never = step throw exhaustivenessCheck } } @@ -162,40 +250,37 @@ const getTransition = (steps, stepId, variables, playerIds) => { return undefined } -async function startStep(post) { - const { current, next, variables } = getTransition(post) - switch (current.type) { - case 'post': - const timeout = parseDuration(current.post.timeout) - await createPost({ - type: 'post', - title: current.post.title, - text: current.post.text, - }, [], post.creatorId) - scheduleEndStep() - break; - default: - throw new Error('TODO') - } - - const newPost = {}; - scheduleEndStep(post); +async function startStep(post: Post) { + const play = post.play! + // const { current, next, variables } = getTransition(play.game.steps, play.step.id, play.variables, play.playerIds) + + // switch (current.type) { + // case 'post': + // const timeout = parseDuration(current.post.timeout) + // await createPost({ + // type: 'post', + // title: current.post.title, + // text: current.post.text, + // }, [], post.creatorId) + // scheduleEndStep(post) + // break; + // default: + // throw new Error('TODO') + // } + + // const newPost = {}; + // scheduleEndStep(post); } -async function endStep(post) { +async function endStep(post: Post) { // TODO } -function scheduleEndStep(post) { - schedule.scheduleJob(post.play.stepTimeout, async () => { - const currentPost = await Post.findOne({ - where: { - state: 'active', - id: post.id - } - }) - if (!deepEquals(currentPost?.play, post.play)) { +function scheduleEndStep(post: Post, timeout: number) { + schedule.scheduleJob(timeout, async () => { + const currentPost = await getPost(post.id) + if (!isEqual(currentPost?.play, post.play)) { // The state of the play has changed, this job is outdated. return } @@ -204,7 +289,7 @@ function scheduleEndStep(post) { } async function initializePlayTasks() { - const plays = await Post.findAll({ + const plays: Post[] = await Post.findAll({ where: { state: 'active', 'play': { [Op.not]: null } @@ -212,7 +297,7 @@ async function initializePlayTasks() { }) for (const post of plays) { - const play = post.play + const play = post.play! if (play.status !== 'started') { continue } @@ -221,7 +306,7 @@ async function initializePlayTasks() { if (stepTimeout < new Date()) { endStep(post) } else { - scheduleEndStep(post) + scheduleEndStep(post, play.stepTimeout) } } } @@ -239,13 +324,9 @@ const EVENTS = { } } -function registerPlaySocketEvents(socket, io) { - socket.on(EVENTS.outgoing.updateGame, async ({ id, game }) => { - const post = await Post.findOne({ - where: { - id - } - }) +function registerPlaySocketEvents(socket: Socket, io: Server) { + socket.on(EVENTS.outgoing.updateGame, async ({ id, game }: { id: number, game: Game }) => { + const post = await getPost(id) const newPlay = { ...post.play, @@ -263,28 +344,32 @@ function registerPlaySocketEvents(socket, io) { } }) - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) }) - socket.on(EVENTS.outgoing.start, async ({ id }) => { - const post = await Post.findOne({ + socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => { + const post: Post = await Post.findOne({ where: { id } }) + const play = post.play! - let newPlay; - if (post.play.status === 'paused') { + let newPlay: Play; + if (play.status === 'paused') { newPlay = { - ...post.play, + ...play, status: 'started', - stepTimeout: +new Date() + post.play.step.post.timeout + stepTimeout: +new Date() + play.step.post.timeout } } else { - const firstStep = getFirstLeafStep(post.play.game.steps[0]); + const firstStep = getFirstLeafStep(play.game.steps[0], play.variables, play.playerIds); + if (!firstStep) { + throw new Error('No step.') + } newPlay = { - ...post.play, + ...play, status: 'started', step: firstStep.step, variables: firstStep.variables, @@ -301,13 +386,13 @@ function registerPlaySocketEvents(socket, io) { id } }) - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) const newPost = { ...post, play: newPlay } await startStep(newPost) }) - socket.on(EVENTS.outgoing.next, async ({ id }) => { + socket.on(EVENTS.outgoing.next, async ({ id }: { id: number }) => { const post = await Post.findOne({ where: { id @@ -339,10 +424,10 @@ function registerPlaySocketEvents(socket, io) { id } }) - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) }) - socket.on(EVENTS.outgoing.pause, async ({ id }) => { + socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { const post = await Post.findOne({ where: { id @@ -361,10 +446,10 @@ function registerPlaySocketEvents(socket, io) { id } }) - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) }) - socket.on(EVENTS.outgoing.stop, async ({ id }) => { + socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => { const post = await Post.findOne({ where: { id @@ -383,7 +468,7 @@ function registerPlaySocketEvents(socket, io) { id } }) - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) }) } diff --git a/package-lock.json b/package-lock.json index e1119db..f1b5f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,11 @@ "web-push": "^3.6.7" }, "devDependencies": { + "@types/lodash": "^4.17.0", + "@types/node-schedule": "^2.1.6", "eslint": "^8.56.0", - "eslint-config-airbnb-typescript-prettier": "^5.0.0" + "eslint-config-airbnb-typescript-prettier": "^5.0.0", + "typescript": "^5.4.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2204,6 +2207,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -2217,6 +2226,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-schedule": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.6.tgz", + "integrity": "sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -8332,11 +8350,10 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10589,6 +10606,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, "@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -10602,6 +10625,15 @@ "undici-types": "~5.26.4" } }, + "@types/node-schedule": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.6.tgz", + "integrity": "sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/semver": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", @@ -15142,11 +15174,10 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "devOptional": true, - "peer": true + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "devOptional": true }, "umzug": { "version": "2.3.0", diff --git a/package.json b/package.json index 5991543..154a846 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,10 @@ "web-push": "^3.6.7" }, "devDependencies": { + "@types/lodash": "^4.17.0", + "@types/node-schedule": "^2.1.6", "eslint": "^8.56.0", - "eslint-config-airbnb-typescript-prettier": "^5.0.0" + "eslint-config-airbnb-typescript-prettier": "^5.0.0", + "typescript": "^5.4.2" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c40c209 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowJs": false, + "outDir": "./", + "noEmitOnError": false, + "incremental": true, + }, + "include": [ + "./**/*" + ], + "exclude": [ + "node_modules" + ] +} From 4c39b3f90742bc56ce2214c645c555d75643c282 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 13 Mar 2024 18:45:18 +0100 Subject: [PATCH 08/21] making progress --- Helpers.js | 6 +- Play.ts | 478 ----------------------- PlayServer.js | 378 ++++++++++++++++++ PlayServer.ts | 517 +++++++++++++++++++++++++ ScheduledTasks.js | 7 +- Socket.js | 4 +- migration-updates/add-move-to-posts.js | 24 ++ models/Post.js | 1 + package-lock.json | 189 +++++++++ package.json | 3 +- routes/Post.js | 50 ++- tsconfig.json | 1 - 12 files changed, 1171 insertions(+), 487 deletions(-) delete mode 100644 Play.ts create mode 100644 PlayServer.js create mode 100644 PlayServer.ts create mode 100644 migration-updates/add-move-to-posts.js diff --git a/Helpers.js b/Helpers.js index e285e54..cbce5e3 100644 --- a/Helpers.js +++ b/Helpers.js @@ -966,7 +966,8 @@ const fullPostAttributes = [ 'totalRatings', 'totalLinks', 'game', - 'play' + 'play', + 'move' ] // todo: replace all use cases with const fullPostAttributes above @@ -988,6 +989,7 @@ function findFullPostAttributes(model, accountId) { 'totalLinks', 'game', 'play', + 'move', // accountLike('post', model, accountId), // accountComment('post', model, accountId), // accountLink('post', model, accountId), @@ -1715,6 +1717,7 @@ function createPost(data, files, accountId) { glassBeadGame, game, play, + move, card, color, watermark, @@ -1738,6 +1741,7 @@ function createPost(data, files, accountId) { lastActivity: new Date(), game, play, + move, }) // todo: add the correct notification type diff --git a/Play.ts b/Play.ts deleted file mode 100644 index e37d802..0000000 --- a/Play.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { isEqual, omit } from 'lodash' -import schedule from 'node-schedule' -import sequelize from 'sequelize' -import { Server, Socket } from 'socket.io' - -const { Post } = require('./models') - -const { Op } = sequelize - -export const POST_TYPE = [ -'post', - 'play', - 'comment', - 'bead', - 'poll-answer', - 'card-face', - 'gbg-room-comment', - 'url-block', - 'image-block', - 'audio-block', -] as const - -export type PostType = (typeof POST_TYPE)[number] - -export type Post = { - id: number - type: PostType - mediaTypes: string - title: string - text: string - createdAt: string - updatedAt: string - totalComments: number - totalLikes: number - totalRatings: number - totalReposts: number - totalLinks: number - creatorId: number - game?: Game - play?: Play -} - -export type Step = { - id: string -} & ( - | { - type: 'post' - post: { - title: string - text: string - timeout: number - } - } - | { - type: 'game' - gameId: number - } - | { - type: 'rounds' - amount: string - steps: Step[] - } - | { - type: 'turns' - steps: Step[] - } -) - -export type LeafStep = Extract - -export type Game = { - steps: Step[] -} - -export type PlayVariables = Record - -export type Play = { - game: Game - gameId: number - playerIds: number[] - status: 'waiting' | 'started' | 'paused' | 'stopped' | 'ended' - variables: PlayVariables -} & ( - | { status: 'waiting' | 'stopped' | 'ended' } - | { status: 'paused'; step: LeafStep } - | { status: 'started'; step: LeafStep; stepTimeout: number } -) - -async function getPost(id: number) { - return await Post.findOne({ - where: { - state: 'active', - id - } - }) -} - -const getFirstLeafStep = ( - step: Step | undefined, - variables: PlayVariables, - playerIds: number[] -): undefined | { step: LeafStep; variables: PlayVariables } => { - if (!step) { - return undefined - } - switch (step.type) { - case 'game': - throw new Error('TODO') - case 'post': - return { step, variables } - case 'rounds': { - const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) - if (!firstStep) { - return undefined - } - return { - step: firstStep.step, - variables: { - ...firstStep.variables, - [`${step.id}_round`]: firstStep.variables[`${step.id}_round`] ?? 1, - }, - } - } - case 'turns': { - const firstStep = getFirstLeafStep(step.steps[0], variables, playerIds) - if (!firstStep) { - return undefined - } - return { - step: firstStep.step, - variables: { - ...firstStep.variables, - [`${step.id}_player`]: firstStep.variables[`${step.id}_player`] ?? playerIds[0], - }, - } - } - default: { - const exhaustivenessCheck: never = step - throw exhaustivenessCheck - } - } -} - -const getTransition = ( - steps: Step[], - stepId: string, - variables: PlayVariables, - playerIds: number[] -): undefined | { current: LeafStep; next?: LeafStep; variables: PlayVariables } => { - let current: LeafStep | undefined - let currentVariables = variables - for (let i = 0; i < steps.length; i++) { - const step = steps[i] - - if (current) { - const nextStep = getFirstLeafStep(step, currentVariables, playerIds) - if (nextStep) { - return { current, next: nextStep.step, variables: nextStep.variables } - } - } else { - switch (step.type) { - case 'game': - throw new Error('TODO') - case 'post': - if (step.id === stepId) { - current = step - } - break - case 'rounds': { - const result = getTransition(step.steps, stepId, currentVariables, playerIds) - if (!result) { - break - } - - if (result.next) { - return result - } - - const roundKey = `${step.id}_round` - const currentRound = currentVariables[roundKey] as number - if (currentRound < +step.amount) { - const firstStep = getFirstLeafStep( - step, - { - ...result.variables, - [roundKey]: currentRound + 1, - }, - playerIds - ) - if (firstStep) { - return { - current: result.current, - next: firstStep.step, - variables: firstStep.variables, - } - } - } - - current = result.current - currentVariables = omit(result.variables, roundKey) - break - } - case 'turns': { - const result = getTransition(step.steps, stepId, currentVariables, playerIds) - if (!result) { - break - } - - if (result.next) { - return result - } - - const playerKey = `${step.id}_player` - const currentPlayerId = result.variables[playerKey] as number - const currentPlayerIndex = playerIds.indexOf(currentPlayerId) - if (currentPlayerIndex < playerIds.length - 1) { - const firstStep = getFirstLeafStep( - step, - { - ...result.variables, - [playerKey]: playerIds[currentPlayerIndex + 1], - }, - playerIds - ) - if (firstStep) { - return { - current: result.current, - next: firstStep.step, - variables: firstStep.variables, - } - } - } - - current = result.current - currentVariables = omit(result.variables, playerKey) - break - } - default: { - const exhaustivenessCheck: never = step - throw exhaustivenessCheck - } - } - } - } - - if (current) { - return { current, variables: currentVariables } - } - - return undefined -} - - -async function startStep(post: Post) { - const play = post.play! - // const { current, next, variables } = getTransition(play.game.steps, play.step.id, play.variables, play.playerIds) - - // switch (current.type) { - // case 'post': - // const timeout = parseDuration(current.post.timeout) - // await createPost({ - // type: 'post', - // title: current.post.title, - // text: current.post.text, - // }, [], post.creatorId) - // scheduleEndStep(post) - // break; - // default: - // throw new Error('TODO') - // } - - // const newPost = {}; - // scheduleEndStep(post); -} - -async function endStep(post: Post) { - // TODO -} - -function scheduleEndStep(post: Post, timeout: number) { - schedule.scheduleJob(timeout, async () => { - const currentPost = await getPost(post.id) - if (!isEqual(currentPost?.play, post.play)) { - // The state of the play has changed, this job is outdated. - return - } - endStep(currentPost) - }) -} - -async function initializePlayTasks() { - const plays: Post[] = await Post.findAll({ - where: { - state: 'active', - 'play': { [Op.not]: null } - } - }) - - for (const post of plays) { - const play = post.play! - if (play.status !== 'started') { - continue - } - - const stepTimeout = new Date(play.stepTimeout); - if (stepTimeout < new Date()) { - endStep(post) - } else { - scheduleEndStep(post, play.stepTimeout) - } - } -} - -const EVENTS = { - outgoing: { - updateGame: 'play:outgoing-update-game', - start: 'play:outgoing-start', - next: 'play:outgoing-next', - pause: 'play:outgoing-pause', - stop: 'play:outgoing-stop' - }, - incoming: { - updated: 'play:incoming-updated' - } -} - -function registerPlaySocketEvents(socket: Socket, io: Server) { - socket.on(EVENTS.outgoing.updateGame, async ({ id, game }: { id: number, game: Game }) => { - const post = await getPost(id) - - const newPlay = { - ...post.play, - game - } - - await Post.update({ - play: { - ...post.play, - game - } - }, { - where: { - id - } - }) - - io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) - }) - - socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => { - const post: Post = await Post.findOne({ - where: { - id - } - }) - const play = post.play! - - let newPlay: Play; - if (play.status === 'paused') { - newPlay = { - ...play, - status: 'started', - stepTimeout: +new Date() + play.step.post.timeout - } - } else { - const firstStep = getFirstLeafStep(play.game.steps[0], play.variables, play.playerIds); - if (!firstStep) { - throw new Error('No step.') - } - - newPlay = { - ...play, - status: 'started', - step: firstStep.step, - variables: firstStep.variables, - stepTimeout: - +new Date() + firstStep.step.post.timeout, - } - - } - - await Post.update({ - play: newPlay - }, { - where: { - id - } - }) - io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) - - const newPost = { ...post, play: newPlay } - await startStep(newPost) - }) - - socket.on(EVENTS.outgoing.next, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) - const play = post.play; - const transition = getTransition( - play.game.steps, - play.step.id, - play.variables, - play.playerIds - ) - const newPlay = transition?.next ? { - ...play, - step: transition.next, - variables: transition.variables - } : { - game: play.game, - gameId: play.gameId, - playerIds: play.playerIds, - status: 'ended', - variables: {} - } - - await Post.update({ - play: newPlay - }, { - where: { - id - } - }) - io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) - }) - - socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) - - const newPlay = { - ...post.play, - status: 'paused' - } - - await Post.update({ - play: newPlay - }, { - where: { - id - } - }) - io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) - }) - - socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) - - const newPlay = { - ...post.play, - status: 'stopped' - } - - await Post.update({ - play: newPlay - }, { - where: { - id - } - }) - io.in(`${id}`).emit(EVENTS.incoming.updated, { play: newPlay }) - }) -} - -module.exports = { - registerPlaySocketEvents, - initializePlayTasks -} diff --git a/PlayServer.js b/PlayServer.js new file mode 100644 index 0000000..4a03588 --- /dev/null +++ b/PlayServer.js @@ -0,0 +1,378 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.registerPlayServerEvents = exports.initializePlayServerTasks = void 0; +const lodash_1 = require("lodash"); +const node_schedule_1 = __importDefault(require("node-schedule")); +const parse_duration_1 = __importDefault(require("parse-duration")); +const sequelize_1 = __importDefault(require("sequelize")); +const { Post, Link } = require('./models'); +const { createPost } = require('./Helpers'); +const { Op } = sequelize_1.default; +const POST_TYPE = [ + 'post', + 'play', + 'comment', + 'bead', + 'poll-answer', + 'card-face', + 'gbg-room-comment', + 'url-block', + 'image-block', + 'audio-block', +]; +function getPost(id) { + return __awaiter(this, void 0, void 0, function* () { + const post = yield Post.findOne({ + where: { + state: 'active', + id + } + }); + if (!post) { + throw new Error(`Post ${id} not found`); + } + return post; + }); +} +function updatePost(id, data) { + return __awaiter(this, void 0, void 0, function* () { + return yield Post.update(data, { where: { id } }); + }); +} +const getFirstMove = (step, variables, playerIds) => { + var _a, _b; + if (!step) { + return undefined; + } + switch (step.type) { + case 'game': + throw new Error('TODO'); + case 'move': + return { step, variables }; + case 'rounds': { + const firstStep = getFirstMove(step.steps[0], variables, playerIds); + if (!firstStep) { + return undefined; + } + return { + step: firstStep.step, + variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_a = firstStep.variables[step.name]) !== null && _a !== void 0 ? _a : 1 }), + }; + } + case 'turns': { + const firstStep = getFirstMove(step.steps[0], variables, playerIds); + if (!firstStep) { + return undefined; + } + return { + step: firstStep.step, + variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_b = firstStep.variables[step.name]) !== null && _b !== void 0 ? _b : playerIds[0] }), + }; + } + default: { + const exhaustivenessCheck = step; + throw exhaustivenessCheck; + } + } +}; +const getTransition = (steps, stepId, variables, playerIds) => { + let current; + let currentVariables = variables; + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + if (current) { + const nextStep = getFirstMove(step, currentVariables, playerIds); + if (nextStep) { + return { current, next: nextStep.step, variables: nextStep.variables }; + } + } + else { + switch (step.type) { + case 'game': + throw new Error('TODO'); + case 'move': + if (step.id === stepId) { + current = step; + } + break; + case 'rounds': + case 'turns': { + const result = getTransition(step.steps, stepId, currentVariables, playerIds); + if (!result) { + break; + } + if (result.next) { + return result; + } + let next; + if (step.type === 'rounds') { + const round = currentVariables[step.name]; + next = round < +step.amount ? round + 1 : undefined; + } + else { + const playerId = currentVariables[step.name]; + const playerIndex = playerIds.indexOf(playerId); + next = playerIds[playerIndex + 1]; + } + if (next) { + const firstStep = getFirstMove(step, Object.assign(Object.assign({}, result.variables), { [step.name]: next }), playerIds); + if (firstStep) { + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables, + }; + } + } + current = result.current; + currentVariables = (0, lodash_1.omit)(result.variables, step.name); + break; + } + default: { + const exhaustivenessCheck = step; + throw exhaustivenessCheck; + } + } + } + } + if (current) { + return { current, variables: currentVariables }; + } + return undefined; +}; +function createChild(data, parent) { + return __awaiter(this, void 0, void 0, function* () { + const { post } = yield createPost(data, [], parent.creatorId); + yield Link.create({ + creatorId: parent.creatorId, + itemAId: parent.id, + itemAType: parent.type, + itemBId: post.id, + itemBType: post.type, + relationship: 'parent', + state: 'active', + totalLikes: 0, + totalComments: 0, + totalRatings: 0 + }); + return post; + }); +} +function insertVariables(text, variables) { + return text === null || text === void 0 ? void 0 : text.replace(/\(([^)]+)\)/, (substring, variableName) => { + if (variableName in variables) { + return `${variables[variableName]}`; + } + return substring; + }); +} +function startStep(playPost, step, variables, io) { + return __awaiter(this, void 0, void 0, function* () { + const play = playPost.play; + const now = +new Date(); + const timeout = now + (0, parse_duration_1.default)(step.timeout); + const move = { + status: 'started', + startedAt: now, + timeout, + playId: playPost.id + }; + const movePost = yield createChild({ + type: 'post', + mediaTypes: '', + title: insertVariables(step.title, variables), + text: insertVariables(step.text, variables), + move + }, playPost); + const newPlay = Object.assign(Object.assign({}, play), { status: 'started', stepId: step.id, moveId: movePost.id, variables: variables }); + yield updatePost(playPost.id, { play: newPlay }); + scheduleMoveTimeout(movePost, timeout, io); + io.in(playPost.id).emit(EVENTS.incoming.updated, { play: newPlay }); + }); +} +function moveTimeout(movePost, io) { + return __awaiter(this, void 0, void 0, function* () { + const move = movePost.move; + const newMove = Object.assign(Object.assign({}, move), { status: 'ended' }); + updatePost(movePost.id, { + move: newMove + }); + if (move.playId) { + const playPost = yield getPost(move.playId); + nextStep(playPost, io); + } + }); +} +function nextStep(playPost, io) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + const play = playPost.play; + if (play.status !== 'started') { + return; + } + const transition = getTransition(play.game.steps, play.stepId, play.variables, play.playerIds); + if (transition === null || transition === void 0 ? void 0 : transition.next) { + console.log('next', transition.next, transition.variables); + yield startStep(playPost, transition.next, transition.variables, io); + } + else { + const newPlay = { + game: play.game, + gameId: play.gameId, + playerIds: play.playerIds, + status: 'ended', + variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables + }; + // await createChild({ + // type: 'post', + // mediaTypes: '', + // text: 'Play ended!', + // }, playPost) + yield updatePost(playPost.id, { play: newPlay }); + io.in(playPost.id).emit(EVENTS.incoming.updated, { play: newPlay }); + } + }); +} +function scheduleMoveTimeout(post, timeout, io) { + node_schedule_1.default.scheduleJob(timeout, () => __awaiter(this, void 0, void 0, function* () { + const currentPost = yield getPost(post.id); + if (!(0, lodash_1.isEqual)(currentPost.move, post.move)) { + // The state of the move has changed, this job is outdated. + return; + } + moveTimeout(currentPost, io); + })); +} +const EVENTS = { + outgoing: { + updateGame: 'outgoing-update-play-game', + start: 'outgoing-start-play', + stop: 'outgoing-stop-play', + skip: 'outgoing-skip-move', + pause: 'outgoing-pause-move', + }, + incoming: { + updated: 'incoming-play-updated' + } +}; +function initializePlayServerTasks(io) { + return __awaiter(this, void 0, void 0, function* () { + const moves = yield Post.findAll({ + where: { + state: 'active', + 'move': { [Op.not]: null } + } + }); + for (const post of moves) { + const move = post.move; + if (move.status !== 'started') { + continue; + } + console.log(move.timeout, +new Date()); + if (move.timeout < +new Date()) { + moveTimeout(post, io); + } + else { + scheduleMoveTimeout(post, move.timeout, io); + } + } + }); +} +exports.initializePlayServerTasks = initializePlayServerTasks; +function registerPlayServerEvents(socket, io) { + return __awaiter(this, void 0, void 0, function* () { + socket.on(EVENTS.outgoing.updateGame, (_a) => __awaiter(this, [_a], void 0, function* ({ id, game }) { + const post = yield getPost(id); + const newPlay = Object.assign(Object.assign({}, post.play), { game }); + yield updatePost(id, { + play: newPlay + }); + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + })); + socket.on(EVENTS.outgoing.start, (_b) => __awaiter(this, [_b], void 0, function* ({ id }) { + const playPost = yield Post.findOne({ + where: { + id + } + }); + const play = playPost.play; + if (play.status === 'started') { + return; + } + const step = getFirstMove(play.game.steps[0], play.variables, play.playerIds); + if (!step) { + throw new Error('No step.'); + } + yield startStep(playPost, step.step, step.variables, io); + })); + socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { + const post = yield Post.findOne({ + where: { + id + } + }); + const play = post.play; + const transition = getTransition(play.game.steps, play.step.id, play.variables, play.playerIds); + const newPlay = (transition === null || transition === void 0 ? void 0 : transition.next) ? Object.assign(Object.assign({}, play), { step: transition.next, variables: transition.variables }) : { + game: play.game, + gameId: play.gameId, + playerIds: play.playerIds, + status: 'ended', + variables: {} + }; + yield Post.update({ + play: newPlay + }, { + where: { + id + } + }); + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + })); + socket.on(EVENTS.outgoing.pause, (_d) => __awaiter(this, [_d], void 0, function* ({ id }) { + const post = yield Post.findOne({ + where: { + id + } + }); + const newPlay = Object.assign(Object.assign({}, post.play), { status: 'paused' }); + yield Post.update({ + play: newPlay + }, { + where: { + id + } + }); + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + })); + socket.on(EVENTS.outgoing.stop, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { + const post = yield Post.findOne({ + where: { + id + } + }); + const newPlay = Object.assign(Object.assign({}, post.play), { status: 'stopped' }); + yield Post.update({ + play: newPlay + }, { + where: { + id + } + }); + io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + })); + }); +} +exports.registerPlayServerEvents = registerPlayServerEvents; diff --git a/PlayServer.ts b/PlayServer.ts new file mode 100644 index 0000000..8489326 --- /dev/null +++ b/PlayServer.ts @@ -0,0 +1,517 @@ +import { isEqual, omit } from 'lodash' +import schedule from 'node-schedule' +import parseDuration from 'parse-duration' +import sequelize from 'sequelize' +import { Server, Socket } from 'socket.io' + +const { Post, Link } = require('./models') +const { createPost } = require('./Helpers') + +const { Op } = sequelize + +const POST_TYPE = [ + 'post', + 'play', + 'comment', + 'bead', + 'poll-answer', + 'card-face', + 'gbg-room-comment', + 'url-block', + 'image-block', + 'audio-block', +] as const + +type PostType = (typeof POST_TYPE)[number] + +type Post = { + id: number + type: PostType + mediaTypes: string + title: string + text: string + createdAt: string + updatedAt: string + totalComments: number + totalLikes: number + totalRatings: number + totalReposts: number + totalLinks: number + creatorId: number + game?: Game + play?: Play + move?: Move +} + +type Step = { + id: string +} & ( + | { + type: 'move' + title: string + text: string + timeout: string + } + | { + type: 'game' + gameId: number + } + | { + type: 'rounds' + name: string + amount: string + steps: Step[] + } + | { + type: 'turns' + name: string + steps: Step[] + } + ) + +type MoveStep = Extract + +type Game = { + steps: Step[] +} + +type Move = ( + | { status: 'skipped' | 'ended' } + | { status: 'paused'; elapsedTime: number } + | { + status: 'started' + startedAt: number + timeout: number + } +) & { playId?: number } + +type PlayVariables = Record + +export type Play = { + game: Game + gameId: number + playerIds: number[] + variables: PlayVariables +} & ( + | { status: 'waiting' | 'stopped' | 'ended' } + | { status: 'started'; stepId: string; moveId: number } + ) + +async function getPost(id: number): Promise { + const post = await Post.findOne({ + where: { + state: 'active', + id + } + }) + + if (!post) { + throw new Error(`Post ${id} not found`) + } + + return post; +} + +async function updatePost(id: number, data: Partial) { + return await Post.update(data, { where: { id } }) +} + +const getFirstMove = ( + step: Step | undefined, + variables: PlayVariables, + playerIds: number[] +): undefined | { step: MoveStep; variables: PlayVariables } => { + if (!step) { + return undefined + } + switch (step.type) { + case 'game': + throw new Error('TODO') + case 'move': + return { step, variables } + case 'rounds': { + const firstStep = getFirstMove(step.steps[0], variables, playerIds) + if (!firstStep) { + return undefined + } + return { + step: firstStep.step, + variables: { + ...firstStep.variables, + [step.name]: firstStep.variables[step.name] ?? 1, + }, + } + } + case 'turns': { + const firstStep = getFirstMove(step.steps[0], variables, playerIds) + if (!firstStep) { + return undefined + } + return { + step: firstStep.step, + variables: { + ...firstStep.variables, + [step.name]: firstStep.variables[step.name] ?? playerIds[0], + }, + } + } + default: { + const exhaustivenessCheck: never = step + throw exhaustivenessCheck + } + } +} + +const getTransition = ( + steps: Step[], + stepId: string, + variables: PlayVariables, + playerIds: number[] +): undefined | { current: MoveStep; next?: MoveStep; variables: PlayVariables } => { + let current: MoveStep | undefined + let currentVariables = variables + for (let i = 0; i < steps.length; i++) { + const step = steps[i] + + if (current) { + const nextStep = getFirstMove(step, currentVariables, playerIds) + if (nextStep) { + return { current, next: nextStep.step, variables: nextStep.variables } + } + } else { + switch (step.type) { + case 'game': + throw new Error('TODO') + case 'move': + if (step.id === stepId) { + current = step + } + break + case 'rounds': + case 'turns': { + const result = getTransition(step.steps, stepId, currentVariables, playerIds) + if (!result) { + break + } + + if (result.next) { + return result + } + + let next; + if (step.type === 'rounds') { + const round = currentVariables[step.name] as number + next = round < +step.amount ? round + 1 : undefined + } else { + const playerId = currentVariables[step.name] as number + const playerIndex = playerIds.indexOf(playerId) + next = playerIds[playerIndex + 1] + } + if (next) { + const firstStep = getFirstMove( + step, + { + ...result.variables, + [step.name]: next, + }, + playerIds + ) + if (firstStep) { + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables, + } + } + } + + current = result.current + currentVariables = omit(result.variables, step.name) + break + } + default: { + const exhaustivenessCheck: never = step + throw exhaustivenessCheck + } + } + } + } + + if (current) { + return { current, variables: currentVariables } + } + + return undefined +} + + +async function createChild(data: any, parent: Post) { + const { post } = await createPost(data, [], parent.creatorId) + await Link.create({ + creatorId: parent.creatorId, + itemAId: parent.id, + itemAType: parent.type, + itemBId: post.id, + itemBType: post.type, + relationship: 'parent', + state: 'active', + totalLikes: 0, + totalComments: 0, + totalRatings: 0 + }) + return post +} + +function insertVariables(text: string, variables: PlayVariables) { + return text?.replace(/\(([^)]+)\)/, (substring, variableName) => { + if (variableName in variables) { + return `${variables[variableName]}` + } + return substring + }) +} + +async function startStep(playPost: Post, step: MoveStep, variables: PlayVariables, io: Server) { + const play = playPost.play! + const now = +new Date(); + const timeout = now + parseDuration(step.timeout)! + + const move: Move = { + status: 'started', + startedAt: now, + timeout, + playId: playPost.id + } + const movePost = await createChild({ + type: 'post', + mediaTypes: '', + title: insertVariables(step.title, variables), + text: insertVariables(step.text, variables), + move + }, playPost) + + const newPlay: Play = { + ...play, + status: 'started', + stepId: step.id, + moveId: movePost.id, + variables: variables, + } + + await updatePost(playPost.id, { play: newPlay }) + + scheduleMoveTimeout(movePost, timeout, io) + io.in(playPost.id as any).emit(EVENTS.incoming.updated, { play: newPlay }) +} + +async function moveTimeout(movePost: Post, io: Server) { + const move = movePost.move! + const newMove: Move = { + ...move, + status: 'ended' + } + updatePost(movePost.id, { + move: newMove + }) + if (move.playId) { + const playPost = await getPost(move.playId); + nextStep(playPost, io) + } +} + +async function nextStep(playPost: Post, io: Server) { + const play = playPost.play!; + if (play.status !== 'started') { + return; + } + const transition = getTransition( + play.game.steps, + play.stepId, + play.variables, + play.playerIds + ) + + if (transition?.next) { + console.log('next', transition.next, transition.variables) + await startStep(playPost, transition.next, transition.variables, io) + } else { + const newPlay: Play = { + game: play.game, + gameId: play.gameId, + playerIds: play.playerIds, + status: 'ended', + variables: transition?.variables ?? play.variables + } + // await createChild({ + // type: 'post', + // mediaTypes: '', + // text: 'Play ended!', + // }, playPost) + await updatePost(playPost.id, { play: newPlay }) + io.in(playPost.id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + } +} + +function scheduleMoveTimeout(post: Post, timeout: number, io: Server) { + schedule.scheduleJob(timeout, async () => { + const currentPost = await getPost(post.id) + if (!isEqual(currentPost.move, post.move)) { + // The state of the move has changed, this job is outdated. + return + } + moveTimeout(currentPost, io) + }) +} + +const EVENTS = { + outgoing: { + updateGame: 'outgoing-update-play-game', + start: 'outgoing-start-play', + stop: 'outgoing-stop-play', + + skip: 'outgoing-skip-move', + pause: 'outgoing-pause-move', + }, + incoming: { + updated: 'incoming-play-updated' + } +} + +export async function initializePlayServerTasks(io: Server) { + const moves: Post[] = await Post.findAll({ + where: { + state: 'active', + 'move': { [Op.not]: null } + } + }) + + for (const post of moves) { + const move = post.move! + if (move.status !== 'started') { + continue + } + + console.log(move.timeout, +new Date()) + if (move.timeout < +new Date()) { + moveTimeout(post, io) + } else { + scheduleMoveTimeout(post, move.timeout, io) + } + } +} + +export async function registerPlayServerEvents(socket: Socket, io: Server) { + socket.on(EVENTS.outgoing.updateGame, async ({ id, game }: { id: number, game: Game }) => { + const post = await getPost(id) + + const newPlay: Play = { + ...post.play!, + game + } + + await updatePost(id, { + play: newPlay + }) + + io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => { + const playPost: Post = await Post.findOne({ + where: { + id + } + }) + const play = playPost.play! + + if (play.status === 'started') { + return + } + + const step = getFirstMove(play.game.steps[0], play.variables, play.playerIds); + if (!step) { + throw new Error('No step.') + } + + await startStep(playPost, step.step, step.variables, io) + }) + + socket.on(EVENTS.outgoing.skip, async ({ id }: { id: number }) => { + const post = await Post.findOne({ + where: { + id + } + }) + const play = post.play; + const transition = getTransition( + play.game.steps, + play.step.id, + play.variables, + play.playerIds + ) + const newPlay = transition?.next ? { + ...play, + step: transition.next, + variables: transition.variables + } : { + game: play.game, + gameId: play.gameId, + playerIds: play.playerIds, + status: 'ended', + variables: {} + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + const newPlay = { + ...post.play, + status: 'paused' + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + }) + + socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => { + const post = await Post.findOne({ + where: { + id + } + }) + + const newPlay = { + ...post.play, + status: 'stopped' + } + + await Post.update({ + play: newPlay + }, { + where: { + id + } + }) + io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + }) +} diff --git a/ScheduledTasks.js b/ScheduledTasks.js index b540178..52a2330 100644 --- a/ScheduledTasks.js +++ b/ScheduledTasks.js @@ -5,9 +5,10 @@ const { Op } = sequelize const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) -const { initializePlayTasks } = require('./Play') +const { io } = require('./Socket') +const { initializePlayServerTasks } = require('./PlayServer') -function scheduleEventNotification(data) { +async function scheduleEventNotification(data) { const { type, postId, @@ -342,7 +343,7 @@ async function initializeScheduledTasks() { } }) - initializePlayTasks() + await initializePlayServerTasks(io) } module.exports = { diff --git a/Socket.js b/Socket.js index f90db7f..fa86e79 100644 --- a/Socket.js +++ b/Socket.js @@ -6,7 +6,7 @@ const socketServer = require('http').createServer() const socketIo = require('socket.io') const io = socketIo(socketServer, { cors: { origin: whitelist } }) // socket.io cheatsheet: https://socket.io/docs/v3/emit-cheatsheet/ -const { registerPlaySocketEvents } = require('./Play') +const { registerPlayServerEvents } = require('./PlayServer') const sockets = [] const rooms = [] // space, chat, post, or game + id: `space-58` @@ -202,7 +202,7 @@ io.on('connection', (socket) => { // } // }) - registerPlaySocketEvents(socket, io) + registerPlayServerEvents(socket, io) }) socketServer.listen(5001) diff --git a/migration-updates/add-move-to-posts.js b/migration-updates/add-move-to-posts.js new file mode 100644 index 0000000..6c9e860 --- /dev/null +++ b/migration-updates/add-move-to-posts.js @@ -0,0 +1,24 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.addColumn( + 'Posts', + 'move', + { + type: Sequelize.DataTypes.JSON, + }, + { transaction: t } + ), + ]) + }) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => { + return Promise.all([ + queryInterface.removeColumn('Posts', 'move', { transaction: t }), + ]) + }) + }, +} diff --git a/models/Post.js b/models/Post.js index 8c37f02..9d76627 100644 --- a/models/Post.js +++ b/models/Post.js @@ -27,6 +27,7 @@ module.exports = (sequelize, DataTypes) => { totalGlassBeadGames: DataTypes.INTEGER, game: DataTypes.JSON, play: DataTypes.JSON, + move: DataTypes.JSON, lastActivity: DataTypes.DATE, }, {} diff --git a/package-lock.json b/package-lock.json index f1b5f72..8e00e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "devDependencies": { "@types/lodash": "^4.17.0", "@types/node-schedule": "^2.1.6", + "concurrently": "^8.2.2", "eslint": "^8.56.0", "eslint-config-airbnb-typescript-prettier": "^5.0.0", "typescript": "^5.4.2" @@ -3290,6 +3291,66 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -3437,6 +3498,22 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7334,6 +7411,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", @@ -7713,6 +7799,15 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -7813,6 +7908,12 @@ "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -8200,6 +8301,15 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -11402,6 +11512,49 @@ "typedarray": "^0.0.6" } }, + "concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, "config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -11519,6 +11672,15 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -14401,6 +14563,15 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "safe-array-concat": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", @@ -14674,6 +14845,12 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -14747,6 +14924,12 @@ "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" }, + "spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, "split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -15058,6 +15241,12 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 154a846..400c664 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "deps": "docker-compose up --build mysql", "migrate": "sequelize db:migrate", "seed": "sequelize db:seed:all", - "dev": "nodemon Server.js", + "dev": "concurrently -n node,ts 'nodemon Server.js' 'tsc --watch'", "start": "node Server.js" }, "keywords": [], @@ -44,6 +44,7 @@ "devDependencies": { "@types/lodash": "^4.17.0", "@types/node-schedule": "^2.1.6", + "concurrently": "^8.2.2", "eslint": "^8.56.0", "eslint-config-airbnb-typescript-prettier": "^5.0.0", "typescript": "^5.4.2" diff --git a/routes/Post.js b/routes/Post.js index f74d981..a126031 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -758,6 +758,38 @@ router.get('/post-comments', async (req, res) => { .catch((error) => res.status(500).json({ message: 'Error', error })) }) +router.get('/post-children', async (req, res) => { + const accountId = req.user ? req.user.id : null + const { postId, limit, offset } = req.query; + + const links = await Link.findAll({ + offset: +offset, + limit: +limit, + order: [['createdAt', 'DESC']], + attributes: ['state'], + include: [ + { + model: Post, + attributes: fullPostAttributes, + include: [ + { + model: User, + as: 'Creator', + attributes: ['id', 'handle', 'name', 'flagImagePath', 'coverImagePath'], + } + ] + } + ], + where: { + state: 'active', + relationship: 'parent', + itemAType: 'post', + itemAId: postId, + } + }) + res.status(200).json({ children: links.map(link => link.Post) }) +}) + router.get('/post-indirect-spaces', async (req, res) => { const { postId } = req.query const post = await Post.findOne({ @@ -1734,7 +1766,7 @@ router.post('/update-post', authenticateToken, async (req, res) => { if (!post) res.status(401).json({ message: 'Unauthorized' }) else { const toUpdate = {}; - for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'play']) { + for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'play', 'move']) { if (key in req.body) { toUpdate[key] = req.body[key] } @@ -3126,6 +3158,22 @@ router.post('/delete-comment', authenticateToken, async (req, res) => { { state: 'deleted' }, { where: { id: postId, creatorId: accountId } } ) + await Link.update( + { state: 'deleted', }, + { + where: { + state: 'active', + [Op.or]: [ + { + itemAId: postId + }, + { + itemBId: postId + } + ] + } + } + ); // get links & root post for tally updates const rootLink = await Link.findOne({ where: { itemBId: postId, itemBType: 'comment', relationship: 'root' }, diff --git a/tsconfig.json b/tsconfig.json index c40c209..c946444 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,6 @@ "allowJs": false, "outDir": "./", "noEmitOnError": false, - "incremental": true, }, "include": [ "./**/*" From 9df24d469406a143a763d22bf346f8e2c4a6752e Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 13 Mar 2024 21:30:54 +0100 Subject: [PATCH 09/21] even more progress --- PlayServer.js => GameServer.js | 235 ++++++++++--------- PlayServer.ts => GameServer.ts | 308 +++++++++++++++---------- Helpers.js | 21 +- ScheduledTasks.js | 6 +- Socket.js | 4 +- migration-updates/add-play-to-posts.js | 24 -- models/Post.js | 4 +- routes/Post.js | 2 +- 8 files changed, 331 insertions(+), 273 deletions(-) rename PlayServer.js => GameServer.js (58%) rename PlayServer.ts => GameServer.ts (59%) delete mode 100644 migration-updates/add-play-to-posts.js diff --git a/PlayServer.js b/GameServer.js similarity index 58% rename from PlayServer.js rename to GameServer.js index 4a03588..638f2f4 100644 --- a/PlayServer.js +++ b/GameServer.js @@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.registerPlayServerEvents = exports.initializePlayServerTasks = void 0; +exports.registerGameServerEvents = exports.initializeGameServerTasks = void 0; const lodash_1 = require("lodash"); const node_schedule_1 = __importDefault(require("node-schedule")); const parse_duration_1 = __importDefault(require("parse-duration")); @@ -22,7 +22,6 @@ const { createPost } = require('./Helpers'); const { Op } = sequelize_1.default; const POST_TYPE = [ 'post', - 'play', 'comment', 'bead', 'poll-answer', @@ -178,16 +177,18 @@ function insertVariables(text, variables) { return substring; }); } -function startStep(playPost, step, variables, io) { +function startStep(gamePost, step, variables, io) { return __awaiter(this, void 0, void 0, function* () { - const play = playPost.play; + const game = gamePost.game; + const play = game.play; const now = +new Date(); const timeout = now + (0, parse_duration_1.default)(step.timeout); const move = { status: 'started', + elapsedTime: 0, startedAt: now, timeout, - playId: playPost.id + gameId: gamePost.id }; const movePost = yield createChild({ type: 'post', @@ -195,79 +196,80 @@ function startStep(playPost, step, variables, io) { title: insertVariables(step.title, variables), text: insertVariables(step.text, variables), move - }, playPost); - const newPlay = Object.assign(Object.assign({}, play), { status: 'started', stepId: step.id, moveId: movePost.id, variables: variables }); - yield updatePost(playPost.id, { play: newPlay }); - scheduleMoveTimeout(movePost, timeout, io); - io.in(playPost.id).emit(EVENTS.incoming.updated, { play: newPlay }); + }, gamePost); + const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'started', step, moveId: movePost.id, variables: variables }) }); + yield updatePost(gamePost.id, { game: newGame }); + scheduleMoveTimeout(movePost.id, move, timeout, io); + console.log('hu'); + io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); }); } -function moveTimeout(movePost, io) { +function moveTimeout(id, move, io) { return __awaiter(this, void 0, void 0, function* () { - const move = movePost.move; + console.log('move timeout!'); const newMove = Object.assign(Object.assign({}, move), { status: 'ended' }); - updatePost(movePost.id, { + yield updatePost(id, { move: newMove }); - if (move.playId) { - const playPost = yield getPost(move.playId); - nextStep(playPost, io); + if (move.gameId) { + const gamePost = yield getPost(move.gameId); + console.log(gamePost); + nextStep(gamePost, io); } }); } -function nextStep(playPost, io) { +function nextStep(gamePost, io) { return __awaiter(this, void 0, void 0, function* () { var _a; - const play = playPost.play; + const game = gamePost.game; + const play = game.play; if (play.status !== 'started') { return; } - const transition = getTransition(play.game.steps, play.stepId, play.variables, play.playerIds); + const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); if (transition === null || transition === void 0 ? void 0 : transition.next) { - console.log('next', transition.next, transition.variables); - yield startStep(playPost, transition.next, transition.variables, io); + yield startStep(gamePost, transition.next, transition.variables, io); } else { - const newPlay = { - game: play.game, - gameId: play.gameId, - playerIds: play.playerIds, - status: 'ended', - variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables - }; + const newGame = Object.assign(Object.assign({}, game), { play: { + playerIds: play.playerIds, + status: 'ended', + variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables + } }); // await createChild({ // type: 'post', // mediaTypes: '', // text: 'Play ended!', // }, playPost) - yield updatePost(playPost.id, { play: newPlay }); - io.in(playPost.id).emit(EVENTS.incoming.updated, { play: newPlay }); + yield updatePost(gamePost.id, { game: newGame }); + io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); } }); } -function scheduleMoveTimeout(post, timeout, io) { +function scheduleMoveTimeout(id, move, timeout, io) { node_schedule_1.default.scheduleJob(timeout, () => __awaiter(this, void 0, void 0, function* () { - const currentPost = yield getPost(post.id); - if (!(0, lodash_1.isEqual)(currentPost.move, post.move)) { + const currentPost = yield getPost(id); + if (!(0, lodash_1.isEqual)(currentPost.move, move)) { + console.log(currentPost.move, move); // The state of the move has changed, this job is outdated. return; } - moveTimeout(currentPost, io); + moveTimeout(id, move, io); })); } const EVENTS = { outgoing: { - updateGame: 'outgoing-update-play-game', - start: 'outgoing-start-play', - stop: 'outgoing-stop-play', - skip: 'outgoing-skip-move', - pause: 'outgoing-pause-move', + update: 'gs:outgoing-update-game', + start: 'gs:outgoing-start-game', + stop: 'gs:outgoing-stop-game', + skip: 'gs:outgoing-skip-move', + pause: 'gs:outgoing-pause-move', }, incoming: { - updated: 'incoming-play-updated' + updated: 'gs:incoming-updated-game' } }; -function initializePlayServerTasks(io) { +function initializeGameServerTasks(io) { return __awaiter(this, void 0, void 0, function* () { const moves = yield Post.findAll({ where: { @@ -280,99 +282,118 @@ function initializePlayServerTasks(io) { if (move.status !== 'started') { continue; } - console.log(move.timeout, +new Date()); if (move.timeout < +new Date()) { - moveTimeout(post, io); + moveTimeout(post.id, move, io); } else { - scheduleMoveTimeout(post, move.timeout, io); + scheduleMoveTimeout(post.id, move, move.timeout, io); } } }); } -exports.initializePlayServerTasks = initializePlayServerTasks; -function registerPlayServerEvents(socket, io) { +exports.initializeGameServerTasks = initializeGameServerTasks; +function registerGameServerEvents(socket, io) { return __awaiter(this, void 0, void 0, function* () { - socket.on(EVENTS.outgoing.updateGame, (_a) => __awaiter(this, [_a], void 0, function* ({ id, game }) { - const post = yield getPost(id); - const newPlay = Object.assign(Object.assign({}, post.play), { game }); + socket.on(EVENTS.outgoing.update, (_a) => __awaiter(this, [_a], void 0, function* ({ id, game }) { yield updatePost(id, { - play: newPlay + game }); - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + io.in(id).emit(EVENTS.incoming.updated, { game }); })); socket.on(EVENTS.outgoing.start, (_b) => __awaiter(this, [_b], void 0, function* ({ id }) { - const playPost = yield Post.findOne({ + const gamePost = yield Post.findOne({ where: { id } }); - const play = playPost.play; + const game = gamePost.game; + const play = game.play; if (play.status === 'started') { return; } - const step = getFirstMove(play.game.steps[0], play.variables, play.playerIds); - if (!step) { - throw new Error('No step.'); + if (play.status === 'paused') { + const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'started' }) }); + yield updatePost(id, { + game: newGame + }); + const movePost = yield getPost(play.moveId); + const move = movePost.move; + if (move.status === 'paused') { + const now = +new Date(); + const timeout = now + (0, parse_duration_1.default)(play.step.timeout) - move.elapsedTime; + const newMove = Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout }); + yield updatePost(play.moveId, { + move: newMove + }); + scheduleMoveTimeout(play.moveId, newMove, timeout, io); + } + io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); + } + else { + const step = getFirstMove(game.steps[0], play.variables, play.playerIds); + if (!step) { + throw new Error('No step.'); + } + yield startStep(gamePost, step.step, step.variables, io); } - yield startStep(playPost, step.step, step.variables, io); })); socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { - const post = yield Post.findOne({ - where: { - id - } - }); - const play = post.play; - const transition = getTransition(play.game.steps, play.step.id, play.variables, play.playerIds); - const newPlay = (transition === null || transition === void 0 ? void 0 : transition.next) ? Object.assign(Object.assign({}, play), { step: transition.next, variables: transition.variables }) : { - game: play.game, - gameId: play.gameId, - playerIds: play.playerIds, - status: 'ended', - variables: {} - }; - yield Post.update({ - play: newPlay - }, { - where: { - id - } + const post = yield getPost(id); + const game = post.game; + const play = game.play; + if (play.status !== 'started') { + return; + } + const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); + const newGame = Object.assign(Object.assign({}, game), { play: (transition === null || transition === void 0 ? void 0 : transition.next) ? Object.assign(Object.assign({}, play), { step: transition.next, variables: transition.variables }) : { + playerIds: play.playerIds, + status: 'ended', + variables: {} + } }); + yield updatePost(id, { + game: newGame }); - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); })); socket.on(EVENTS.outgoing.pause, (_d) => __awaiter(this, [_d], void 0, function* ({ id }) { - const post = yield Post.findOne({ - where: { - id - } - }); - const newPlay = Object.assign(Object.assign({}, post.play), { status: 'paused' }); - yield Post.update({ - play: newPlay - }, { - where: { - id - } - }); - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + const post = yield getPost(id); + const game = post.game; + const play = game.play; + if (play.status !== 'started') { + return; + } + const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'paused' }) }); + yield updatePost(id, { game: newGame }); + const movePost = yield getPost(play.moveId); + const move = movePost.move; + if (move.status === 'started') { + const now = +new Date(); + const newMove = Object.assign(Object.assign({}, move), { status: 'paused', elapsedTime: move.elapsedTime + now - move.startedAt, remainingTime: move.timeout - now }); + yield updatePost(play.moveId, { + move: newMove + }); + } + io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); })); socket.on(EVENTS.outgoing.stop, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { - const post = yield Post.findOne({ - where: { - id - } - }); - const newPlay = Object.assign(Object.assign({}, post.play), { status: 'stopped' }); - yield Post.update({ - play: newPlay - }, { - where: { - id - } - }); - io.in(id).emit(EVENTS.incoming.updated, { play: newPlay }); + const post = yield getPost(id); + const game = post.game; + const play = game.play; + if (play.status !== 'started' && play.status !== 'paused') { + return; + } + const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'stopped' }) }); + yield updatePost(id, { game: newGame }); + const movePost = yield getPost(play.moveId); + const move = movePost.move; + if (move.status === 'started' || move.status === 'paused') { + const newMove = Object.assign(Object.assign({}, move), { status: 'stopped' }); + yield updatePost(play.moveId, { + move: newMove + }); + } + io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); })); }); } -exports.registerPlayServerEvents = registerPlayServerEvents; +exports.registerGameServerEvents = registerGameServerEvents; diff --git a/PlayServer.ts b/GameServer.ts similarity index 59% rename from PlayServer.ts rename to GameServer.ts index 8489326..0cd295d 100644 --- a/PlayServer.ts +++ b/GameServer.ts @@ -11,7 +11,6 @@ const { Op } = sequelize const POST_TYPE = [ 'post', - 'play', 'comment', 'bead', 'poll-answer', @@ -39,7 +38,6 @@ type Post = { totalLinks: number creatorId: number game?: Game - play?: Play move?: Move } @@ -73,28 +71,28 @@ type MoveStep = Extract type Game = { steps: Step[] + play: Play } type Move = ( - | { status: 'skipped' | 'ended' } - | { status: 'paused'; elapsedTime: number } + | { status: 'skipped' | 'ended' | 'stopped' } + | { status: 'paused'; elapsedTime: number; remainingTime: number } | { status: 'started' + elapsedTime: number startedAt: number timeout: number } -) & { playId?: number } +) & { gameId?: number } type PlayVariables = Record export type Play = { - game: Game - gameId: number playerIds: number[] variables: PlayVariables } & ( | { status: 'waiting' | 'stopped' | 'ended' } - | { status: 'started'; stepId: string; moveId: number } + | { status: 'started' | 'paused'; step: MoveStep; moveId: number } ) async function getPost(id: number): Promise { @@ -271,16 +269,18 @@ function insertVariables(text: string, variables: PlayVariables) { }) } -async function startStep(playPost: Post, step: MoveStep, variables: PlayVariables, io: Server) { - const play = playPost.play! +async function startStep(gamePost: Post, step: MoveStep, variables: PlayVariables, io: Server) { + const game = gamePost.game! + const play = game.play const now = +new Date(); const timeout = now + parseDuration(step.timeout)! const move: Move = { status: 'started', + elapsedTime: 0, startedAt: now, timeout, - playId: playPost.id + gameId: gamePost.id } const movePost = await createChild({ type: 'post', @@ -288,96 +288,103 @@ async function startStep(playPost: Post, step: MoveStep, variables: PlayVariable title: insertVariables(step.title, variables), text: insertVariables(step.text, variables), move - }, playPost) + }, gamePost) - const newPlay: Play = { - ...play, - status: 'started', - stepId: step.id, - moveId: movePost.id, - variables: variables, + const newGame: Game = { + ...game, + play: { + ...play, + status: 'started', + step, + moveId: movePost.id, + variables: variables, + } } - await updatePost(playPost.id, { play: newPlay }) + await updatePost(gamePost.id, { game: newGame }) - scheduleMoveTimeout(movePost, timeout, io) - io.in(playPost.id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + scheduleMoveTimeout(movePost.id, move, timeout, io) + console.log('hu') + io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) } -async function moveTimeout(movePost: Post, io: Server) { - const move = movePost.move! +async function moveTimeout(id: number, move: Move, io: Server) { + console.log('move timeout!') const newMove: Move = { ...move, status: 'ended' } - updatePost(movePost.id, { + await updatePost(id, { move: newMove }) - if (move.playId) { - const playPost = await getPost(move.playId); - nextStep(playPost, io) + if (move.gameId) { + const gamePost = await getPost(move.gameId); + console.log(gamePost) + nextStep(gamePost, io) } } -async function nextStep(playPost: Post, io: Server) { - const play = playPost.play!; +async function nextStep(gamePost: Post, io: Server) { + const game = gamePost.game!; + const play = game.play!; if (play.status !== 'started') { return; } const transition = getTransition( - play.game.steps, - play.stepId, + game.steps, + play.step.id, play.variables, play.playerIds ) if (transition?.next) { - console.log('next', transition.next, transition.variables) - await startStep(playPost, transition.next, transition.variables, io) + await startStep(gamePost, transition.next, transition.variables, io) } else { - const newPlay: Play = { - game: play.game, - gameId: play.gameId, - playerIds: play.playerIds, - status: 'ended', - variables: transition?.variables ?? play.variables + const newGame: Game = { + ...game, + play: { + playerIds: play.playerIds, + status: 'ended', + variables: transition?.variables ?? play.variables + } } // await createChild({ // type: 'post', // mediaTypes: '', // text: 'Play ended!', // }, playPost) - await updatePost(playPost.id, { play: newPlay }) - io.in(playPost.id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + await updatePost(gamePost.id, { game: newGame }) + io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) } } -function scheduleMoveTimeout(post: Post, timeout: number, io: Server) { +function scheduleMoveTimeout(id: number, move: Move, timeout: number, io: Server) { schedule.scheduleJob(timeout, async () => { - const currentPost = await getPost(post.id) - if (!isEqual(currentPost.move, post.move)) { + const currentPost = await getPost(id) + if (!isEqual(currentPost.move, move)) { + console.log(currentPost.move, move) // The state of the move has changed, this job is outdated. return } - moveTimeout(currentPost, io) + moveTimeout(id, move, io) }) } const EVENTS = { outgoing: { - updateGame: 'outgoing-update-play-game', - start: 'outgoing-start-play', - stop: 'outgoing-stop-play', + update: 'gs:outgoing-update-game', + start: 'gs:outgoing-start-game', + stop: 'gs:outgoing-stop-game', - skip: 'outgoing-skip-move', - pause: 'outgoing-pause-move', + skip: 'gs:outgoing-skip-move', + pause: 'gs:outgoing-pause-move', }, incoming: { - updated: 'incoming-play-updated' + updated: 'gs:incoming-updated-game' } } -export async function initializePlayServerTasks(io: Server) { +export async function initializeGameServerTasks(io: Server) { const moves: Post[] = await Post.findAll({ where: { state: 'active', @@ -391,127 +398,174 @@ export async function initializePlayServerTasks(io: Server) { continue } - console.log(move.timeout, +new Date()) if (move.timeout < +new Date()) { - moveTimeout(post, io) + moveTimeout(post.id, move, io) } else { - scheduleMoveTimeout(post, move.timeout, io) + scheduleMoveTimeout(post.id, move, move.timeout, io) } } } -export async function registerPlayServerEvents(socket: Socket, io: Server) { - socket.on(EVENTS.outgoing.updateGame, async ({ id, game }: { id: number, game: Game }) => { - const post = await getPost(id) - - const newPlay: Play = { - ...post.play!, - game - } - +export async function registerGameServerEvents(socket: Socket, io: Server) { + socket.on(EVENTS.outgoing.update, async ({ id, game }: { id: number, game: Game }) => { await updatePost(id, { - play: newPlay + game }) - io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(id as any).emit(EVENTS.incoming.updated, { game }) }) socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => { - const playPost: Post = await Post.findOne({ + const gamePost: Post = await Post.findOne({ where: { id } }) - const play = playPost.play! + const game = gamePost.game! + const play = game.play; if (play.status === 'started') { return } - const step = getFirstMove(play.game.steps[0], play.variables, play.playerIds); - if (!step) { - throw new Error('No step.') - } + if (play.status === 'paused') { + const newGame: Game = { + ...game, + play: { + ...play, + status: 'started' + } + } + await updatePost(id, { + game: newGame + }) + const movePost = await getPost(play.moveId); + const move = movePost.move!; + if (move.status === 'paused') { + const now = + new Date(); + const timeout = now + parseDuration(play.step.timeout)! - move.elapsedTime; + const newMove: Move = { + ...move, + status: 'started', + elapsedTime: move.elapsedTime, + startedAt: now, + timeout, + } + await updatePost(play.moveId, { + move: newMove + }) + scheduleMoveTimeout(play.moveId, newMove, timeout, io) + } + io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) + } else { + const step = getFirstMove(game.steps[0], play.variables, play.playerIds); + if (!step) { + throw new Error('No step.') + } - await startStep(playPost, step.step, step.variables, io) + await startStep(gamePost, step.step, step.variables, io) + } }) socket.on(EVENTS.outgoing.skip, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) - const play = post.play; + const post = await getPost(id); + const game = post.game!; + const play = game.play; + if (play.status !== 'started') { + return + } const transition = getTransition( - play.game.steps, + game.steps, play.step.id, play.variables, play.playerIds ) - const newPlay = transition?.next ? { - ...play, - step: transition.next, - variables: transition.variables - } : { - game: play.game, - gameId: play.gameId, - playerIds: play.playerIds, - status: 'ended', - variables: {} + const newGame: Game = { + ...game, + play: transition?.next ? { + ...play, + step: transition.next, + variables: transition.variables + } : { + playerIds: play.playerIds, + status: 'ended', + variables: {} + } } - await Post.update({ - play: newPlay - }, { - where: { - id - } + await updatePost(id, { + game: newGame }) - io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) }) socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) + const post = await getPost(id) + const game = post.game! + const play = game.play; - const newPlay = { - ...post.play, - status: 'paused' + if (play.status !== 'started') { + return; } - await Post.update({ - play: newPlay - }, { - where: { - id + + const newGame: Game = { + ...game, + play: { + ...play, + status: 'paused' } - }) - io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + } + await updatePost(id, { game: newGame }) + + const movePost = await getPost(play.moveId); + const move = movePost.move!; + if (move.status === 'started') { + const now = + new Date(); + const newMove: Move = { + ...move, + status: 'paused', + elapsedTime: move.elapsedTime + now - move.startedAt, + remainingTime: move.timeout - now + } + await updatePost(play.moveId, { + move: newMove + }) + } + + io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) }) socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => { - const post = await Post.findOne({ - where: { - id - } - }) + const post = await getPost(id) + const game = post.game! + const play = game.play; - const newPlay = { - ...post.play, - status: 'stopped' + if (play.status !== 'started' && play.status !== 'paused') { + return } - await Post.update({ - play: newPlay - }, { - where: { - id + const newGame: Game = { + ...game, + play: { + ...play, + status: 'stopped' } - }) - io.in(id as any).emit(EVENTS.incoming.updated, { play: newPlay }) + } + await updatePost(id, { game: newGame }) + + const movePost = await getPost(play.moveId); + const move = movePost.move!; + if (move.status === 'started' || move.status === 'paused') { + const newMove: Move = { + ...move, + status: 'stopped', + } + await updatePost(play.moveId, { + move: newMove + }) + } + + io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) }) } diff --git a/Helpers.js b/Helpers.js index cbce5e3..8e2a04f 100644 --- a/Helpers.js +++ b/Helpers.js @@ -966,7 +966,6 @@ const fullPostAttributes = [ 'totalRatings', 'totalLinks', 'game', - 'play', 'move' ] @@ -988,7 +987,6 @@ function findFullPostAttributes(model, accountId) { 'totalRatings', 'totalLinks', 'game', - 'play', 'move', // accountLike('post', model, accountId), // accountComment('post', model, accountId), @@ -1150,13 +1148,24 @@ function findPostInclude(accountId) { }, { model: Link, - as: 'Plays', + as: 'Original', + required: false, + where: { relationship: 'spawn', state: 'active' }, + include: { + model: Post, + as: 'Parent', + attributes: ['id', 'title', 'game', 'state'] + } + }, + { + model: Link, + as: 'Spawns', separate: true, - where: { relationship: 'play', state: 'active' }, + where: { relationship: 'spawn', state: 'active' }, order: [['index', 'ASC']], include: { model: Post, - attributes: ['id', 'title', 'play', 'state'] + attributes: ['id', 'title', 'game', 'state'] } }, { @@ -1716,7 +1725,6 @@ function createPost(data, files, accountId) { poll, glassBeadGame, game, - play, move, card, color, @@ -1740,7 +1748,6 @@ function createPost(data, files, accountId) { watermark: !!watermark, lastActivity: new Date(), game, - play, move, }) diff --git a/ScheduledTasks.js b/ScheduledTasks.js index 52a2330..a8aa667 100644 --- a/ScheduledTasks.js +++ b/ScheduledTasks.js @@ -5,8 +5,8 @@ const { Op } = sequelize const { User, Event, UserEvent, Notification, Post, Weave, GlassBeadGame } = require('./models') const sgMail = require('@sendgrid/mail') sgMail.setApiKey(process.env.SENDGRID_API_KEY) -const { io } = require('./Socket') -const { initializePlayServerTasks } = require('./PlayServer') +const io = require('./Socket') +const { initializeGameServerTasks } = require('./GameServer') async function scheduleEventNotification(data) { const { @@ -343,7 +343,7 @@ async function initializeScheduledTasks() { } }) - await initializePlayServerTasks(io) + await initializeGameServerTasks(io) } module.exports = { diff --git a/Socket.js b/Socket.js index fa86e79..6d7b37d 100644 --- a/Socket.js +++ b/Socket.js @@ -6,7 +6,7 @@ const socketServer = require('http').createServer() const socketIo = require('socket.io') const io = socketIo(socketServer, { cors: { origin: whitelist } }) // socket.io cheatsheet: https://socket.io/docs/v3/emit-cheatsheet/ -const { registerPlayServerEvents } = require('./PlayServer') +const { registerGameServerEvents } = require('./GameServer') const sockets = [] const rooms = [] // space, chat, post, or game + id: `space-58` @@ -202,7 +202,7 @@ io.on('connection', (socket) => { // } // }) - registerPlayServerEvents(socket, io) + registerGameServerEvents(socket, io) }) socketServer.listen(5001) diff --git a/migration-updates/add-play-to-posts.js b/migration-updates/add-play-to-posts.js deleted file mode 100644 index e07a28a..0000000 --- a/migration-updates/add-play-to-posts.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - up: (queryInterface, Sequelize) => { - return queryInterface.sequelize.transaction((t) => { - return Promise.all([ - queryInterface.addColumn( - 'Posts', - 'play', - { - type: Sequelize.DataTypes.JSON, - }, - { transaction: t } - ), - ]) - }) - }, - - down: (queryInterface, Sequelize) => { - return queryInterface.sequelize.transaction((t) => { - return Promise.all([ - queryInterface.removeColumn('Posts', 'play', { transaction: t }), - ]) - }) - }, -} diff --git a/models/Post.js b/models/Post.js index 9d76627..de24265 100644 --- a/models/Post.js +++ b/models/Post.js @@ -26,7 +26,6 @@ module.exports = (sequelize, DataTypes) => { totalRatings: DataTypes.INTEGER, totalGlassBeadGames: DataTypes.INTEGER, game: DataTypes.JSON, - play: DataTypes.JSON, move: DataTypes.JSON, lastActivity: DataTypes.DATE, }, @@ -51,7 +50,8 @@ module.exports = (sequelize, DataTypes) => { Post.hasMany(models.Link, { as: 'UrlBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'ImageBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'AudioBlocks', foreignKey: 'itemAId' }) - Post.hasMany(models.Link, { as: 'Plays', foreignKey: 'itemAId' }) + Post.hasOne(models.Link, { as: 'Original', foreignKey: 'itemBId' }) + Post.hasMany(models.Link, { as: 'Spawns', foreignKey: 'itemAId' }) Post.hasOne(models.Link, { as: 'MediaLink', foreignKey: 'itemAId' }) // used for post map (todo: rethink...) Post.hasMany(models.Link, { as: 'OutgoingPostLinks', foreignKey: 'itemAId' }) diff --git a/routes/Post.js b/routes/Post.js index a126031..83e8979 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -1766,7 +1766,7 @@ router.post('/update-post', authenticateToken, async (req, res) => { if (!post) res.status(401).json({ message: 'Unauthorized' }) else { const toUpdate = {}; - for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'play', 'move']) { + for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'move']) { if (key in req.body) { toUpdate[key] = req.body[key] } From a4b01a4f7dd44d2741f0df712363878bd41ae5e8 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Mon, 25 Mar 2024 14:09:51 +0100 Subject: [PATCH 10/21] remixes --- GameServer.js | 207 +++++++++++++++++++++---------------- GameServer.ts | 274 ++++++++++++++++++++++++------------------------- Helpers.js | 58 ++++++++++- models/Post.js | 4 +- routes/Post.js | 21 +++- 5 files changed, 322 insertions(+), 242 deletions(-) diff --git a/GameServer.js b/GameServer.js index 638f2f4..2c6afb7 100644 --- a/GameServer.js +++ b/GameServer.js @@ -51,33 +51,24 @@ function updatePost(id, data) { }); } const getFirstMove = (step, variables, playerIds) => { - var _a, _b; + var _a; if (!step) { return undefined; } switch (step.type) { - case 'game': - throw new Error('TODO'); case 'move': return { step, variables }; - case 'rounds': { + case 'sequence': { const firstStep = getFirstMove(step.steps[0], variables, playerIds); if (!firstStep) { return undefined; } - return { - step: firstStep.step, - variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_a = firstStep.variables[step.name]) !== null && _a !== void 0 ? _a : 1 }), - }; - } - case 'turns': { - const firstStep = getFirstMove(step.steps[0], variables, playerIds); - if (!firstStep) { - return undefined; + if (!step.repeat) { + return firstStep; } return { step: firstStep.step, - variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_b = firstStep.variables[step.name]) !== null && _b !== void 0 ? _b : playerIds[0] }), + variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_a = firstStep.variables[step.name]) !== null && _a !== void 0 ? _a : (step.repeat.type === 'rounds' ? 1 : playerIds[0]) }), }; } default: { @@ -99,15 +90,12 @@ const getTransition = (steps, stepId, variables, playerIds) => { } else { switch (step.type) { - case 'game': - throw new Error('TODO'); case 'move': if (step.id === stepId) { current = step; } break; - case 'rounds': - case 'turns': { + case 'sequence': { const result = getTransition(step.steps, stepId, currentVariables, playerIds); if (!result) { break; @@ -115,24 +103,26 @@ const getTransition = (steps, stepId, variables, playerIds) => { if (result.next) { return result; } - let next; - if (step.type === 'rounds') { - const round = currentVariables[step.name]; - next = round < +step.amount ? round + 1 : undefined; - } - else { - const playerId = currentVariables[step.name]; - const playerIndex = playerIds.indexOf(playerId); - next = playerIds[playerIndex + 1]; - } - if (next) { - const firstStep = getFirstMove(step, Object.assign(Object.assign({}, result.variables), { [step.name]: next }), playerIds); - if (firstStep) { - return { - current: result.current, - next: firstStep.step, - variables: firstStep.variables, - }; + if (step.repeat) { + let nextValue; + if (step.repeat.type === 'rounds') { + const round = currentVariables[step.name]; + nextValue = round < +step.repeat.amount ? round + 1 : undefined; + } + else { + const playerId = currentVariables[step.name]; + const playerIndex = playerIds.indexOf(playerId); + nextValue = playerIds[playerIndex + 1]; + } + if (nextValue) { + const firstStep = getFirstMove(step, Object.assign(Object.assign({}, result.variables), { [step.name]: nextValue }), playerIds); + if (firstStep) { + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables, + }; + } } } current = result.current; @@ -170,7 +160,7 @@ function createChild(data, parent) { }); } function insertVariables(text, variables) { - return text === null || text === void 0 ? void 0 : text.replace(/\(([^)]+)\)/, (substring, variableName) => { + return text === null || text === void 0 ? void 0 : text.replace(/\[([^\]]+)\]/, (substring, variableName) => { if (variableName in variables) { return `${variables[variableName]}`; } @@ -197,10 +187,15 @@ function startStep(gamePost, step, variables, io) { text: insertVariables(step.text, variables), move }, gamePost); - const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'started', step, moveId: movePost.id, variables: variables }) }); + const newGame = Object.assign(Object.assign({}, game), { play: { + status: 'started', + step, + moveId: movePost.id, + variables: variables, + playerIds: play.playerIds, + } }); yield updatePost(gamePost.id, { game: newGame }); scheduleMoveTimeout(movePost.id, move, timeout, io); - console.log('hu'); io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); }); } @@ -223,12 +218,24 @@ function nextStep(gamePost, io) { var _a; const game = gamePost.game; const play = game.play; - if (play.status !== 'started') { + if (play.status !== 'started' && play.status !== 'paused') { return; } const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); if (transition === null || transition === void 0 ? void 0 : transition.next) { - yield startStep(gamePost, transition.next, transition.variables, io); + if (play.status === 'started') { + yield startStep(gamePost, transition.next, transition.variables, io); + } + else { + const newGame = Object.assign(Object.assign({}, game), { play: { + status: 'paused', + step: transition.next, + variables: transition.variables, + playerIds: play.playerIds, + } }); + yield updatePost(gamePost.id, { game: newGame }); + io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); + } } else { const newGame = Object.assign(Object.assign({}, game), { play: { @@ -236,11 +243,6 @@ function nextStep(gamePost, io) { status: 'ended', variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables } }); - // await createChild({ - // type: 'post', - // mediaTypes: '', - // text: 'Play ended!', - // }, playPost) yield updatePost(gamePost.id, { game: newGame }); io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); } @@ -259,14 +261,14 @@ function scheduleMoveTimeout(id, move, timeout, io) { } const EVENTS = { outgoing: { - update: 'gs:outgoing-update-game', - start: 'gs:outgoing-start-game', - stop: 'gs:outgoing-stop-game', - skip: 'gs:outgoing-skip-move', - pause: 'gs:outgoing-pause-move', + update: 'gs:outgoing-update', + start: 'gs:outgoing-start', + stop: 'gs:outgoing-stop', + skip: 'gs:outgoing-skip', + pause: 'gs:outgoing-pause', }, incoming: { - updated: 'gs:incoming-updated-game' + updated: 'gs:incoming-updated' } }; function initializeGameServerTasks(io) { @@ -308,29 +310,43 @@ function registerGameServerEvents(socket, io) { }); const game = gamePost.game; const play = game.play; + console.log(play); if (play.status === 'started') { return; } if (play.status === 'paused') { - const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'started' }) }); - yield updatePost(id, { - game: newGame - }); - const movePost = yield getPost(play.moveId); - const move = movePost.move; - if (move.status === 'paused') { - const now = +new Date(); - const timeout = now + (0, parse_duration_1.default)(play.step.timeout) - move.elapsedTime; - const newMove = Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout }); - yield updatePost(play.moveId, { - move: newMove + const moveId = play.moveId; + if (!moveId) { + console.log('startstep'); + yield startStep(gamePost, play.step, play.variables, io); + } + else { + const newGame = Object.assign(Object.assign({}, game), { play: { + status: 'started', + moveId, + playerIds: play.playerIds, + step: play.step, + variables: play.variables, + } }); + yield updatePost(id, { + game: newGame }); - scheduleMoveTimeout(play.moveId, newMove, timeout, io); + const movePost = yield getPost(moveId); + const move = movePost.move; + if (move.status === 'paused') { + const now = +new Date(); + const timeout = now + (0, parse_duration_1.default)(play.step.timeout) - move.elapsedTime; + const newMove = Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout }); + yield updatePost(moveId, { + move: newMove + }); + scheduleMoveTimeout(moveId, newMove, timeout, io); + } + io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); } - io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); } else { - const step = getFirstMove(game.steps[0], play.variables, play.playerIds); + const step = getFirstMove(game.steps[0], {}, play.playerIds); if (!step) { throw new Error('No step.'); } @@ -338,22 +354,19 @@ function registerGameServerEvents(socket, io) { } })); socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { - const post = yield getPost(id); - const game = post.game; - const play = game.play; - if (play.status !== 'started') { - return; + const gamePost = yield getPost(id); + const play = gamePost.game.play; + if ((play.status === 'started' || play.status === 'paused') && play.moveId) { + const movePost = yield getPost(play.moveId); + const move = movePost.move; + if (move.status === 'started' || move.status === 'paused') { + const newMove = Object.assign(Object.assign({}, move), { status: 'skipped' }); + yield updatePost(play.moveId, { + move: newMove + }); + } } - const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); - const newGame = Object.assign(Object.assign({}, game), { play: (transition === null || transition === void 0 ? void 0 : transition.next) ? Object.assign(Object.assign({}, play), { step: transition.next, variables: transition.variables }) : { - playerIds: play.playerIds, - status: 'ended', - variables: {} - } }); - yield updatePost(id, { - game: newGame - }); - io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); + yield nextStep(gamePost, io); })); socket.on(EVENTS.outgoing.pause, (_d) => __awaiter(this, [_d], void 0, function* ({ id }) { const post = yield getPost(id); @@ -362,7 +375,13 @@ function registerGameServerEvents(socket, io) { if (play.status !== 'started') { return; } - const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'paused' }) }); + const newGame = Object.assign(Object.assign({}, game), { play: { + status: 'paused', + step: play.step, + variables: play.variables, + moveId: play.moveId, + playerIds: play.playerIds, + } }); yield updatePost(id, { game: newGame }); const movePost = yield getPost(play.moveId); const move = movePost.move; @@ -382,15 +401,21 @@ function registerGameServerEvents(socket, io) { if (play.status !== 'started' && play.status !== 'paused') { return; } - const newGame = Object.assign(Object.assign({}, game), { play: Object.assign(Object.assign({}, play), { status: 'stopped' }) }); + const newGame = Object.assign(Object.assign({}, game), { play: { + status: 'stopped', + variables: play.variables, + playerIds: play.playerIds, + } }); yield updatePost(id, { game: newGame }); - const movePost = yield getPost(play.moveId); - const move = movePost.move; - if (move.status === 'started' || move.status === 'paused') { - const newMove = Object.assign(Object.assign({}, move), { status: 'stopped' }); - yield updatePost(play.moveId, { - move: newMove - }); + if (play.moveId) { + const movePost = yield getPost(play.moveId); + const move = movePost.move; + if (move.status === 'started' || move.status === 'paused') { + const newMove = Object.assign(Object.assign({}, move), { status: 'stopped' }); + yield updatePost(play.moveId, { + move: newMove + }); + } } io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); })); diff --git a/GameServer.ts b/GameServer.ts index 0cd295d..9e1032d 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -41,28 +41,20 @@ type Post = { move?: Move } -type Step = { +export type Step = { id: string + name: string + originalStep?: { gameId: number; stepId: string } } & ( | { type: 'move' - title: string + title?: string text: string timeout: string } | { - type: 'game' - gameId: number - } - | { - type: 'rounds' - name: string - amount: string - steps: Step[] - } - | { - type: 'turns' - name: string + type: 'sequence' + repeat?: { type: 'rounds'; amount: number } | { type: 'turns' } steps: Step[] } ) @@ -92,6 +84,7 @@ export type Play = { variables: PlayVariables } & ( | { status: 'waiting' | 'stopped' | 'ended' } + | { status: 'paused'; step: MoveStep; moveId?: number } | { status: 'started' | 'paused'; step: MoveStep; moveId: number } ) @@ -123,33 +116,22 @@ const getFirstMove = ( return undefined } switch (step.type) { - case 'game': - throw new Error('TODO') case 'move': return { step, variables } - case 'rounds': { + case 'sequence': { const firstStep = getFirstMove(step.steps[0], variables, playerIds) if (!firstStep) { return undefined } - return { - step: firstStep.step, - variables: { - ...firstStep.variables, - [step.name]: firstStep.variables[step.name] ?? 1, - }, - } - } - case 'turns': { - const firstStep = getFirstMove(step.steps[0], variables, playerIds) - if (!firstStep) { - return undefined + if (!step.repeat) { + return firstStep } + return { step: firstStep.step, variables: { ...firstStep.variables, - [step.name]: firstStep.variables[step.name] ?? playerIds[0], + [step.name]: firstStep.variables[step.name] ?? (step.repeat.type === 'rounds' ? 1 : playerIds[0]), }, } } @@ -178,15 +160,12 @@ const getTransition = ( } } else { switch (step.type) { - case 'game': - throw new Error('TODO') case 'move': if (step.id === stepId) { current = step } break - case 'rounds': - case 'turns': { + case 'sequence': { const result = getTransition(step.steps, stepId, currentVariables, playerIds) if (!result) { break @@ -196,29 +175,31 @@ const getTransition = ( return result } - let next; - if (step.type === 'rounds') { - const round = currentVariables[step.name] as number - next = round < +step.amount ? round + 1 : undefined - } else { - const playerId = currentVariables[step.name] as number - const playerIndex = playerIds.indexOf(playerId) - next = playerIds[playerIndex + 1] - } - if (next) { - const firstStep = getFirstMove( - step, - { - ...result.variables, - [step.name]: next, - }, - playerIds - ) - if (firstStep) { - return { - current: result.current, - next: firstStep.step, - variables: firstStep.variables, + if (step.repeat) { + let nextValue; + if (step.repeat.type === 'rounds') { + const round = currentVariables[step.name] as number + nextValue = round < +step.repeat.amount ? round + 1 : undefined + } else { + const playerId = currentVariables[step.name] as number + const playerIndex = playerIds.indexOf(playerId) + nextValue = playerIds[playerIndex + 1] + } + if (nextValue) { + const firstStep = getFirstMove( + step, + { + ...result.variables, + [step.name]: nextValue, + }, + playerIds + ) + if (firstStep) { + return { + current: result.current, + next: firstStep.step, + variables: firstStep.variables, + } } } } @@ -260,8 +241,8 @@ async function createChild(data: any, parent: Post) { return post } -function insertVariables(text: string, variables: PlayVariables) { - return text?.replace(/\(([^)]+)\)/, (substring, variableName) => { +function insertVariables(text: string | undefined, variables: PlayVariables) { + return text?.replace(/\[([^\]]+)\]/, (substring, variableName) => { if (variableName in variables) { return `${variables[variableName]}` } @@ -293,18 +274,17 @@ async function startStep(gamePost: Post, step: MoveStep, variables: PlayVariable const newGame: Game = { ...game, play: { - ...play, status: 'started', step, moveId: movePost.id, variables: variables, + playerIds: play.playerIds, } } await updatePost(gamePost.id, { game: newGame }) scheduleMoveTimeout(movePost.id, move, timeout, io) - console.log('hu') io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) } @@ -327,7 +307,7 @@ async function moveTimeout(id: number, move: Move, io: Server) { async function nextStep(gamePost: Post, io: Server) { const game = gamePost.game!; const play = game.play!; - if (play.status !== 'started') { + if (play.status !== 'started' && play.status !== 'paused') { return; } const transition = getTransition( @@ -338,7 +318,21 @@ async function nextStep(gamePost: Post, io: Server) { ) if (transition?.next) { - await startStep(gamePost, transition.next, transition.variables, io) + if (play.status === 'started') { + await startStep(gamePost, transition.next, transition.variables, io) + } else { + const newGame: Game = { + ...game, + play: { + status: 'paused', + step: transition.next, + variables: transition.variables, + playerIds: play.playerIds, + } + } + await updatePost(gamePost.id, { game: newGame }) + io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) + } } else { const newGame: Game = { ...game, @@ -348,11 +342,6 @@ async function nextStep(gamePost: Post, io: Server) { variables: transition?.variables ?? play.variables } } - // await createChild({ - // type: 'post', - // mediaTypes: '', - // text: 'Play ended!', - // }, playPost) await updatePost(gamePost.id, { game: newGame }) io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) } @@ -372,15 +361,15 @@ function scheduleMoveTimeout(id: number, move: Move, timeout: number, io: Server const EVENTS = { outgoing: { - update: 'gs:outgoing-update-game', - start: 'gs:outgoing-start-game', - stop: 'gs:outgoing-stop-game', + update: 'gs:outgoing-update', + start: 'gs:outgoing-start', + stop: 'gs:outgoing-stop', - skip: 'gs:outgoing-skip-move', - pause: 'gs:outgoing-pause-move', + skip: 'gs:outgoing-skip', + pause: 'gs:outgoing-pause', }, incoming: { - updated: 'gs:incoming-updated-game' + updated: 'gs:incoming-updated' } } @@ -424,41 +413,52 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { const game = gamePost.game! const play = game.play; + console.log(play) + if (play.status === 'started') { return } if (play.status === 'paused') { - const newGame: Game = { - ...game, - play: { - ...play, - status: 'started' - } - } - await updatePost(id, { - game: newGame - }) - const movePost = await getPost(play.moveId); - const move = movePost.move!; - if (move.status === 'paused') { - const now = + new Date(); - const timeout = now + parseDuration(play.step.timeout)! - move.elapsedTime; - const newMove: Move = { - ...move, - status: 'started', - elapsedTime: move.elapsedTime, - startedAt: now, - timeout, + const moveId = play.moveId; + if (!moveId) { + console.log('startstep') + await startStep(gamePost, play.step, play.variables, io) + } else { + const newGame: Game = { + ...game, + play: { + status: 'started', + moveId, + playerIds: play.playerIds, + step: play.step, + variables: play.variables, + } } - await updatePost(play.moveId, { - move: newMove + await updatePost(id, { + game: newGame }) - scheduleMoveTimeout(play.moveId, newMove, timeout, io) + const movePost = await getPost(moveId); + const move = movePost.move!; + if (move.status === 'paused') { + const now = + new Date(); + const timeout = now + parseDuration(play.step.timeout)! - move.elapsedTime; + const newMove: Move = { + ...move, + status: 'started', + elapsedTime: move.elapsedTime, + startedAt: now, + timeout, + } + await updatePost(moveId, { + move: newMove + }) + scheduleMoveTimeout(moveId, newMove, timeout, io) + } + io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) } - io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) } else { - const step = getFirstMove(game.steps[0], play.variables, play.playerIds); + const step = getFirstMove(game.steps[0], {}, play.playerIds); if (!step) { throw new Error('No step.') } @@ -468,35 +468,22 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { }) socket.on(EVENTS.outgoing.skip, async ({ id }: { id: number }) => { - const post = await getPost(id); - const game = post.game!; - const play = game.play; - if (play.status !== 'started') { - return - } - const transition = getTransition( - game.steps, - play.step.id, - play.variables, - play.playerIds - ) - const newGame: Game = { - ...game, - play: transition?.next ? { - ...play, - step: transition.next, - variables: transition.variables - } : { - playerIds: play.playerIds, - status: 'ended', - variables: {} + const gamePost = await getPost(id); + const play = gamePost.game!.play + if ((play.status === 'started' || play.status === 'paused') && play.moveId) { + const movePost = await getPost(play.moveId); + const move = movePost.move!; + if (move.status === 'started' || move.status === 'paused') { + const newMove: Move = { + ...move, + status: 'skipped', + } + await updatePost(play.moveId, { + move: newMove + }) } } - - await updatePost(id, { - game: newGame - }) - io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) + await nextStep(gamePost, io) }) socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { @@ -508,12 +495,14 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { return; } - const newGame: Game = { ...game, play: { - ...play, - status: 'paused' + status: 'paused', + step: play.step, + variables: play.variables, + moveId: play.moveId, + playerIds: play.playerIds, } } await updatePost(id, { game: newGame }) @@ -548,22 +537,25 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { const newGame: Game = { ...game, play: { - ...play, - status: 'stopped' + status: 'stopped', + variables: play.variables, + playerIds: play.playerIds, } } await updatePost(id, { game: newGame }) - const movePost = await getPost(play.moveId); - const move = movePost.move!; - if (move.status === 'started' || move.status === 'paused') { - const newMove: Move = { - ...move, - status: 'stopped', + if (play.moveId) { + const movePost = await getPost(play.moveId); + const move = movePost.move!; + if (move.status === 'started' || move.status === 'paused') { + const newMove: Move = { + ...move, + status: 'stopped', + } + await updatePost(play.moveId, { + move: newMove + }) } - await updatePost(play.moveId, { - move: newMove - }) } io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) diff --git a/Helpers.js b/Helpers.js index 8e2a04f..cab2476 100644 --- a/Helpers.js +++ b/Helpers.js @@ -39,6 +39,7 @@ var ffmpeg = require('fluent-ffmpeg') const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path ffmpeg.setFfmpegPath(ffmpegPath) const sgMail = require('@sendgrid/mail') +const { uniq } = require('lodash') sgMail.setApiKey(process.env.SENDGRID_API_KEY) const imageMBLimit = 10 @@ -1148,9 +1149,9 @@ function findPostInclude(accountId) { }, { model: Link, - as: 'Original', + as: 'Originals', required: false, - where: { relationship: 'spawn', state: 'active' }, + where: { relationship: 'remix', state: 'active' }, include: { model: Post, as: 'Parent', @@ -1159,9 +1160,9 @@ function findPostInclude(accountId) { }, { model: Link, - as: 'Spawns', + as: 'Remixes', separate: true, - where: { relationship: 'spawn', state: 'active' }, + where: { relationship: 'remix', state: 'active' }, order: [['index', 'ASC']], include: { model: Post, @@ -1707,6 +1708,50 @@ function addGBGPlayers(postId, creator, settings) { }) } +async function addRemixes(accountId, game, postId) { + let originals = [] + function findOriginals(steps) { + for (const step of steps) { + if (step.originalStep) { + originals.push(step.originalStep.gameId) + } + if (step.type === 'sequence') { + findOriginals(step.steps) + } + } + } + findOriginals(game.steps) + originals = uniq(originals); + const links = await Link.findAll({ + attributes: ['itemAId'], + where: { + state: 'active', + itemAType: 'post', + itemBType: 'post', + itemAId: originals, + itemBId: postId, + relationship: 'remix' + } + }) + for (const originalId of originals) { + if (links?.some(link => link.itemAId === originalId)) { + continue + } + await Link.create({ + creatorId: accountId, + state: 'active', + itemAType: 'post', + itemBType: 'post', + itemAId: originalId, + itemBId: postId, + relationship: 'remix', + totalLikes: 0, + totalComments: 0, + totalRatings: 0 + }) + } +} + // todo: // + check notifyMentions is adding the correct notification type function createPost(data, files, accountId) { @@ -1751,6 +1796,10 @@ function createPost(data, files, accountId) { move, }) + if (game) { + await addRemixes(accountId, game, post.id) + } + // todo: add the correct notification type const notifyMentions = mentions?.length ? await new Promise(async (resolve) => { @@ -2245,6 +2294,7 @@ module.exports = { accountLink, uploadFiles, createPost, + addRemixes, attachComment, scheduleNextBeadDeadline, pushNotification, diff --git a/models/Post.js b/models/Post.js index de24265..2050873 100644 --- a/models/Post.js +++ b/models/Post.js @@ -50,8 +50,8 @@ module.exports = (sequelize, DataTypes) => { Post.hasMany(models.Link, { as: 'UrlBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'ImageBlocks', foreignKey: 'itemAId' }) Post.hasMany(models.Link, { as: 'AudioBlocks', foreignKey: 'itemAId' }) - Post.hasOne(models.Link, { as: 'Original', foreignKey: 'itemBId' }) - Post.hasMany(models.Link, { as: 'Spawns', foreignKey: 'itemAId' }) + Post.hasOne(models.Link, { as: 'Originals', foreignKey: 'itemBId' }) + Post.hasMany(models.Link, { as: 'Remixes', foreignKey: 'itemAId' }) Post.hasOne(models.Link, { as: 'MediaLink', foreignKey: 'itemAId' }) // used for post map (todo: rethink...) Post.hasMany(models.Link, { as: 'OutgoingPostLinks', foreignKey: 'itemAId' }) diff --git a/routes/Post.js b/routes/Post.js index 83e8979..6aa999e 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -34,6 +34,7 @@ const { pushNotification, fullPostAttributes, defaultPostValues, + addRemixes, } = require('../Helpers') const { Space, @@ -363,20 +364,29 @@ router.get('/link-data', authenticateToken, async (req, res) => { res.status(200).json({ source, link, target }) }) -router.get('/target-from-text', authenticateToken, async (req, res) => { +router.get('/search', authenticateToken, async (req, res) => { const accountId = req.user ? req.user.id : null - const { type, sourceId, text, userId } = req.query + const { type, sourceId, search, userId, mediaType } = req.query const where = { type: type.toLowerCase(), state: 'active', - [Op.or]: [{ text: { [Op.like]: `%${text}%` } }, { title: { [Op.like]: `%${text}%` } }], + [Op.or]: [{ text: { [Op.like]: `%${search}%` } }, { title: { [Op.like]: `%${search}%` } }], } if (sourceId) where[Op.not] = { id: sourceId } if (userId) where.creatorId = userId + if (mediaType) { + if (mediaType === 'game') { + where[Op.and] = [{ mediaTypes: { [Op.like]: `%${mediaType}%` } }, { [Op.not]: { mediaTypes: { [Op.like]: `%glass-bead-game%` } } }] + } + } else { + where.mediaTypes = { [Op.like]: `%${mediaType}%` } + } + const matchingPosts = await Post.findAll({ where, limit: 10, - include: findPostInclude(accountId), + // TODO: no idea why this fails + include: findPostInclude(accountId).filter(include => !['UrlBlocks', 'ImageBlocks', 'AudioBlocks'].includes(include.as)), }) res.status(200).json(matchingPosts) }) @@ -1777,6 +1787,9 @@ router.post('/update-post', authenticateToken, async (req, res) => { { where: { id, creatorId: accountId } } ) promises.push(updatePost) + if ('game' in req.body) { + await addRemixes(accountId, req.body.game, id) + } if ('urls' in req.body) { const newUrls = req.body.urls; // update urls From c14feed43d2a7d6f6371e1c80d0c382a7f69565e Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 26 Mar 2024 16:14:25 +0100 Subject: [PATCH 11/21] improve flow --- GameServer.js | 226 ++++++++++++++++++++------------------ GameServer.ts | 293 ++++++++++++++++++++++++++----------------------- routes/Post.js | 19 +++- 3 files changed, 292 insertions(+), 246 deletions(-) diff --git a/GameServer.js b/GameServer.js index 2c6afb7..ae1bc54 100644 --- a/GameServer.js +++ b/GameServer.js @@ -34,10 +34,12 @@ const POST_TYPE = [ function getPost(id) { return __awaiter(this, void 0, void 0, function* () { const post = yield Post.findOne({ + raw: true, + nest: true, where: { state: 'active', id - } + }, }); if (!post) { throw new Error(`Post ${id} not found`); @@ -45,9 +47,10 @@ function getPost(id) { return post; }); } -function updatePost(id, data) { +function updatePost(post, data) { return __awaiter(this, void 0, void 0, function* () { - return yield Post.update(data, { where: { id } }); + yield Post.update(data, { where: { id: post.id } }); + return Object.assign(Object.assign({}, post), data); }); } const getFirstMove = (step, variables, playerIds) => { @@ -167,7 +170,7 @@ function insertVariables(text, variables) { return substring; }); } -function startStep(gamePost, step, variables, io) { +function startNewMove(gamePost, step, variables, io) { return __awaiter(this, void 0, void 0, function* () { const game = gamePost.game; const play = game.play; @@ -187,33 +190,20 @@ function startStep(gamePost, step, variables, io) { text: insertVariables(step.text, variables), move }, gamePost); - const newGame = Object.assign(Object.assign({}, game), { play: { - status: 'started', - step, - moveId: movePost.id, - variables: variables, - playerIds: play.playerIds, - } }); - yield updatePost(gamePost.id, { game: newGame }); - scheduleMoveTimeout(movePost.id, move, timeout, io); - io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); - }); -} -function moveTimeout(id, move, io) { - return __awaiter(this, void 0, void 0, function* () { - console.log('move timeout!'); - const newMove = Object.assign(Object.assign({}, move), { status: 'ended' }); - yield updatePost(id, { - move: newMove + scheduleMoveTimeout(movePost, io); + const changedGamePost = yield updatePost(gamePost, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'started', + step, + moveId: movePost.id, + variables: variables, + playerIds: play.playerIds, + } }) }); - if (move.gameId) { - const gamePost = yield getPost(move.gameId); - console.log(gamePost); - nextStep(gamePost, io); - } + return { changedGamePost, changedMoves: [movePost] }; }); } -function nextStep(gamePost, io) { +function nextMove(gamePost, io) { return __awaiter(this, void 0, void 0, function* () { var _a; const game = gamePost.game; @@ -224,39 +214,58 @@ function nextStep(gamePost, io) { const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); if (transition === null || transition === void 0 ? void 0 : transition.next) { if (play.status === 'started') { - yield startStep(gamePost, transition.next, transition.variables, io); + return yield startNewMove(gamePost, transition.next, transition.variables, io); } else { - const newGame = Object.assign(Object.assign({}, game), { play: { - status: 'paused', - step: transition.next, - variables: transition.variables, - playerIds: play.playerIds, - } }); - yield updatePost(gamePost.id, { game: newGame }); - io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); + const changedGamePost = yield updatePost(gamePost, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'paused', + step: transition.next, + variables: transition.variables, + playerIds: play.playerIds, + } }) + }); + return { + changedGamePost, + }; } } else { - const newGame = Object.assign(Object.assign({}, game), { play: { - playerIds: play.playerIds, - status: 'ended', - variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables - } }); - yield updatePost(gamePost.id, { game: newGame }); - io.in(gamePost.id).emit(EVENTS.incoming.updated, { game: newGame }); + const changedGamePost = yield updatePost(gamePost, { + game: Object.assign(Object.assign({}, game), { play: { + playerIds: play.playerIds, + status: 'ended', + variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables + } }) + }); + return { changedGamePost }; + } + }); +} +function moveTimeout(movePost, io) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + const move = movePost.move; + const newMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'ended' }) + }); + if (move.gameId) { + const gamePost = yield getPost(move.gameId); + const changes = yield nextMove(gamePost, io); + if (changes) { + emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: [newMovePost, ...(_a = changes.changedMoves) !== null && _a !== void 0 ? _a : []] })); + } } }); } -function scheduleMoveTimeout(id, move, timeout, io) { - node_schedule_1.default.scheduleJob(timeout, () => __awaiter(this, void 0, void 0, function* () { - const currentPost = yield getPost(id); - if (!(0, lodash_1.isEqual)(currentPost.move, move)) { - console.log(currentPost.move, move); - // The state of the move has changed, this job is outdated. +function scheduleMoveTimeout(movePost, io) { + node_schedule_1.default.scheduleJob(movePost.move.timeout, () => __awaiter(this, void 0, void 0, function* () { + const currentMovePost = yield getPost(movePost.id); + if (!(0, lodash_1.isEqual)(currentMovePost.move, movePost.move)) { + console.log('The state of the move has changed, skipping job.'); return; } - moveTimeout(id, move, io); + yield moveTimeout(currentMovePost, io); })); } const EVENTS = { @@ -285,64 +294,69 @@ function initializeGameServerTasks(io) { continue; } if (move.timeout < +new Date()) { - moveTimeout(post.id, move, io); + yield moveTimeout(post, io); } else { - scheduleMoveTimeout(post.id, move, move.timeout, io); + scheduleMoveTimeout(post, io); } } }); } exports.initializeGameServerTasks = initializeGameServerTasks; +function emitChanges(io, { changedGamePost, changedMoves }) { + io.in(changedGamePost.id).emit(EVENTS.incoming.updated, { game: changedGamePost.game, changedChildren: changedMoves }); +} function registerGameServerEvents(socket, io) { return __awaiter(this, void 0, void 0, function* () { socket.on(EVENTS.outgoing.update, (_a) => __awaiter(this, [_a], void 0, function* ({ id, game }) { - yield updatePost(id, { + const post = yield getPost(id); + const changedGamePost = yield updatePost(post, { game }); - io.in(id).emit(EVENTS.incoming.updated, { game }); + emitChanges(io, { changedGamePost }); })); socket.on(EVENTS.outgoing.start, (_b) => __awaiter(this, [_b], void 0, function* ({ id }) { - const gamePost = yield Post.findOne({ - where: { - id - } - }); + const gamePost = yield getPost(id); const game = gamePost.game; const play = game.play; - console.log(play); if (play.status === 'started') { return; } + let changes; if (play.status === 'paused') { const moveId = play.moveId; if (!moveId) { - console.log('startstep'); - yield startStep(gamePost, play.step, play.variables, io); + changes = yield startNewMove(gamePost, play.step, play.variables, io); } else { - const newGame = Object.assign(Object.assign({}, game), { play: { - status: 'started', - moveId, - playerIds: play.playerIds, - step: play.step, - variables: play.variables, - } }); - yield updatePost(id, { - game: newGame + const changedGamePost = yield updatePost(gamePost, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'started', + moveId, + playerIds: play.playerIds, + step: play.step, + variables: play.variables, + } }) }); const movePost = yield getPost(moveId); const move = movePost.move; if (move.status === 'paused') { const now = +new Date(); const timeout = now + (0, parse_duration_1.default)(play.step.timeout) - move.elapsedTime; - const newMove = Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout }); - yield updatePost(moveId, { - move: newMove + const newMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout }) }); - scheduleMoveTimeout(moveId, newMove, timeout, io); + scheduleMoveTimeout(newMovePost, io); + changes = { + changedGamePost, + changedMoves: [newMovePost] + }; + } + else { + changes = { + changedGamePost + }; } - io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); } } else { @@ -350,23 +364,25 @@ function registerGameServerEvents(socket, io) { if (!step) { throw new Error('No step.'); } - yield startStep(gamePost, step.step, step.variables, io); + changes = yield startNewMove(gamePost, step.step, step.variables, io); } + emitChanges(io, changes); })); socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { const gamePost = yield getPost(id); const play = gamePost.game.play; + let skippedMovePost; if ((play.status === 'started' || play.status === 'paused') && play.moveId) { const movePost = yield getPost(play.moveId); const move = movePost.move; if (move.status === 'started' || move.status === 'paused') { - const newMove = Object.assign(Object.assign({}, move), { status: 'skipped' }); - yield updatePost(play.moveId, { - move: newMove + skippedMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'skipped' }) }); } } - yield nextStep(gamePost, io); + const changes = yield nextMove(gamePost, io); + emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: skippedMovePost && [skippedMovePost] })); })); socket.on(EVENTS.outgoing.pause, (_d) => __awaiter(this, [_d], void 0, function* ({ id }) { const post = yield getPost(id); @@ -375,24 +391,25 @@ function registerGameServerEvents(socket, io) { if (play.status !== 'started') { return; } - const newGame = Object.assign(Object.assign({}, game), { play: { - status: 'paused', - step: play.step, - variables: play.variables, - moveId: play.moveId, - playerIds: play.playerIds, - } }); - yield updatePost(id, { game: newGame }); + const changedGamePost = yield updatePost(post, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'paused', + step: play.step, + variables: play.variables, + moveId: play.moveId, + playerIds: play.playerIds, + } }) + }); const movePost = yield getPost(play.moveId); const move = movePost.move; + let pausedMovePost; if (move.status === 'started') { const now = +new Date(); - const newMove = Object.assign(Object.assign({}, move), { status: 'paused', elapsedTime: move.elapsedTime + now - move.startedAt, remainingTime: move.timeout - now }); - yield updatePost(play.moveId, { - move: newMove + pausedMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'paused', elapsedTime: move.elapsedTime + now - move.startedAt, remainingTime: move.timeout - now }) }); } - io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); + emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] }); })); socket.on(EVENTS.outgoing.stop, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { const post = yield getPost(id); @@ -401,23 +418,24 @@ function registerGameServerEvents(socket, io) { if (play.status !== 'started' && play.status !== 'paused') { return; } - const newGame = Object.assign(Object.assign({}, game), { play: { - status: 'stopped', - variables: play.variables, - playerIds: play.playerIds, - } }); - yield updatePost(id, { game: newGame }); + const changedGamePost = yield updatePost(post, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'stopped', + variables: play.variables, + playerIds: play.playerIds, + } }) + }); + let stoppedMovePost; if (play.moveId) { const movePost = yield getPost(play.moveId); const move = movePost.move; if (move.status === 'started' || move.status === 'paused') { - const newMove = Object.assign(Object.assign({}, move), { status: 'stopped' }); - yield updatePost(play.moveId, { - move: newMove + stoppedMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'stopped' }) }); } } - io.in(id).emit(EVENTS.incoming.updated, { game: newGame }); + emitChanges(io, { changedGamePost, changedMoves: stoppedMovePost && [stoppedMovePost] }); })); }); } diff --git a/GameServer.ts b/GameServer.ts index 9e1032d..7702a30 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -90,10 +90,12 @@ export type Play = { async function getPost(id: number): Promise { const post = await Post.findOne({ + raw: true, + nest: true, where: { state: 'active', id - } + }, }) if (!post) { @@ -103,8 +105,12 @@ async function getPost(id: number): Promise { return post; } -async function updatePost(id: number, data: Partial) { - return await Post.update(data, { where: { id } }) +async function updatePost(post: Post, data: Partial): Promise { + await Post.update(data, { where: { id: post.id } }) + return { + ...post, + ...data, + } } const getFirstMove = ( @@ -224,7 +230,7 @@ const getTransition = ( } -async function createChild(data: any, parent: Post) { +async function createChild(data: any, parent: Post): Promise { const { post } = await createPost(data, [], parent.creatorId) await Link.create({ creatorId: parent.creatorId, @@ -250,7 +256,9 @@ function insertVariables(text: string | undefined, variables: PlayVariables) { }) } -async function startStep(gamePost: Post, step: MoveStep, variables: PlayVariables, io: Server) { +type Changes = { changedGamePost: Post, changedMoves?: Post[] } + +async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVariables, io: Server): Promise { const game = gamePost.game! const play = game.play const now = +new Date(); @@ -270,41 +278,25 @@ async function startStep(gamePost: Post, step: MoveStep, variables: PlayVariable text: insertVariables(step.text, variables), move }, gamePost) + scheduleMoveTimeout(movePost, io) - const newGame: Game = { - ...game, - play: { - status: 'started', - step, - moveId: movePost.id, - variables: variables, - playerIds: play.playerIds, + const changedGamePost = await updatePost(gamePost, { + game: { + ...game, + play: { + status: 'started', + step, + moveId: movePost.id, + variables: variables, + playerIds: play.playerIds, + } } - } - - await updatePost(gamePost.id, { game: newGame }) - - scheduleMoveTimeout(movePost.id, move, timeout, io) - io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) -} - -async function moveTimeout(id: number, move: Move, io: Server) { - console.log('move timeout!') - const newMove: Move = { - ...move, - status: 'ended' - } - await updatePost(id, { - move: newMove }) - if (move.gameId) { - const gamePost = await getPost(move.gameId); - console.log(gamePost) - nextStep(gamePost, io) - } + + return { changedGamePost, changedMoves: [movePost] } } -async function nextStep(gamePost: Post, io: Server) { +async function nextMove(gamePost: Post, io: Server): Promise { const game = gamePost.game!; const play = game.play!; if (play.status !== 'started' && play.status !== 'paused') { @@ -319,43 +311,64 @@ async function nextStep(gamePost: Post, io: Server) { if (transition?.next) { if (play.status === 'started') { - await startStep(gamePost, transition.next, transition.variables, io) + return await startNewMove(gamePost, transition.next, transition.variables, io) } else { - const newGame: Game = { + const changedGamePost = await updatePost(gamePost, { + game: { + ...game, + play: { + status: 'paused', + step: transition.next, + variables: transition.variables, + playerIds: play.playerIds, + } + } + }) + return { + changedGamePost, + } + } + } else { + const changedGamePost = await updatePost(gamePost, { + game: { ...game, play: { - status: 'paused', - step: transition.next, - variables: transition.variables, playerIds: play.playerIds, + status: 'ended', + variables: transition?.variables ?? play.variables } } - await updatePost(gamePost.id, { game: newGame }) - io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) + }) + return { changedGamePost } + } +} + + +async function moveTimeout(movePost: Post, io: Server) { + const move = movePost.move! + const newMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'ended' } - } else { - const newGame: Game = { - ...game, - play: { - playerIds: play.playerIds, - status: 'ended', - variables: transition?.variables ?? play.variables - } + }) + if (move.gameId) { + const gamePost = await getPost(move.gameId); + const changes = await nextMove(gamePost, io) + if (changes) { + emitChanges(io, { ...changes!, changedMoves: [newMovePost, ...changes.changedMoves ?? []] }) } - await updatePost(gamePost.id, { game: newGame }) - io.in(gamePost.id as any).emit(EVENTS.incoming.updated, { game: newGame }) } } -function scheduleMoveTimeout(id: number, move: Move, timeout: number, io: Server) { - schedule.scheduleJob(timeout, async () => { - const currentPost = await getPost(id) - if (!isEqual(currentPost.move, move)) { - console.log(currentPost.move, move) - // The state of the move has changed, this job is outdated. +function scheduleMoveTimeout(movePost: Post, io: Server) { + schedule.scheduleJob((movePost.move! as Extract).timeout, async () => { + const currentMovePost = await getPost(movePost.id) + if (!isEqual(currentMovePost.move, movePost.move)) { + console.log('The state of the move has changed, skipping job.') return } - moveTimeout(id, move, io) + await moveTimeout(currentMovePost, io) }) } @@ -388,74 +401,78 @@ export async function initializeGameServerTasks(io: Server) { } if (move.timeout < +new Date()) { - moveTimeout(post.id, move, io) + await moveTimeout(post, io) } else { - scheduleMoveTimeout(post.id, move, move.timeout, io) + scheduleMoveTimeout(post, io) } } } +function emitChanges(io: Server, { changedGamePost, changedMoves }: Changes) { + io.in(changedGamePost.id as any).emit(EVENTS.incoming.updated, { game: changedGamePost.game!, changedChildren: changedMoves }) +} + export async function registerGameServerEvents(socket: Socket, io: Server) { socket.on(EVENTS.outgoing.update, async ({ id, game }: { id: number, game: Game }) => { - await updatePost(id, { + const post = await getPost(id) + const changedGamePost = await updatePost(post, { game }) - io.in(id as any).emit(EVENTS.incoming.updated, { game }) + emitChanges(io, { changedGamePost }) }) socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => { - const gamePost: Post = await Post.findOne({ - where: { - id - } - }) + const gamePost: Post = await getPost(id) const game = gamePost.game! const play = game.play; - console.log(play) - if (play.status === 'started') { return } + let changes: Changes; if (play.status === 'paused') { const moveId = play.moveId; if (!moveId) { - console.log('startstep') - await startStep(gamePost, play.step, play.variables, io) + changes = await startNewMove(gamePost, play.step, play.variables, io) } else { - const newGame: Game = { - ...game, - play: { - status: 'started', - moveId, - playerIds: play.playerIds, - step: play.step, - variables: play.variables, + const changedGamePost = await updatePost(gamePost, { + game: { + ...game, + play: { + status: 'started', + moveId, + playerIds: play.playerIds, + step: play.step, + variables: play.variables, + } } - } - await updatePost(id, { - game: newGame }) const movePost = await getPost(moveId); const move = movePost.move!; if (move.status === 'paused') { const now = + new Date(); const timeout = now + parseDuration(play.step.timeout)! - move.elapsedTime; - const newMove: Move = { - ...move, - status: 'started', - elapsedTime: move.elapsedTime, - startedAt: now, - timeout, - } - await updatePost(moveId, { - move: newMove + const newMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'started', + elapsedTime: move.elapsedTime, + startedAt: now, + timeout, + } }) - scheduleMoveTimeout(moveId, newMove, timeout, io) + scheduleMoveTimeout(newMovePost, io) + changes = { + changedGamePost, + changedMoves: [newMovePost] + } + } else { + changes = { + changedGamePost + } } - io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) } } else { const step = getFirstMove(game.steps[0], {}, play.playerIds); @@ -463,27 +480,29 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { throw new Error('No step.') } - await startStep(gamePost, step.step, step.variables, io) + changes = await startNewMove(gamePost, step.step, step.variables, io) } + emitChanges(io, changes) }) socket.on(EVENTS.outgoing.skip, async ({ id }: { id: number }) => { const gamePost = await getPost(id); const play = gamePost.game!.play + let skippedMovePost: Post | undefined; if ((play.status === 'started' || play.status === 'paused') && play.moveId) { const movePost = await getPost(play.moveId); const move = movePost.move!; if (move.status === 'started' || move.status === 'paused') { - const newMove: Move = { - ...move, - status: 'skipped', - } - await updatePost(play.moveId, { - move: newMove + skippedMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'skipped', + } }) } } - await nextStep(gamePost, io) + const changes = await nextMove(gamePost, io) + emitChanges(io, { ...changes!, changedMoves: skippedMovePost && [skippedMovePost] }) }) socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { @@ -495,34 +514,35 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { return; } - const newGame: Game = { - ...game, - play: { - status: 'paused', - step: play.step, - variables: play.variables, - moveId: play.moveId, - playerIds: play.playerIds, + const changedGamePost = await updatePost(post, { + game: { + ...game, + play: { + status: 'paused', + step: play.step, + variables: play.variables, + moveId: play.moveId, + playerIds: play.playerIds, + } } - } - await updatePost(id, { game: newGame }) + }) const movePost = await getPost(play.moveId); const move = movePost.move!; + let pausedMovePost: Post | undefined; if (move.status === 'started') { const now = + new Date(); - const newMove: Move = { - ...move, - status: 'paused', - elapsedTime: move.elapsedTime + now - move.startedAt, - remainingTime: move.timeout - now - } - await updatePost(play.moveId, { - move: newMove + pausedMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'paused', + elapsedTime: move.elapsedTime + now - move.startedAt, + remainingTime: move.timeout - now + } }) } - io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) + emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] }) }) socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => { @@ -534,30 +554,31 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { return } - const newGame: Game = { - ...game, - play: { - status: 'stopped', - variables: play.variables, - playerIds: play.playerIds, + const changedGamePost = await updatePost(post, { + game: { + ...game, + play: { + status: 'stopped', + variables: play.variables, + playerIds: play.playerIds, + } } - } - await updatePost(id, { game: newGame }) + }) + let stoppedMovePost: Post | undefined; if (play.moveId) { const movePost = await getPost(play.moveId); const move = movePost.move!; if (move.status === 'started' || move.status === 'paused') { - const newMove: Move = { - ...move, - status: 'stopped', - } - await updatePost(play.moveId, { - move: newMove + stoppedMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'stopped', + } }) } } - io.in(id as any).emit(EVENTS.incoming.updated, { game: newGame }) + emitChanges(io, { changedGamePost, changedMoves: stoppedMovePost && [stoppedMovePost] }) }) } diff --git a/routes/Post.js b/routes/Post.js index 6aa999e..11ae6ec 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -366,7 +366,7 @@ router.get('/link-data', authenticateToken, async (req, res) => { router.get('/search', authenticateToken, async (req, res) => { const accountId = req.user ? req.user.id : null - const { type, sourceId, search, userId, mediaType } = req.query + const { type, sourceId, search, userId, mediaType, ids } = req.query const where = { type: type.toLowerCase(), state: 'active', @@ -770,11 +770,9 @@ router.get('/post-comments', async (req, res) => { router.get('/post-children', async (req, res) => { const accountId = req.user ? req.user.id : null - const { postId, limit, offset } = req.query; + const { postId, limit, offset, childrenIds } = req.query; - const links = await Link.findAll({ - offset: +offset, - limit: +limit, + const query = { order: [['createdAt', 'DESC']], attributes: ['state'], include: [ @@ -796,7 +794,16 @@ router.get('/post-children', async (req, res) => { itemAType: 'post', itemAId: postId, } - }) + } + + if (childrenIds) { + query.where.itemBId = childrenIds.split(',') + } else { + query.offset = +offset; + query.limit = +limit; + } + + const links = await Link.findAll(query) res.status(200).json({ children: links.map(link => link.Post) }) }) From 2b005b1033fa794de04700480ad8f7f3deaf4374 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Tue, 26 Mar 2024 16:31:36 +0100 Subject: [PATCH 12/21] add new post notification --- routes/Post.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/routes/Post.js b/routes/Post.js index 11ae6ec..fb2e78c 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -1442,7 +1442,7 @@ router.post('/create-comment', authenticateToken, async (req, res) => { }) const parentPost = await Post.findOne({ where: { id: parent.id }, - attributes: ['id', 'type'], + attributes: ['id', 'type', 'game'], include: { model: User, as: 'Creator', @@ -1486,6 +1486,12 @@ router.post('/create-comment', authenticateToken, async (req, res) => {

`, }) + + if (parentPost.game) { + const io = req.app.get('socketio') + io.to(parent.id).emit('gs:incoming-updated', { changedChildren: [post] }) + } + Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) From 7a8cadfbc359ee3be11fb07a5af8cc4b0840d08c Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 27 Mar 2024 03:02:14 +0100 Subject: [PATCH 13/21] improve variable handling --- GameServer.js | 54 +++++++++++++++++++++++---------------- GameServer.ts | 70 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/GameServer.js b/GameServer.js index ae1bc54..ed0f885 100644 --- a/GameServer.js +++ b/GameServer.js @@ -53,7 +53,7 @@ function updatePost(post, data) { return Object.assign(Object.assign({}, post), data); }); } -const getFirstMove = (step, variables, playerIds) => { +const getFirstMove = (step, variables, players) => { var _a; if (!step) { return undefined; @@ -62,7 +62,10 @@ const getFirstMove = (step, variables, playerIds) => { case 'move': return { step, variables }; case 'sequence': { - const firstStep = getFirstMove(step.steps[0], variables, playerIds); + if (step.repeat && ((step.repeat.type === 'rounds' && step.repeat.amount === 0) || (step.repeat.type === 'turns' && players.length === 0))) { + return undefined; + } + const firstStep = getFirstMove(step.steps[0], variables, players); if (!firstStep) { return undefined; } @@ -71,7 +74,7 @@ const getFirstMove = (step, variables, playerIds) => { } return { step: firstStep.step, - variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_a = firstStep.variables[step.name]) !== null && _a !== void 0 ? _a : (step.repeat.type === 'rounds' ? 1 : playerIds[0]) }), + variables: Object.assign(Object.assign({}, firstStep.variables), { [step.name]: (_a = firstStep.variables[step.name]) !== null && _a !== void 0 ? _a : (step.repeat.type === 'rounds' ? 1 : players[0]) }), }; } default: { @@ -80,13 +83,13 @@ const getFirstMove = (step, variables, playerIds) => { } } }; -const getTransition = (steps, stepId, variables, playerIds) => { +const getTransition = (steps, stepId, variables, players) => { let current; let currentVariables = variables; for (let i = 0; i < steps.length; i++) { const step = steps[i]; if (current) { - const nextStep = getFirstMove(step, currentVariables, playerIds); + const nextStep = getFirstMove(step, currentVariables, players); if (nextStep) { return { current, next: nextStep.step, variables: nextStep.variables }; } @@ -99,7 +102,7 @@ const getTransition = (steps, stepId, variables, playerIds) => { } break; case 'sequence': { - const result = getTransition(step.steps, stepId, currentVariables, playerIds); + const result = getTransition(step.steps, stepId, currentVariables, players); if (!result) { break; } @@ -113,12 +116,12 @@ const getTransition = (steps, stepId, variables, playerIds) => { nextValue = round < +step.repeat.amount ? round + 1 : undefined; } else { - const playerId = currentVariables[step.name]; - const playerIndex = playerIds.indexOf(playerId); - nextValue = playerIds[playerIndex + 1]; + const player = currentVariables[step.name]; + const playerIndex = players.findIndex(p => p.id === player.id); + nextValue = players[playerIndex + 1]; } if (nextValue) { - const firstStep = getFirstMove(step, Object.assign(Object.assign({}, result.variables), { [step.name]: nextValue }), playerIds); + const firstStep = getFirstMove(step, Object.assign(Object.assign({}, result.variables), { [step.name]: nextValue }), players); if (firstStep) { return { current: result.current, @@ -165,6 +168,10 @@ function createChild(data, parent) { function insertVariables(text, variables) { return text === null || text === void 0 ? void 0 : text.replace(/\[([^\]]+)\]/, (substring, variableName) => { if (variableName in variables) { + const value = variables[variableName]; + if (typeof value === 'object') { + return `@${value.name}`; + } return `${variables[variableName]}`; } return substring; @@ -197,7 +204,6 @@ function startNewMove(gamePost, step, variables, io) { step, moveId: movePost.id, variables: variables, - playerIds: play.playerIds, } }) }); return { changedGamePost, changedMoves: [movePost] }; @@ -211,7 +217,7 @@ function nextMove(gamePost, io) { if (play.status !== 'started' && play.status !== 'paused') { return; } - const transition = getTransition(game.steps, play.step.id, play.variables, play.playerIds); + const transition = getTransition(game.steps, play.step.id, play.variables, game.players); if (transition === null || transition === void 0 ? void 0 : transition.next) { if (play.status === 'started') { return yield startNewMove(gamePost, transition.next, transition.variables, io); @@ -222,7 +228,6 @@ function nextMove(gamePost, io) { status: 'paused', step: transition.next, variables: transition.variables, - playerIds: play.playerIds, } }) }); return { @@ -233,7 +238,6 @@ function nextMove(gamePost, io) { else { const changedGamePost = yield updatePost(gamePost, { game: Object.assign(Object.assign({}, game), { play: { - playerIds: play.playerIds, status: 'ended', variables: (_a = transition === null || transition === void 0 ? void 0 : transition.variables) !== null && _a !== void 0 ? _a : play.variables } }) @@ -333,7 +337,6 @@ function registerGameServerEvents(socket, io) { game: Object.assign(Object.assign({}, game), { play: { status: 'started', moveId, - playerIds: play.playerIds, step: play.step, variables: play.variables, } }) @@ -360,11 +363,22 @@ function registerGameServerEvents(socket, io) { } } else { - const step = getFirstMove(game.steps[0], {}, play.playerIds); - if (!step) { - throw new Error('No step.'); + const variables = {}; + const step = getFirstMove(game.steps[0], variables, game.players); + if (step) { + changes = yield startNewMove(gamePost, step.step, step.variables, io); + } + else { + const changedGamePost = yield updatePost(gamePost, { + game: Object.assign(Object.assign({}, game), { play: { + status: 'ended', + variables + } }) + }); + changes = { + changedGamePost + }; } - changes = yield startNewMove(gamePost, step.step, step.variables, io); } emitChanges(io, changes); })); @@ -397,7 +411,6 @@ function registerGameServerEvents(socket, io) { step: play.step, variables: play.variables, moveId: play.moveId, - playerIds: play.playerIds, } }) }); const movePost = yield getPost(play.moveId); @@ -422,7 +435,6 @@ function registerGameServerEvents(socket, io) { game: Object.assign(Object.assign({}, game), { play: { status: 'stopped', variables: play.variables, - playerIds: play.playerIds, } }) }); let stoppedMovePost; diff --git a/GameServer.ts b/GameServer.ts index 7702a30..ed0c048 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -64,8 +64,17 @@ type MoveStep = Extract type Game = { steps: Step[] play: Play + players: BaseUser[] } +export type BaseUser = { + id: number + handle: string + name: string + flagImagePath: string +} + + type Move = ( | { status: 'skipped' | 'ended' | 'stopped' } | { status: 'paused'; elapsedTime: number; remainingTime: number } @@ -77,10 +86,9 @@ type Move = ( } ) & { gameId?: number } -type PlayVariables = Record +type PlayVariables = Record export type Play = { - playerIds: number[] variables: PlayVariables } & ( | { status: 'waiting' | 'stopped' | 'ended' } @@ -116,7 +124,7 @@ async function updatePost(post: Post, data: Partial): Promise { const getFirstMove = ( step: Step | undefined, variables: PlayVariables, - playerIds: number[] + players: BaseUser[] ): undefined | { step: MoveStep; variables: PlayVariables } => { if (!step) { return undefined @@ -125,7 +133,11 @@ const getFirstMove = ( case 'move': return { step, variables } case 'sequence': { - const firstStep = getFirstMove(step.steps[0], variables, playerIds) + if (step.repeat && ((step.repeat.type === 'rounds' && step.repeat.amount === 0) || (step.repeat.type === 'turns' && players.length === 0))) { + return undefined + } + + const firstStep = getFirstMove(step.steps[0], variables, players) if (!firstStep) { return undefined } @@ -137,7 +149,7 @@ const getFirstMove = ( step: firstStep.step, variables: { ...firstStep.variables, - [step.name]: firstStep.variables[step.name] ?? (step.repeat.type === 'rounds' ? 1 : playerIds[0]), + [step.name]: firstStep.variables[step.name] ?? (step.repeat.type === 'rounds' ? 1 : players[0]), }, } } @@ -152,7 +164,7 @@ const getTransition = ( steps: Step[], stepId: string, variables: PlayVariables, - playerIds: number[] + players: BaseUser[] ): undefined | { current: MoveStep; next?: MoveStep; variables: PlayVariables } => { let current: MoveStep | undefined let currentVariables = variables @@ -160,7 +172,7 @@ const getTransition = ( const step = steps[i] if (current) { - const nextStep = getFirstMove(step, currentVariables, playerIds) + const nextStep = getFirstMove(step, currentVariables, players) if (nextStep) { return { current, next: nextStep.step, variables: nextStep.variables } } @@ -172,7 +184,7 @@ const getTransition = ( } break case 'sequence': { - const result = getTransition(step.steps, stepId, currentVariables, playerIds) + const result = getTransition(step.steps, stepId, currentVariables, players) if (!result) { break } @@ -187,9 +199,9 @@ const getTransition = ( const round = currentVariables[step.name] as number nextValue = round < +step.repeat.amount ? round + 1 : undefined } else { - const playerId = currentVariables[step.name] as number - const playerIndex = playerIds.indexOf(playerId) - nextValue = playerIds[playerIndex + 1] + const player = currentVariables[step.name] as BaseUser + const playerIndex = players.findIndex(p => p.id === player.id) + nextValue = players[playerIndex + 1] } if (nextValue) { const firstStep = getFirstMove( @@ -198,7 +210,7 @@ const getTransition = ( ...result.variables, [step.name]: nextValue, }, - playerIds + players ) if (firstStep) { return { @@ -250,6 +262,10 @@ async function createChild(data: any, parent: Post): Promise { function insertVariables(text: string | undefined, variables: PlayVariables) { return text?.replace(/\[([^\]]+)\]/, (substring, variableName) => { if (variableName in variables) { + const value = variables[variableName] + if (typeof value === 'object') { + return `@${value.name}` + } return `${variables[variableName]}` } return substring @@ -288,7 +304,6 @@ async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVaria step, moveId: movePost.id, variables: variables, - playerIds: play.playerIds, } } }) @@ -306,7 +321,7 @@ async function nextMove(gamePost: Post, io: Server): Promise Date: Wed, 27 Mar 2024 05:30:47 +0100 Subject: [PATCH 14/21] resolve player variable --- GameServer.js | 25 ++++++++++++++----------- GameServer.ts | 31 ++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/GameServer.js b/GameServer.js index ed0f885..0325f0f 100644 --- a/GameServer.js +++ b/GameServer.js @@ -165,8 +165,9 @@ function createChild(data, parent) { return post; }); } +const variableRegex = /\[([^\]]+)\]/; function insertVariables(text, variables) { - return text === null || text === void 0 ? void 0 : text.replace(/\[([^\]]+)\]/, (substring, variableName) => { + return text === null || text === void 0 ? void 0 : text.replace(variableRegex, (substring, variableName) => { if (variableName in variables) { const value = variables[variableName]; if (typeof value === 'object') { @@ -177,19 +178,20 @@ function insertVariables(text, variables) { return substring; }); } +function resolveVariable(text, variables) { + const match = text === null || text === void 0 ? void 0 : text.match(variableRegex); + console.log(text, match, variables); + if (match) { + return variables[match[1]]; + } +} function startNewMove(gamePost, step, variables, io) { return __awaiter(this, void 0, void 0, function* () { const game = gamePost.game; const play = game.play; const now = +new Date(); const timeout = now + (0, parse_duration_1.default)(step.timeout); - const move = { - status: 'started', - elapsedTime: 0, - startedAt: now, - timeout, - gameId: gamePost.id - }; + const move = Object.assign(Object.assign({ status: 'started', elapsedTime: 0, startedAt: now, timeout, gameId: gamePost.id }, step.move), { player: resolveVariable(step.move.player, variables) }); const movePost = yield createChild({ type: 'post', mediaTypes: '', @@ -383,6 +385,7 @@ function registerGameServerEvents(socket, io) { emitChanges(io, changes); })); socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { + var _d; const gamePost = yield getPost(id); const play = gamePost.game.play; let skippedMovePost; @@ -396,9 +399,9 @@ function registerGameServerEvents(socket, io) { } } const changes = yield nextMove(gamePost, io); - emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: skippedMovePost && [skippedMovePost] })); + emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: skippedMovePost && [skippedMovePost, ...(_d = changes === null || changes === void 0 ? void 0 : changes.changedMoves) !== null && _d !== void 0 ? _d : []] })); })); - socket.on(EVENTS.outgoing.pause, (_d) => __awaiter(this, [_d], void 0, function* ({ id }) { + socket.on(EVENTS.outgoing.pause, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { const post = yield getPost(id); const game = post.game; const play = game.play; @@ -424,7 +427,7 @@ function registerGameServerEvents(socket, io) { } emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] }); })); - socket.on(EVENTS.outgoing.stop, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { + socket.on(EVENTS.outgoing.stop, (_f) => __awaiter(this, [_f], void 0, function* ({ id }) { const post = yield getPost(id); const game = post.game; const play = game.play; diff --git a/GameServer.ts b/GameServer.ts index ed0c048..0b44f74 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -51,6 +51,7 @@ export type Step = { title?: string text: string timeout: string + move: MoveConfig & { player: string } } | { type: 'sequence' @@ -75,7 +76,15 @@ export type BaseUser = { } -type Move = ( +type MoveConfig = ( + | { + type: 'audio' + maxDuration: number + } + | { type: 'text' } +) + +export type Move = ( | { status: 'skipped' | 'ended' | 'stopped' } | { status: 'paused'; elapsedTime: number; remainingTime: number } | { @@ -84,7 +93,7 @@ type Move = ( startedAt: number timeout: number } -) & { gameId?: number } +) & { gameId?: number; player?: BaseUser } & MoveConfig type PlayVariables = Record @@ -259,8 +268,10 @@ async function createChild(data: any, parent: Post): Promise { return post } +const variableRegex = /\[([^\]]+)\]/ + function insertVariables(text: string | undefined, variables: PlayVariables) { - return text?.replace(/\[([^\]]+)\]/, (substring, variableName) => { + return text?.replace(variableRegex, (substring, variableName) => { if (variableName in variables) { const value = variables[variableName] if (typeof value === 'object') { @@ -272,6 +283,14 @@ function insertVariables(text: string | undefined, variables: PlayVariables) { }) } +function resolveVariable(text: string | undefined, variables: PlayVariables) { + const match = text?.match(variableRegex); + console.log(text, match, variables) + if (match) { + return variables[match[1]] + } +} + type Changes = { changedGamePost: Post, changedMoves?: Post[] } async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVariables, io: Server): Promise { @@ -285,7 +304,9 @@ async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVaria elapsedTime: 0, startedAt: now, timeout, - gameId: gamePost.id + gameId: gamePost.id, + ...step.move, + player: resolveVariable(step.move.player, variables) as BaseUser } const movePost = await createChild({ type: 'post', @@ -526,7 +547,7 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { } } const changes = await nextMove(gamePost, io) - emitChanges(io, { ...changes!, changedMoves: skippedMovePost && [skippedMovePost] }) + emitChanges(io, { ...changes!, changedMoves: skippedMovePost && [skippedMovePost, ...changes?.changedMoves ?? []] }) }) socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => { From 683636ecf145df4d8c40ff0d549ade12f35c1b97 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 27 Mar 2024 11:29:02 +0100 Subject: [PATCH 15/21] submissions --- GameServer.js | 23 +++++++++++++++++++---- GameServer.ts | 19 ++++++++++++++++++- Helpers.js | 14 ++++++++++++++ models/Post.js | 1 + routes/Post.js | 18 ++++++++++++++++++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/GameServer.js b/GameServer.js index 0325f0f..7a7ab4e 100644 --- a/GameServer.js +++ b/GameServer.js @@ -281,6 +281,7 @@ const EVENTS = { stop: 'gs:outgoing-stop', skip: 'gs:outgoing-skip', pause: 'gs:outgoing-pause', + submit: 'gs:outgoing-submit' }, incoming: { updated: 'gs:incoming-updated' @@ -384,9 +385,23 @@ function registerGameServerEvents(socket, io) { } emitChanges(io, changes); })); - socket.on(EVENTS.outgoing.skip, (_c) => __awaiter(this, [_c], void 0, function* ({ id }) { + socket.on(EVENTS.outgoing.submit, (_c) => __awaiter(this, [_c], void 0, function* ({ id, moveId }) { var _d; const gamePost = yield getPost(id); + const movePost = yield getPost(moveId); + const move = movePost.move; + if (move.status !== 'started') { + return; + } + let completedMovePost = yield updatePost(movePost, { + move: Object.assign(Object.assign({}, move), { status: 'ended' }) + }); + const changes = yield nextMove(gamePost, io); + emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: [completedMovePost, ...(_d = changes === null || changes === void 0 ? void 0 : changes.changedMoves) !== null && _d !== void 0 ? _d : []] })); + })); + socket.on(EVENTS.outgoing.skip, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { + var _f; + const gamePost = yield getPost(id); const play = gamePost.game.play; let skippedMovePost; if ((play.status === 'started' || play.status === 'paused') && play.moveId) { @@ -399,9 +414,9 @@ function registerGameServerEvents(socket, io) { } } const changes = yield nextMove(gamePost, io); - emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: skippedMovePost && [skippedMovePost, ...(_d = changes === null || changes === void 0 ? void 0 : changes.changedMoves) !== null && _d !== void 0 ? _d : []] })); + emitChanges(io, Object.assign(Object.assign({}, changes), { changedMoves: skippedMovePost && [skippedMovePost, ...(_f = changes === null || changes === void 0 ? void 0 : changes.changedMoves) !== null && _f !== void 0 ? _f : []] })); })); - socket.on(EVENTS.outgoing.pause, (_e) => __awaiter(this, [_e], void 0, function* ({ id }) { + socket.on(EVENTS.outgoing.pause, (_g) => __awaiter(this, [_g], void 0, function* ({ id }) { const post = yield getPost(id); const game = post.game; const play = game.play; @@ -427,7 +442,7 @@ function registerGameServerEvents(socket, io) { } emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] }); })); - socket.on(EVENTS.outgoing.stop, (_f) => __awaiter(this, [_f], void 0, function* ({ id }) { + socket.on(EVENTS.outgoing.stop, (_h) => __awaiter(this, [_h], void 0, function* ({ id }) { const post = yield getPost(id); const game = post.game; const play = game.play; diff --git a/GameServer.ts b/GameServer.ts index 0b44f74..bdb2923 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -411,9 +411,9 @@ const EVENTS = { update: 'gs:outgoing-update', start: 'gs:outgoing-start', stop: 'gs:outgoing-stop', - skip: 'gs:outgoing-skip', pause: 'gs:outgoing-pause', + submit: 'gs:outgoing-submit' }, incoming: { updated: 'gs:incoming-updated' @@ -530,6 +530,23 @@ export async function registerGameServerEvents(socket: Socket, io: Server) { emitChanges(io, changes) }) + socket.on(EVENTS.outgoing.submit, async ({ id, moveId }) => { + const gamePost = await getPost(id); + const movePost = await getPost(moveId) + const move = movePost.move! + if (move.status !== 'started') { + return + } + let completedMovePost = await updatePost(movePost, { + move: { + ...move, + status: 'ended' + } + }) + const changes = await nextMove(gamePost, io) + emitChanges(io, { ...changes!, changedMoves: [completedMovePost, ...changes?.changedMoves ?? []] }) + }) + socket.on(EVENTS.outgoing.skip, async ({ id }: { id: number }) => { const gamePost = await getPost(id); const play = gamePost.game!.play diff --git a/Helpers.js b/Helpers.js index cab2476..353655d 100644 --- a/Helpers.js +++ b/Helpers.js @@ -2023,6 +2023,20 @@ function attachComment(comment, parent, accountId) { totalComments: 0, totalRatings: 0, }) + if (parent.relationship !== 'parent') { + await Link.create({ + creatorId: accountId, + itemAId: parent.id, + itemAType: parent.type, + itemBId: comment.id, + itemBType: comment.type, + relationship: parent.relationship, + state: 'active', + totalLikes: 0, + totalComments: 0, + totalRatings: 0, + }) + } const addRootLink = await Link.create({ creatorId: accountId, itemAId: rootPost.id, diff --git a/models/Post.js b/models/Post.js index 2050873..0c70f69 100644 --- a/models/Post.js +++ b/models/Post.js @@ -52,6 +52,7 @@ module.exports = (sequelize, DataTypes) => { Post.hasMany(models.Link, { as: 'AudioBlocks', foreignKey: 'itemAId' }) Post.hasOne(models.Link, { as: 'Originals', foreignKey: 'itemBId' }) Post.hasMany(models.Link, { as: 'Remixes', foreignKey: 'itemAId' }) + Post.hasMany(models.Link, { as: 'Submissions', foreignKey: 'itemAId' }) Post.hasOne(models.Link, { as: 'MediaLink', foreignKey: 'itemAId' }) // used for post map (todo: rethink...) Post.hasMany(models.Link, { as: 'OutgoingPostLinks', foreignKey: 'itemAId' }) diff --git a/routes/Post.js b/routes/Post.js index fb2e78c..3ab98d8 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -784,6 +784,24 @@ router.get('/post-children', async (req, res) => { model: User, as: 'Creator', attributes: ['id', 'handle', 'name', 'flagImagePath', 'coverImagePath'], + }, + { + model: Link, + as: 'Submissions', + separate: true, + where: { relationship: 'submission', state: 'active' }, + order: [['index', 'ASC']], + include: { + model: Post, + attributes: ['id', 'type', 'text'], + include: [ + { + model: User, + as: 'Creator', + attributes: ['id', 'handle', 'name', 'flagImagePath', 'coverImagePath'], + }, + ] + } } ] } From 0a01d76974d9650cdfc9ec5e422d95b8d21b4a67 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 27 Mar 2024 12:57:21 +0100 Subject: [PATCH 16/21] submission part optional --- GameServer.js | 16 ++++++++-------- GameServer.ts | 21 ++++++++------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/GameServer.js b/GameServer.js index 7a7ab4e..0160499 100644 --- a/GameServer.js +++ b/GameServer.js @@ -178,20 +178,20 @@ function insertVariables(text, variables) { return substring; }); } -function resolveVariable(text, variables) { - const match = text === null || text === void 0 ? void 0 : text.match(variableRegex); - console.log(text, match, variables); - if (match) { - return variables[match[1]]; - } -} function startNewMove(gamePost, step, variables, io) { return __awaiter(this, void 0, void 0, function* () { const game = gamePost.game; const play = game.play; const now = +new Date(); const timeout = now + (0, parse_duration_1.default)(step.timeout); - const move = Object.assign(Object.assign({ status: 'started', elapsedTime: 0, startedAt: now, timeout, gameId: gamePost.id }, step.move), { player: resolveVariable(step.move.player, variables) }); + const move = { + status: 'started', + elapsedTime: 0, + startedAt: now, + timeout, + gameId: gamePost.id, + submission: step.submission && (Object.assign(Object.assign({}, step.submission), { player: variables[step.submission.player] })) + }; const movePost = yield createChild({ type: 'post', mediaTypes: '', diff --git a/GameServer.ts b/GameServer.ts index bdb2923..c54bf88 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -51,7 +51,7 @@ export type Step = { title?: string text: string timeout: string - move: MoveConfig & { player: string } + submission?: SubmissionConfig & { player: string } } | { type: 'sequence' @@ -76,7 +76,7 @@ export type BaseUser = { } -type MoveConfig = ( +type SubmissionConfig = ( | { type: 'audio' maxDuration: number @@ -93,7 +93,7 @@ export type Move = ( startedAt: number timeout: number } -) & { gameId?: number; player?: BaseUser } & MoveConfig +) & { gameId?: number; submission?: { player?: BaseUser } & SubmissionConfig } type PlayVariables = Record @@ -283,14 +283,6 @@ function insertVariables(text: string | undefined, variables: PlayVariables) { }) } -function resolveVariable(text: string | undefined, variables: PlayVariables) { - const match = text?.match(variableRegex); - console.log(text, match, variables) - if (match) { - return variables[match[1]] - } -} - type Changes = { changedGamePost: Post, changedMoves?: Post[] } async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVariables, io: Server): Promise { @@ -299,14 +291,17 @@ async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVaria const now = +new Date(); const timeout = now + parseDuration(step.timeout)! + const move: Move = { status: 'started', elapsedTime: 0, startedAt: now, timeout, gameId: gamePost.id, - ...step.move, - player: resolveVariable(step.move.player, variables) as BaseUser + submission: step.submission && ({ + ...step.submission, + player: variables[step.submission.player] as BaseUser + }) } const movePost = await createChild({ type: 'post', From ebbd19d25c186e8badc861b473d0d1d68a396c78 Mon Sep 17 00:00:00 2001 From: Nicola Marcacci Rossi Date: Wed, 27 Mar 2024 18:30:45 +0100 Subject: [PATCH 17/21] audio moves --- GameServer.js | 14 +-- GameServer.ts | 42 +++++--- routes/Post.js | 277 ++++++++++++++++++++++++++++--------------------- 3 files changed, 189 insertions(+), 144 deletions(-) diff --git a/GameServer.js b/GameServer.js index 0160499..9ebb7a9 100644 --- a/GameServer.js +++ b/GameServer.js @@ -180,18 +180,14 @@ function insertVariables(text, variables) { } function startNewMove(gamePost, step, variables, io) { return __awaiter(this, void 0, void 0, function* () { + var _a, _b; const game = gamePost.game; const play = game.play; const now = +new Date(); - const timeout = now + (0, parse_duration_1.default)(step.timeout); - const move = { - status: 'started', - elapsedTime: 0, - startedAt: now, - timeout, - gameId: gamePost.id, - submission: step.submission && (Object.assign(Object.assign({}, step.submission), { player: variables[step.submission.player] })) - }; + const timeout = now + ((_a = (0, parse_duration_1.default)(step.timeout)) !== null && _a !== void 0 ? _a : 0); + const move = Object.assign({ status: 'started', elapsedTime: 0, startedAt: now, timeout, gameId: gamePost.id }, step.submission && ({ + submission: Object.assign(Object.assign({}, step.submission.type === 'audio' ? Object.assign(Object.assign({}, step.submission), { maxDuration: (_b = (0, parse_duration_1.default)(step.submission.maxDuration)) !== null && _b !== void 0 ? _b : 0 }) : Object.assign({}, step.submission)), { player: variables[step.submission.player] }) + })); const movePost = yield createChild({ type: 'post', mediaTypes: '', diff --git a/GameServer.ts b/GameServer.ts index c54bf88..9f0de02 100644 --- a/GameServer.ts +++ b/GameServer.ts @@ -51,7 +51,11 @@ export type Step = { title?: string text: string timeout: string - submission?: SubmissionConfig & { player: string } + submission?: { player: string } & ({ + type: 'audio' + maxDuration: string + } + | { type: 'text' }) } | { type: 'sequence' @@ -75,17 +79,8 @@ export type BaseUser = { flagImagePath: string } - -type SubmissionConfig = ( - | { - type: 'audio' - maxDuration: number - } - | { type: 'text' } -) - export type Move = ( - | { status: 'skipped' | 'ended' | 'stopped' } + | { status: 'skipped' | 'ended' | 'stopped' | 'timeout' } | { status: 'paused'; elapsedTime: number; remainingTime: number } | { status: 'started' @@ -93,7 +88,15 @@ export type Move = ( startedAt: number timeout: number } -) & { gameId?: number; submission?: { player?: BaseUser } & SubmissionConfig } +) & { gameId?: number; submission?: MoveSubmission } +export type MoveSubmission = { player?: BaseUser } & ({ + type: 'audio' + maxDuration: number +} + | { type: 'text' }) +export type MoveSubmissionAudio = Extract + +export type MoveStatus = Move['status'] type PlayVariables = Record @@ -289,7 +292,7 @@ async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVaria const game = gamePost.game! const play = game.play const now = +new Date(); - const timeout = now + parseDuration(step.timeout)! + const timeout = now + (parseDuration(step.timeout) ?? 0) const move: Move = { @@ -298,9 +301,16 @@ async function startNewMove(gamePost: Post, step: MoveStep, variables: PlayVaria startedAt: now, timeout, gameId: gamePost.id, - submission: step.submission && ({ - ...step.submission, - player: variables[step.submission.player] as BaseUser + ...step.submission && ({ + submission: { + ...step.submission.type === 'audio' ? { + ...step.submission, + maxDuration: parseDuration(step.submission.maxDuration) ?? 0 + } : { + ...step.submission + }, + player: variables[step.submission.player] as BaseUser, + } }) } const movePost = await createChild({ diff --git a/routes/Post.js b/routes/Post.js index 3ab98d8..68752f1 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -800,6 +800,28 @@ router.get('/post-children', async (req, res) => { as: 'Creator', attributes: ['id', 'handle', 'name', 'flagImagePath', 'coverImagePath'], }, + { + model: Link, + as: 'AudioBlocks', + separate: true, + where: { itemBType: 'audio-block' }, + attributes: ['index'], + order: [['index', 'ASC']], + include: { + model: Post, + attributes: ['id', 'text'], + include: { + model: Link, + as: 'MediaLink', + where: { state: 'active', relationship: 'parent' }, + attributes: ['id'], + include: { + model: Audio, + attributes: ['url'], + }, + }, + }, + }, ] } } @@ -1645,107 +1667,111 @@ router.post('/create-poll-answer', authenticateToken, async (req, res) => { }) router.post('/create-bead', authenticateToken, async (req, res) => { - const accountId = req.user ? req.user.id : null - if (!accountId) res.status(401).json({ message: 'Unauthorized' }) - else { - const { postData, files } = await uploadFiles(req, res, accountId) - const { post: newBead } = await createPost(postData, files, accountId) - const { parent } = postData.links + try { + const accountId = req.user ? req.user.id : null + if (!accountId) res.status(401).json({ message: 'Unauthorized' }) + else { + const { postData, files } = await uploadFiles(req, res, accountId) + const { post: newBead } = await createPost(postData, files, accountId) + const { parent } = postData.links - const creator = await User.findOne({ - where: { id: accountId }, - attributes: ['name', 'handle'], - }) + const creator = await User.findOne({ + where: { id: accountId }, + attributes: ['name', 'handle'], + }) - const gamePost = await Post.findOne({ - where: { id: parent.id }, - include: [ - { - model: User, - as: 'Creator', - attributes: ['id', 'name', 'handle', 'email', 'emailsDisabled'], - }, - { model: GlassBeadGame }, - { - model: User, - as: 'Players', - attributes: ['id', 'name', 'handle', 'email', 'emailsDisabled'], - through: { where: { type: 'glass-bead-game' }, attributes: ['index'] }, - }, - { - model: Post, - as: 'Beads', - required: false, - through: { where: { state: 'active' }, attributes: ['index'] }, - include: { + const gamePost = await Post.findOne({ + where: { id: parent.id }, + include: [ + { model: User, as: 'Creator', attributes: ['id', 'name', 'handle', 'email', 'emailsDisabled'], }, - }, - ], - }) + { model: GlassBeadGame }, + { + model: User, + as: 'Players', + attributes: ['id', 'name', 'handle', 'email', 'emailsDisabled'], + through: { where: { type: 'glass-bead-game' }, attributes: ['index'] }, + }, + { + model: Post, + as: 'Beads', + required: false, + through: { where: { state: 'active' }, attributes: ['index'] }, + include: { + model: User, + as: 'Creator', + attributes: ['id', 'name', 'handle', 'email', 'emailsDisabled'], + }, + }, + ], + }) - const createLink = await Link.create({ - creatorId: accountId, - itemAId: parent.id, - itemAType: 'post', - itemBId: newBead.id, - itemBType: 'bead', - index: gamePost.GlassBeadGame.totalBeads, - relationship: 'parent', - state: 'active', - totalLikes: 0, - totalComments: 0, - totalRatings: 0, - }) + let newDeadline = 0; - const { synchronous, multiplayer, moveTimeWindow } = gamePost.GlassBeadGame - const notifyPlayers = - !synchronous && multiplayer - ? await new Promise(async (resolve) => { - // find other players to notify - const otherPlayers = [] - if (gamePost.Players.length) { - // if restricted game, use linked Players - otherPlayers.push(...gamePost.Players.filter((p) => p.id !== accountId)) - } else { - // if open game, use linked Bead Creators - gamePost.Beads.forEach((bead) => { - // filter out game creator and existing records - if ( - bead.Creator.id !== accountId && - !otherPlayers.find((p) => p.id === bead.Creator.id) - ) - otherPlayers.push(bead.Creator) - }) - } - // notify players - const sendNotifications = await Promise.all( - otherPlayers.map( - (p) => - new Promise(async (resolve2) => { - const notifyPlayer = await Notification.create({ - type: 'gbg-move-from-other-player', - ownerId: p.id, - postId: parent.id, - userId: accountId, - seen: false, - }) - const emailPlayer = p.emailsDisabled - ? null - : await sgMail.send({ - to: p.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + if (gamePost.GlassBeadGame) { + await Link.create({ + creatorId: accountId, + itemAId: parent.id, + itemAType: 'post', + itemBId: newBead.id, + itemBType: 'bead', + index: gamePost.GlassBeadGame.totalBeads, + relationship: 'parent', + state: 'active', + totalLikes: 0, + totalComments: 0, + totalRatings: 0, + }) + + + const { synchronous, multiplayer, moveTimeWindow } = gamePost.GlassBeadGame + if (!synchronous && multiplayer) { + await new Promise(async (resolve) => { + // find other players to notify + const otherPlayers = [] + if (gamePost.Players.length) { + // if restricted game, use linked Players + otherPlayers.push(...gamePost.Players.filter((p) => p.id !== accountId)) + } else { + // if open game, use linked Bead Creators + gamePost.Beads.forEach((bead) => { + // filter out game creator and existing records + if ( + bead.Creator.id !== accountId && + !otherPlayers.find((p) => p.id === bead.Creator.id) + ) + otherPlayers.push(bead.Creator) + }) + } + // notify players + const sendNotifications = await Promise.all( + otherPlayers.map( + (p) => + new Promise(async (resolve2) => { + const notifyPlayer = await Notification.create({ + type: 'gbg-move-from-other-player', + ownerId: p.id, + postId: parent.id, + userId: accountId, + seen: false, + }) + const emailPlayer = p.emailsDisabled + ? null + : await sgMail.send({ + to: p.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${p.name}, ${creator.name} just added a new bead. https://${appURL}/p/${parent.id} `, - html: ` + html: `

Hi ${p.name},
@@ -1754,40 +1780,53 @@ router.post('/create-bead', authenticateToken, async (req, res) => { bead.

`, - }) - Promise.all([notifyPlayer, emailPlayer]) - .then(() => resolve2()) - .catch((error) => resolve2(error)) - }) - ) - ) - // schedule next deadline - const scheduleNewDeadline = moveTimeWindow - ? await scheduleNextBeadDeadline( - parent.id, - gamePost.GlassBeadGame, - gamePost.Players + }) + Promise.all([notifyPlayer, emailPlayer]) + .then(() => resolve2()) + .catch((error) => resolve2(error)) + }) + ) ) - : null + // schedule next deadline + if (moveTimeWindow) { + newDeadline = await scheduleNextBeadDeadline( + parent.id, + gamePost.GlassBeadGame, + gamePost.Players + ) + } + }) + } - Promise.all([sendNotifications, scheduleNewDeadline]) - .then((data) => resolve(data[1])) - .catch((error) => resolve(error)) + await GlassBeadGame.increment('totalBeads', { + where: { postId: parent.id }, }) - : null - - const incrementTotalBeads = await GlassBeadGame.increment('totalBeads', { - where: { postId: parent.id }, - }) + } else { + await Link.create({ + creatorId: accountId, + itemAId: parent.id, + itemAType: 'post', + itemBId: newBead.id, + itemBType: 'bead', + index: 1, + relationship: 'submission', + state: 'active', + totalLikes: 0, + totalComments: 0, + totalRatings: 0, + }) + } - const updateLastPostActivity = await Post.update( - { lastActivity: new Date() }, - { where: { id: parent.id }, silent: true } - ) + await Post.update( + { lastActivity: new Date() }, + { where: { id: parent.id }, silent: true } + ) - Promise.all([createLink, notifyPlayers, incrementTotalBeads, updateLastPostActivity]) - .then((data) => res.status(200).json({ newBead, newDeadline: data[1] })) - .catch((error) => res.status(500).json({ message: 'Error', error })) + res.status(200).json({ newBead, newDeadline }) + } + } catch (error) { + console.error(error) + res.status(500).json({ message: 'Error', error }) } }) From 4771341a76a8d4074d7f034f63d2c39a6c990d15 Mon Sep 17 00:00:00 2001 From: jhweir Date: Sun, 21 Apr 2024 15:44:46 +0100 Subject: [PATCH 18/21] Indentation updates --- Helpers.js | 358 ++++++++++---------- ScheduledTasks.js | 34 +- routes/Post.js | 837 +++++++++++++++++++++++----------------------- 3 files changed, 623 insertions(+), 606 deletions(-) diff --git a/Helpers.js b/Helpers.js index 353655d..e87ad47 100644 --- a/Helpers.js +++ b/Helpers.js @@ -302,14 +302,14 @@ function notifyMention(creator, user, postId, type) { const sendEmail = skipEmail ? null : await sgMail.send({ - to: user.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: user.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${user.name}, ${creator.name} just mentioned you in a ${type} on weco: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${user.name},
@@ -319,7 +319,7 @@ function notifyMention(creator, user, postId, type) { on weco

`, - }) + }) Promise.all([sendNotification, sendEmail]) .then(() => resolve()) @@ -863,7 +863,7 @@ function totalSpaceResults(filters) { const endDate = createSQLDate(new Date()) return depth === 'Deep' ? [ - literal(`( + literal(`( SELECT COUNT(*) FROM Spaces s WHERE s.id != Space.id @@ -881,10 +881,10 @@ function totalSpaceResults(filters) { OR s.description LIKE '%${search}%' ) AND s.createdAt BETWEEN '${startDate}' AND '${endDate}' )`), - 'totalResults', - ] + 'totalResults', + ] : [ - literal(`( + literal(`( SELECT COUNT(*) FROM Spaces s WHERE s.state = 'active' @@ -900,8 +900,8 @@ function totalSpaceResults(filters) { OR s.description LIKE '%${search}%' ) AND s.createdAt BETWEEN '${startDate}' AND '${endDate}' )`), - 'totalResults', - ] + 'totalResults', + ] } } @@ -967,7 +967,7 @@ const fullPostAttributes = [ 'totalRatings', 'totalLinks', 'game', - 'move' + 'move', ] // todo: replace all use cases with const fullPostAttributes above @@ -1155,8 +1155,8 @@ function findPostInclude(accountId) { include: { model: Post, as: 'Parent', - attributes: ['id', 'title', 'game', 'state'] - } + attributes: ['id', 'title', 'game', 'state'], + }, }, { model: Link, @@ -1166,8 +1166,8 @@ function findPostInclude(accountId) { order: [['index', 'ASC']], include: { model: Post, - attributes: ['id', 'title', 'game', 'state'] - } + attributes: ['id', 'title', 'game', 'state'], + }, }, { model: Reaction, @@ -1632,14 +1632,14 @@ function sendGBGInvite(player, postId, creator, settings) { const sendEmail = player.emailsDisabled ? null : await sgMail.send({ - to: player.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: player.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${player.name}, ${creator.name} just invited you to join a game on weco: https://${appURL}/p/${postId} Log in and go to your notifications to accept or reject the invitation. `, - html: ` + html: `

Hi ${player.name},
@@ -1657,17 +1657,19 @@ function sendGBGInvite(player, postId, creator, settings) {
Allowed bead types: ${allowedBeadTypes.join(',')}
- Time window for moves: ${moveTimeWindow ? `${moveTimeWindow} minutes` : 'Off' - } + Time window for moves: ${ + moveTimeWindow ? `${moveTimeWindow} minutes` : 'Off' + }
- Character limit: ${characterLimit ? `${characterLimit} characters` : 'Off' - } + Character limit: ${ + characterLimit ? `${characterLimit} characters` : 'Off' + }
Audio time limit: ${moveDuration ? `${moveDuration} seconds` : 'Off'}

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) @@ -1721,7 +1723,7 @@ async function addRemixes(accountId, game, postId) { } } findOriginals(game.steps) - originals = uniq(originals); + originals = uniq(originals) const links = await Link.findAll({ attributes: ['itemAId'], where: { @@ -1730,11 +1732,11 @@ async function addRemixes(accountId, game, postId) { itemBType: 'post', itemAId: originals, itemBId: postId, - relationship: 'remix' - } + relationship: 'remix', + }, }) for (const originalId of originals) { - if (links?.some(link => link.itemAId === originalId)) { + if (links?.some((link) => link.itemAId === originalId)) { continue } await Link.create({ @@ -1747,7 +1749,7 @@ async function addRemixes(accountId, game, postId) { relationship: 'remix', totalLikes: 0, totalComments: 0, - totalRatings: 0 + totalRatings: 0, }) } } @@ -1803,14 +1805,14 @@ function createPost(data, files, accountId) { // todo: add the correct notification type const notifyMentions = mentions?.length ? await new Promise(async (resolve) => { - const users = await User.findAll({ - where: { id: mentions, state: 'active' }, - attributes: ['id', 'name', 'email', 'emailsDisabled'], - }) - Promise.all(users.map((user) => notifyMention(creator, user, post.id, type))) - .then(() => resolve()) - .catch((error) => resolve(data, error)) - }) + const users = await User.findAll({ + where: { id: mentions, state: 'active' }, + attributes: ['id', 'name', 'email', 'emailsDisabled'], + }) + Promise.all(users.map((user) => notifyMention(creator, user, post.id, type))) + .then(() => resolve()) + .catch((error) => resolve(data, error)) + }) : null const createUrls = urls @@ -1819,158 +1821,158 @@ function createPost(data, files, accountId) { const createImages = images ? await Promise.all( - images.map((image, i) => createImage(accountId, post.id, type, image, i, files)) - ) + images.map((image, i) => createImage(accountId, post.id, type, image, i, files)) + ) : null const createAudios = audios ? await Promise.all( - audios.map((audio, i) => createAudio(accountId, post.id, type, audio, i, files)) - ) + audios.map((audio, i) => createAudio(accountId, post.id, type, audio, i, files)) + ) : null const createEvent = event ? await Event.create({ - postId: post.id, - state: 'active', - startTime: event.startTime, - endTime: event.endTime, - }) + postId: post.id, + state: 'active', + startTime: event.startTime, + endTime: event.endTime, + }) : null const createPoll = poll ? await new Promise(async (resolve) => { - const { type, answers, locked, governance, action, threshold } = poll - const createPoll = await Poll.create({ - postId: post.id, - type, - answersLocked: locked, - spaceId: governance ? spaceIds[0] : null, - action: action || null, - threshold: threshold || null, - }) - const creatAnswers = await Promise.all( - answers.map((a) => createPollAnswer(a, accountId, post.id, files)) - ) - Promise.all([createPoll, creatAnswers]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const { type, answers, locked, governance, action, threshold } = poll + const createPoll = await Poll.create({ + postId: post.id, + type, + answersLocked: locked, + spaceId: governance ? spaceIds[0] : null, + action: action || null, + threshold: threshold || null, + }) + const creatAnswers = await Promise.all( + answers.map((a) => createPollAnswer(a, accountId, post.id, files)) + ) + Promise.all([createPoll, creatAnswers]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null const createGBG = glassBeadGame ? await new Promise(async (resolve) => { - const { settings, topicImage, topicGroup, beads, sourcePostId } = glassBeadGame - const imageFile = files.find((file) => file.originalname === topicImage.id) - const { players } = settings - const createGame = await GlassBeadGame.create({ - postId: post.id, - state: 'active', - locked: false, - topicGroup, - topicImage: imageFile ? imageFile.url : topicImage.Image.url || null, - synchronous: settings.synchronous, - multiplayer: settings.multiplayer, - allowedBeadTypes: settings.allowedBeadTypes.join(',').toLowerCase(), - playerOrder: players.length ? players.map((p) => p.id).join(',') : null, - totalMoves: settings.totalMoves || null, - movesPerPlayer: settings.movesPerPlayer || null, - moveDuration: settings.moveDuration || null, - moveTimeWindow: settings.moveTimeWindow || null, - characterLimit: settings.characterLimit || null, - introDuration: settings.introDuration || null, - outroDuration: settings.outroDuration || null, - intervalDuration: settings.intervalDuration || null, - nextMoveDeadline: settings.nextMoveDeadline || null, - totalBeads: beads.length + (sourcePostId ? 1 : 0), - }) - - // const linkSourceBead = sourcePostId - // ? await Link.create({ - // state: 'active', - // // type: 'gbg-post', - // index: 0, - // relationship: 'source', - // creatorId: accountId, - // itemAId: post.id, - // itemBId: sourcePostId, - // totalLikes: 0, - // totalComments: 0, - // totalRatings: 0, - // }) - // : null - - // const notifySourceCreator = - // sourcePostId && sourceCreatorId !== accountId - // ? await new Promise(async (Resolve) => { - // const sourceCreator = await User.findOne({ - // where: { id: sourceCreatorId }, - // attributes: ['name', 'email', 'emailsDisabled'], - // }) - // const notifyCreator = await Notification.create({ - // type: 'new-gbg-from-your-post', - // ownerId: sourceCreatorId, - // userId: accountId, - // postId: post.id, - // seen: false, - // }) - // const skipEmail = - // sourceCreator.emailsDisabled || - // (await accountMuted(accountId, sourceCreator)) - // const emailCreator = skipEmail - // ? null - // : await sgMail.send({ - // to: sourceCreator.email, - // from: { - // email: 'admin@weco.io', - // name: 'we { collective }', - // }, - // subject: 'New notification', - // text: ` - // Hi ${sourceCreator.name}, ${creatorName} just created a new glass bead game from your post on weco: https://${appURL}/p/${post.id} - // `, - // html: ` - //

- // Hi ${sourceCreator.name}, - //
- // ${creatorName} - // just created a new glass bead game from your post on weco. - //

- // `, - // }) - // Promise.all([notifyCreator, emailCreator]) - // .then(() => Resolve()) - // .catch((error) => Resolve(error)) - // }) - // : null - - const createBeads = await Promise.all( - beads.map((bead, index) => createBead(bead, index, accountId, post.id, files)) - ) - - const addPlayers = - settings.multiplayer && !!players.length - ? await addGBGPlayers(post.id, creator, settings) - : null - - Promise.all([ - createGame, - // linkSourceBead, - // notifySourceCreator, - createBeads, - addPlayers, - ]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const { settings, topicImage, topicGroup, beads, sourcePostId } = glassBeadGame + const imageFile = files.find((file) => file.originalname === topicImage.id) + const { players } = settings + const createGame = await GlassBeadGame.create({ + postId: post.id, + state: 'active', + locked: false, + topicGroup, + topicImage: imageFile ? imageFile.url : topicImage.Image.url || null, + synchronous: settings.synchronous, + multiplayer: settings.multiplayer, + allowedBeadTypes: settings.allowedBeadTypes.join(',').toLowerCase(), + playerOrder: players.length ? players.map((p) => p.id).join(',') : null, + totalMoves: settings.totalMoves || null, + movesPerPlayer: settings.movesPerPlayer || null, + moveDuration: settings.moveDuration || null, + moveTimeWindow: settings.moveTimeWindow || null, + characterLimit: settings.characterLimit || null, + introDuration: settings.introDuration || null, + outroDuration: settings.outroDuration || null, + intervalDuration: settings.intervalDuration || null, + nextMoveDeadline: settings.nextMoveDeadline || null, + totalBeads: beads.length + (sourcePostId ? 1 : 0), + }) + + // const linkSourceBead = sourcePostId + // ? await Link.create({ + // state: 'active', + // // type: 'gbg-post', + // index: 0, + // relationship: 'source', + // creatorId: accountId, + // itemAId: post.id, + // itemBId: sourcePostId, + // totalLikes: 0, + // totalComments: 0, + // totalRatings: 0, + // }) + // : null + + // const notifySourceCreator = + // sourcePostId && sourceCreatorId !== accountId + // ? await new Promise(async (Resolve) => { + // const sourceCreator = await User.findOne({ + // where: { id: sourceCreatorId }, + // attributes: ['name', 'email', 'emailsDisabled'], + // }) + // const notifyCreator = await Notification.create({ + // type: 'new-gbg-from-your-post', + // ownerId: sourceCreatorId, + // userId: accountId, + // postId: post.id, + // seen: false, + // }) + // const skipEmail = + // sourceCreator.emailsDisabled || + // (await accountMuted(accountId, sourceCreator)) + // const emailCreator = skipEmail + // ? null + // : await sgMail.send({ + // to: sourceCreator.email, + // from: { + // email: 'admin@weco.io', + // name: 'we { collective }', + // }, + // subject: 'New notification', + // text: ` + // Hi ${sourceCreator.name}, ${creatorName} just created a new glass bead game from your post on weco: https://${appURL}/p/${post.id} + // `, + // html: ` + //

+ // Hi ${sourceCreator.name}, + //
+ // ${creatorName} + // just created a new glass bead game from your post on weco. + //

+ // `, + // }) + // Promise.all([notifyCreator, emailCreator]) + // .then(() => Resolve()) + // .catch((error) => Resolve(error)) + // }) + // : null + + const createBeads = await Promise.all( + beads.map((bead, index) => createBead(bead, index, accountId, post.id, files)) + ) + + const addPlayers = + settings.multiplayer && !!players.length + ? await addGBGPlayers(post.id, creator, settings) + : null + + Promise.all([ + createGame, + // linkSourceBead, + // notifySourceCreator, + createBeads, + addPlayers, + ]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null const createCard = card ? await Promise.all( - [card.front, card.back].map((cardFace, index) => - createCardFace(cardFace, index, accountId, post.id, files) - ) - ) + [card.front, card.back].map((cardFace, index) => + createCardFace(cardFace, index, accountId, post.id, files) + ) + ) : null Promise.all([ @@ -2140,14 +2142,14 @@ function scheduleNextBeadDeadline(postId, settings, players) { const sendMoveEmail = nextPlayer.emailsDisabled ? null : await sgMail.send({ - to: nextPlayer.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: nextPlayer.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${nextPlayer.name}, it's your move! Add a new bead to the glass bead game: https://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${nextPlayer.name},
@@ -2156,7 +2158,7 @@ function scheduleNextBeadDeadline(postId, settings, players) { Add a new bead to the glass bead game.

`, - }) + }) const scheduleReminders = await scheduleGBGMoveJobs( postId, nextPlayer, diff --git a/ScheduledTasks.js b/ScheduledTasks.js index a8aa667..04f3135 100644 --- a/ScheduledTasks.js +++ b/ScheduledTasks.js @@ -189,33 +189,37 @@ async function scheduleGBGMoveJobs(postId, player, moveNumber, deadline) { const sendEmail = p.emailsDisabled ? null : await sgMail.send({ - to: p.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` - Hi ${p.name}, ${you ? 'You' : player.name - } failed to make ${you ? 'your' : 'their' - } move in time on this glass bead game: + to: p.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` + Hi ${p.name}, ${ + you ? 'You' : player.name + } failed to make ${ + you ? 'your' : 'their' + } move in time on this glass bead game: http://${config.appURL}/p/${postId} The game has now ended! `, - html: ` + html: `

Hi ${p.name},

- ${you ? 'You' : player.name} failed to make ${you ? 'your' : 'their' - } move in time on this glass bead game. + ${you ? 'You' : player.name} failed to make ${ + you ? 'your' : 'their' + } move in time on this glass bead game.

The game has now ended!

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) diff --git a/routes/Post.js b/routes/Post.js index 68752f1..653b5cb 100644 --- a/routes/Post.js +++ b/routes/Post.js @@ -376,7 +376,10 @@ router.get('/search', authenticateToken, async (req, res) => { if (userId) where.creatorId = userId if (mediaType) { if (mediaType === 'game') { - where[Op.and] = [{ mediaTypes: { [Op.like]: `%${mediaType}%` } }, { [Op.not]: { mediaTypes: { [Op.like]: `%glass-bead-game%` } } }] + where[Op.and] = [ + { mediaTypes: { [Op.like]: `%${mediaType}%` } }, + { [Op.not]: { mediaTypes: { [Op.like]: `%glass-bead-game%` } } }, + ] } } else { where.mediaTypes = { [Op.like]: `%${mediaType}%` } @@ -386,7 +389,9 @@ router.get('/search', authenticateToken, async (req, res) => { where, limit: 10, // TODO: no idea why this fails - include: findPostInclude(accountId).filter(include => !['UrlBlocks', 'ImageBlocks', 'AudioBlocks'].includes(include.as)), + include: findPostInclude(accountId).filter( + (include) => !['UrlBlocks', 'ImageBlocks', 'AudioBlocks'].includes(include.as) + ), }) res.status(200).json(matchingPosts) }) @@ -770,7 +775,7 @@ router.get('/post-comments', async (req, res) => { router.get('/post-children', async (req, res) => { const accountId = req.user ? req.user.id : null - const { postId, limit, offset, childrenIds } = req.query; + const { postId, limit, offset, childrenIds } = req.query const query = { order: [['createdAt', 'DESC']], @@ -798,7 +803,13 @@ router.get('/post-children', async (req, res) => { { model: User, as: 'Creator', - attributes: ['id', 'handle', 'name', 'flagImagePath', 'coverImagePath'], + attributes: [ + 'id', + 'handle', + 'name', + 'flagImagePath', + 'coverImagePath', + ], }, { model: Link, @@ -822,29 +833,29 @@ router.get('/post-children', async (req, res) => { }, }, }, - ] - } - } - ] - } + ], + }, + }, + ], + }, ], where: { state: 'active', relationship: 'parent', itemAType: 'post', itemAId: postId, - } + }, } if (childrenIds) { query.where.itemBId = childrenIds.split(',') } else { - query.offset = +offset; - query.limit = +limit; + query.offset = +offset + query.limit = +limit } const links = await Link.findAll(query) - res.status(200).json({ children: links.map(link => link.Post) }) + res.status(200).json({ children: links.map((link) => link.Post) }) }) router.get('/post-indirect-spaces', async (req, res) => { @@ -1356,15 +1367,15 @@ router.get('/card-faces', async (req, res) => { }) const linkToImage = linkToImageBlock ? await Link.findOne({ - where: { - itemAType: 'image-block', - itemAId: linkToImageBlock.itemBId, - itemBType: 'image', - state: 'active', - }, - attributes: [], - include: { model: Image, attributes: ['url'] }, - }) + where: { + itemAType: 'image-block', + itemAId: linkToImageBlock.itemBId, + itemBType: 'image', + state: 'active', + }, + attributes: [], + include: { model: Image, attributes: ['url'] }, + }) : null blocks.push({ ...link.Post.dataValues, @@ -1393,71 +1404,71 @@ router.post('/create-post', authenticateToken, async (req, res) => { // add spaces and increment space stats const addSpaces = spaceIds ? await new Promise(async (resolve) => { - const addDirectSpaces = await Promise.all( - spaceIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'direct') - ) - ) - // gather direct spaces ancestor ids - const spaces = await Space.findAll({ - where: { id: spaceIds, state: 'active' }, - attributes: ['id'], - include: { - model: Space, - as: 'SpaceAncestors', - attributes: ['id'], - through: { where: { state: 'open' }, attributes: [] }, - }, - }) - let ancestorIds = [] - spaces.forEach((space) => - ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) - ) - // remove duplicates and direct spaces - ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) - // store ancestor ids for response - allSpaceIds.push(...ancestorIds) - const addIndirectSpaces = await Promise.all( - ancestorIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') - ) - ) - // increment space stats - const incrementSpaceStats = await Space.increment('totalPosts', { - where: { id: allSpaceIds }, - silent: true, - }) - Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const addDirectSpaces = await Promise.all( + spaceIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'direct') + ) + ) + // gather direct spaces ancestor ids + const spaces = await Space.findAll({ + where: { id: spaceIds, state: 'active' }, + attributes: ['id'], + include: { + model: Space, + as: 'SpaceAncestors', + attributes: ['id'], + through: { where: { state: 'open' }, attributes: [] }, + }, + }) + let ancestorIds = [] + spaces.forEach((space) => + ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) + ) + // remove duplicates and direct spaces + ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) + // store ancestor ids for response + allSpaceIds.push(...ancestorIds) + const addIndirectSpaces = await Promise.all( + ancestorIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') + ) + ) + // increment space stats + const incrementSpaceStats = await Space.increment('totalPosts', { + where: { id: allSpaceIds }, + silent: true, + }) + Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null // todo: notify source creator const addLink = source ? await new Promise(async (resolve) => { - const createNewLink = await Link.create({ - state: 'active', - creatorId: accountId, - relationship: source.relationship ?? 'link', - itemAType: source.type, - itemBType: 'post', - itemAId: source.id, - itemBId: post.id, - description: source.linkDescription, - totalLikes: 0, - totalComments: 0, - totalRatings: 0, - }) - const updateSourceLinks = await Post.increment('totalLinks', { - where: { id: source.id }, - silent: true, - }) - const updateTargetLinks = await post.update({ totalLinks: 1 }, { silent: true }) - Promise.all([createNewLink, updateSourceLinks, updateTargetLinks]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const createNewLink = await Link.create({ + state: 'active', + creatorId: accountId, + relationship: source.relationship ?? 'link', + itemAType: source.type, + itemBType: 'post', + itemAId: source.id, + itemBId: post.id, + description: source.linkDescription, + totalLikes: 0, + totalComments: 0, + totalRatings: 0, + }) + const updateSourceLinks = await Post.increment('totalLinks', { + where: { id: source.id }, + silent: true, + }) + const updateTargetLinks = await post.update({ totalLinks: 1 }, { silent: true }) + Promise.all([createNewLink, updateSourceLinks, updateTargetLinks]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null Promise.all([addSpaces, addLink]) @@ -1493,14 +1504,14 @@ router.post('/create-comment', authenticateToken, async (req, res) => { const createNotification = isOwnPost ? null : await Notification.create({ - ownerId: parentPost.Creator.id, - type: parentPost.type === 'comment' ? 'comment-reply' : 'post-comment', - seen: false, - spaceAId: postData.originSpaceId, - userId: accountId, - postId: parent.id, - commentId: post.id, - }) + ownerId: parentPost.Creator.id, + type: parentPost.type === 'comment' ? 'comment-reply' : 'post-comment', + seen: false, + spaceAId: postData.originSpaceId, + userId: accountId, + postId: parent.id, + commentId: post.id, + }) const muted = await accountMuted(accountId, parentPost.Creator) const skipEmail = isOwnPost || muted || parentPost.Creator.emailsDisabled const messageText = @@ -1508,14 +1519,14 @@ router.post('/create-comment', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: parentPost.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: parentPost.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${parentPost.Creator.name}, ${account.name} just ${messageText} ${parentPost.type} on weco: http://${appURL}/p/${post.id} `, - html: ` + html: `

Hi ${parentPost.Creator.name},
@@ -1525,7 +1536,7 @@ router.post('/create-comment', authenticateToken, async (req, res) => { on weco

`, - }) + }) if (parentPost.game) { const io = req.app.get('socketio') @@ -1563,44 +1574,44 @@ router.post('/create-chat-message', authenticateToken, async (req, res) => { // add spaces and increment space stats const addSpaces = spaceIds ? await new Promise(async (resolve) => { - const addDirectSpaces = await Promise.all( - spaceIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'direct') - ) - ) - // gather direct spaces ancestor ids - const spaces = await Space.findAll({ - where: { id: spaceIds, state: 'active' }, - attributes: ['id'], - include: { - model: Space, - as: 'SpaceAncestors', - attributes: ['id'], - through: { where: { state: 'open' }, attributes: [] }, - }, - }) - let ancestorIds = [] - spaces.forEach((space) => - ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) - ) - // remove duplicates and direct spaces - ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) - // store ancestor ids for response - allSpaceIds.push(...ancestorIds) - const addIndirectSpaces = await Promise.all( - ancestorIds.map((spaceId) => - createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') - ) - ) - // increment space stats - const incrementSpaceStats = await Space.increment('totalPosts', { - where: { id: allSpaceIds }, - silent: true, - }) - Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const addDirectSpaces = await Promise.all( + spaceIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'direct') + ) + ) + // gather direct spaces ancestor ids + const spaces = await Space.findAll({ + where: { id: spaceIds, state: 'active' }, + attributes: ['id'], + include: { + model: Space, + as: 'SpaceAncestors', + attributes: ['id'], + through: { where: { state: 'open' }, attributes: [] }, + }, + }) + let ancestorIds = [] + spaces.forEach((space) => + ancestorIds.push(...space.SpaceAncestors.map((space) => space.id)) + ) + // remove duplicates and direct spaces + ancestorIds = [...new Set(ancestorIds)].filter((id) => !spaceIds.includes(id)) + // store ancestor ids for response + allSpaceIds.push(...ancestorIds) + const addIndirectSpaces = await Promise.all( + ancestorIds.map((spaceId) => + createSpacePost(accountId, spaceId, post.id, 'post', 'indirect') + ) + ) + // increment space stats + const incrementSpaceStats = await Space.increment('totalPosts', { + where: { id: allSpaceIds }, + silent: true, + }) + Promise.all([addDirectSpaces, addIndirectSpaces, incrementSpaceStats]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) : null // attach parent comment const linkComment = parent ? await attachComment(post, parent, accountId) : null @@ -1709,7 +1720,7 @@ router.post('/create-bead', authenticateToken, async (req, res) => { ], }) - let newDeadline = 0; + let newDeadline = 0 if (gamePost.GlassBeadGame) { await Link.create({ @@ -1726,7 +1737,6 @@ router.post('/create-bead', authenticateToken, async (req, res) => { totalRatings: 0, }) - const { synchronous, multiplayer, moveTimeWindow } = gamePost.GlassBeadGame if (!synchronous && multiplayer) { await new Promise(async (resolve) => { @@ -1761,17 +1771,17 @@ router.post('/create-bead', authenticateToken, async (req, res) => { const emailPlayer = p.emailsDisabled ? null : await sgMail.send({ - to: p.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: p.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${p.name}, ${creator.name} just added a new bead. https://${appURL}/p/${parent.id} `, - html: ` + html: `

Hi ${p.name},
@@ -1780,7 +1790,7 @@ router.post('/create-bead', authenticateToken, async (req, res) => { bead.

`, - }) + }) Promise.all([notifyPlayer, emailPlayer]) .then(() => resolve2()) .catch((error) => resolve2(error)) @@ -1833,7 +1843,7 @@ router.post('/create-bead', authenticateToken, async (req, res) => { // test router.post('/update-post', authenticateToken, async (req, res) => { const accountId = req.user ? req.user.id : null - const id = req.body.id; + const id = req.body.id const post = await Post.findOne({ where: { id, creatorId: accountId }, attributes: ['id', 'type', 'mediaTypes'], @@ -1845,23 +1855,20 @@ router.post('/update-post', authenticateToken, async (req, res) => { }) if (!post) res.status(401).json({ message: 'Unauthorized' }) else { - const toUpdate = {}; + const toUpdate = {} for (const key of ['mediaTypes', 'title', 'text', 'searchableText', 'game', 'move']) { if (key in req.body) { toUpdate[key] = req.body[key] } } const promises = [] - const updatePost = await Post.update( - toUpdate, - { where: { id, creatorId: accountId } } - ) + const updatePost = await Post.update(toUpdate, { where: { id, creatorId: accountId } }) promises.push(updatePost) if ('game' in req.body) { await addRemixes(accountId, req.body.game, id) } if ('urls' in req.body) { - const newUrls = req.body.urls; + const newUrls = req.body.urls // update urls const oldUrlBlockLinks = await Link.findAll({ where: { @@ -2002,17 +2009,17 @@ router.post('/update-post', authenticateToken, async (req, res) => { const sendEmail = user.emailsDisabled ? null : await sgMail.send({ - to: user.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: user.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${user.name}, ${post.Creator.name} just mentioned you in a ${post.type} on weco: http://${appURL}/p/${id} `, - html: ` + html: `

Hi ${user.name},
@@ -2022,7 +2029,7 @@ router.post('/update-post', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([sendNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) @@ -2073,28 +2080,28 @@ router.post('/repost-post', authenticateToken, async (req, res) => { const sendNotification = skipNotification ? null : await Notification.create({ - ownerId: post.Creator.id, - type: 'post-repost', - seen: false, - spaceAId: spaceId, - userId: accountId, - postId, - }) + ownerId: post.Creator.id, + type: 'post-repost', + seen: false, + spaceAId: spaceId, + userId: accountId, + postId, + }) const sendEmail = skipEmail ? null : await sgMail.send({ - to: post.Creator.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: post.Creator.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${post.Creator.name}, ${accountName} just reposted your post on weco: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${post.Creator.name},
@@ -2104,7 +2111,7 @@ router.post('/repost-post', authenticateToken, async (req, res) => { on weco

`, - }) + }) const createReactions = await Promise.all( spaceIds.map((id) => @@ -2150,14 +2157,14 @@ router.post('/repost-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.increment('totalPostLikes', { - by: post.totalLikes, - }) + by: post.totalLikes, + }) : await SpaceUserStat.create({ - spaceId: id, - userId: post.Creator.id, - totalPostLikes: post.totalLikes, - totalUnseenMessages: 0, - }) + spaceId: id, + userId: post.Creator.id, + totalPostLikes: post.totalLikes, + totalUnseenMessages: 0, + }) Promise.all([ createSpacePost, incrementTotalPostLikes, @@ -2227,14 +2234,14 @@ router.post('/repost-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.increment('totalPostLikes', { - by: post.totalLikes, - }) + by: post.totalLikes, + }) : await SpaceUserStat.create({ - spaceId: id, - userId: post.Creator.id, - totalPostLikes: post.totalLikes, - totalUnseenMessages: 0, - }) + spaceId: id, + userId: post.Creator.id, + totalPostLikes: post.totalLikes, + totalUnseenMessages: 0, + }) Promise.all([ createSpacePost, updateSpaceStats, @@ -2298,30 +2305,30 @@ router.post('/add-like', authenticateToken, async (req, res) => { const updateSpaceStats = type !== 'link' ? Promise.all( - item.AllPostSpaces.map( - (space) => - new Promise(async (resolve) => { - const updateSpaceStat = await space.increment('totalPostLikes', { - silent: true, - }) - const spaceUserStat = await SpaceUserStat.findOne({ - where: { spaceId: space.id, userId: item.Creator.id }, - attributes: ['id'], - }) - const updateSpaceUserStat = spaceUserStat - ? await spaceUserStat.increment('totalPostLikes') - : await SpaceUserStat.create({ - spaceId: space.id, - userId: item.Creator.id, - totalPostLikes: 1, - totalUnseenMessages: 0, - }) - Promise.all([updateSpaceStat, updateSpaceUserStat]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) - ) - ) + item.AllPostSpaces.map( + (space) => + new Promise(async (resolve) => { + const updateSpaceStat = await space.increment('totalPostLikes', { + silent: true, + }) + const spaceUserStat = await SpaceUserStat.findOne({ + where: { spaceId: space.id, userId: item.Creator.id }, + attributes: ['id'], + }) + const updateSpaceUserStat = spaceUserStat + ? await spaceUserStat.increment('totalPostLikes') + : await SpaceUserStat.create({ + spaceId: space.id, + userId: item.Creator.id, + totalPostLikes: 1, + totalUnseenMessages: 0, + }) + Promise.all([updateSpaceStat, updateSpaceUserStat]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) + ) + ) : null const createReaction = await Reaction.create({ @@ -2353,14 +2360,14 @@ router.post('/add-like', authenticateToken, async (req, res) => { const createNotification = skipNotification ? null : await Notification.create({ - ownerId: item.Creator.id, - type: `${type}-like`, - seen: false, - userId: accountId, - spaceAId, - postId, - commentId, - }) + ownerId: item.Creator.id, + type: `${type}-like`, + seen: false, + userId: accountId, + spaceAId, + postId, + commentId, + }) const { handle, name, flagImagePath } = await User.findOne({ where: { id: accountId }, @@ -2382,14 +2389,14 @@ router.post('/add-like', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: item.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: item.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${item.Creator.name}, ${name} just liked your ${type} on weco: http://${itemUrl} `, - html: ` + html: `

Hi ${item.Creator.name},
@@ -2399,7 +2406,7 @@ router.post('/add-like', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([ updateTotalLikes, @@ -2438,22 +2445,22 @@ router.post('/remove-like', authenticateToken, async (req, res) => { const updateSpaceStats = type === 'post' ? Promise.all( - item.AllPostSpaces.map( - (space) => - new Promise(async (resolve) => { - const updateSpaceStat = await space.decrement('totalPostLikes', { - silent: true, - }) - const updateSpaceUserStat = await SpaceUserStat.decrement( - 'totalPostLikes', - { where: { spaceId: space.id, userId: item.Creator.id } } - ) - Promise.all([updateSpaceStat, updateSpaceUserStat]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) - ) - ) + item.AllPostSpaces.map( + (space) => + new Promise(async (resolve) => { + const updateSpaceStat = await space.decrement('totalPostLikes', { + silent: true, + }) + const updateSpaceUserStat = await SpaceUserStat.decrement( + 'totalPostLikes', + { where: { spaceId: space.id, userId: item.Creator.id } } + ) + Promise.all([updateSpaceStat, updateSpaceUserStat]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) + ) + ) : null const removeReaction = await Reaction.update( @@ -2517,27 +2524,27 @@ router.post('/add-rating', authenticateToken, async (req, res) => { const sendNotification = skipNotification ? null : await Notification.create({ - ownerId: item.Creator.id, - type: `${type}-rating`, - seen: false, - spaceAId: spaceId, - userId: accountId, - postId, - commentId, - }) + ownerId: item.Creator.id, + type: `${type}-rating`, + seen: false, + spaceAId: spaceId, + userId: accountId, + postId, + commentId, + }) const itemUrl = `${appURL}/p/${id}` const sendEmail = skipEmail ? null : await sgMail.send({ - to: item.Creator.email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` + to: item.Creator.email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` Hi ${item.Creator.name}, ${accountName} just rated your ${type} on weco: http://${itemUrl} `, - html: ` + html: `

Hi ${item.Creator.name},
@@ -2547,7 +2554,7 @@ router.post('/add-rating', authenticateToken, async (req, res) => { on weco

`, - }) + }) Promise.all([updateTotalRatings, createReaction, sendNotification, sendEmail]) .then(() => res.status(200).json({ message: 'Success' })) @@ -2705,29 +2712,33 @@ router.post('/add-link', authenticateToken, async (req, res) => { const sendEmail = skipEmail ? null : await sgMail.send({ - to: email, - from: { email: 'admin@weco.io', name: 'we { collective }' }, - subject: 'New notification', - text: ` - Hi ${name}, ${accountName} just linked ${type === 'user' ? 'you' : `your ${type}` - } to another ${location === 'source' ? sourceType : targetType - } on weco: + to: email, + from: { email: 'admin@weco.io', name: 'we { collective }' }, + subject: 'New notification', + text: ` + Hi ${name}, ${accountName} just linked ${ + type === 'user' ? 'you' : `your ${type}` + } to another ${ + location === 'source' ? sourceType : targetType + } on weco: http://${url} `, - html: ` + html: `

Hi ${name},
${accountName} - just linked ${type === 'user' - ? `you` - : `your ${type}` + just linked ${ + type === 'user' + ? `you` + : `your ${type}` } - to another ${location === 'source' ? sourceType : targetType + to another ${ + location === 'source' ? sourceType : targetType } on weco

`, - }) + }) Promise.all([createNotification, sendEmail]) .then(() => resolve()) .catch((error) => resolve(error)) @@ -2801,34 +2812,34 @@ router.post('/respond-to-event', authenticateToken, async (req, res) => { const updateStatus = previousResponse ? UserEvent.update({ state: 'removed' }, { where: { id: previousResponse.id } }) : new Promise(async (resolve) => { - const removeOtherResponseTypes = await UserEvent.update( - { state: 'removed' }, - { where: { userId: accountId, eventId, state: 'active' } } - ) - - const newResponse = await UserEvent.create({ - userId: accountId, - eventId, - relationship: response, - state: 'active', - }) - - const scheduleReminder = await scheduleEventNotification({ - type: response, - postId, - eventId, - userEventId: newResponse.id, - startTime, - userId: accountId, - userName: user.name, - userEmail: user.email, - emailsDisabled: user.emailsDisabled, - }) - - Promise.all([removeOtherResponseTypes, scheduleReminder]) - .then(() => resolve()) - .catch((error) => resolve(error)) - }) + const removeOtherResponseTypes = await UserEvent.update( + { state: 'removed' }, + { where: { userId: accountId, eventId, state: 'active' } } + ) + + const newResponse = await UserEvent.create({ + userId: accountId, + eventId, + relationship: response, + state: 'active', + }) + + const scheduleReminder = await scheduleEventNotification({ + type: response, + postId, + eventId, + userEventId: newResponse.id, + startTime, + userId: accountId, + userName: user.name, + userEmail: user.email, + emailsDisabled: user.emailsDisabled, + }) + + Promise.all([removeOtherResponseTypes, scheduleReminder]) + .then(() => resolve()) + .catch((error) => resolve(error)) + }) updateStatus .then(() => res.status(200).json({ message: 'Success' })) @@ -2881,89 +2892,89 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { const { type, action, threshold } = post.Poll const executeAction = action ? Promise.all( - voteData.map( - (answer) => - new Promise(async (resolve1) => { - // find poll answer - const pollAnswer = await Post.findOne({ - where: { id: answer.id }, - attributes: ['id', 'text'], - include: { - model: Reaction, - where: { type: 'vote', state: 'active' }, - required: false, - attributes: ['value'], - }, - }) - const answerLink = await Link.findOne({ - where: { - itemAId: postId, - itemAType: 'post', - itemBId: answer.id, - itemBType: 'poll-answer', - }, - attributes: ['id', 'state'], - }) - const { text, Reactions } = pollAnswer - let totalVotes - if (type === 'weighted-choice') - totalVotes = - Reactions.map((r) => +r.value).reduce((a, b) => a + b, 0) / - 100 - else totalVotes = Reactions.length - const createSpace = - action === 'Create spaces' && - answerLink.state !== 'done' && - totalVotes >= threshold - ? new Promise(async (resolve2) => { - const markAnswerDone = await answerLink.update({ - state: 'done', - }) - const newSpace = await Space.create({ - creatorId: post.Creator.id, - handle: uuidv4().substring(0, 15), - name: text, - description: null, - state: 'active', - privacy: 'public', - totalPostLikes: 0, - totalPosts: 0, - totalComments: 0, - totalFollowers: 1, - }) - const createModRelationship = SpaceUser.create({ - relationship: 'moderator', - state: 'active', - spaceId: newSpace.id, - userId: post.Creator.id, - }) - const createFollowerRelationship = SpaceUser.create({ - relationship: 'follower', - state: 'active', - spaceId: newSpace.id, - userId: post.Creator.id, + voteData.map( + (answer) => + new Promise(async (resolve1) => { + // find poll answer + const pollAnswer = await Post.findOne({ + where: { id: answer.id }, + attributes: ['id', 'text'], + include: { + model: Reaction, + where: { type: 'vote', state: 'active' }, + required: false, + attributes: ['value'], + }, + }) + const answerLink = await Link.findOne({ + where: { + itemAId: postId, + itemAType: 'post', + itemBId: answer.id, + itemBType: 'poll-answer', + }, + attributes: ['id', 'state'], + }) + const { text, Reactions } = pollAnswer + let totalVotes + if (type === 'weighted-choice') + totalVotes = + Reactions.map((r) => +r.value).reduce((a, b) => a + b, 0) / + 100 + else totalVotes = Reactions.length + const createSpace = + action === 'Create spaces' && + answerLink.state !== 'done' && + totalVotes >= threshold + ? new Promise(async (resolve2) => { + const markAnswerDone = await answerLink.update({ + state: 'done', + }) + const newSpace = await Space.create({ + creatorId: post.Creator.id, + handle: uuidv4().substring(0, 15), + name: text, + description: null, + state: 'active', + privacy: 'public', + totalPostLikes: 0, + totalPosts: 0, + totalComments: 0, + totalFollowers: 1, + }) + const createModRelationship = SpaceUser.create({ + relationship: 'moderator', + state: 'active', + spaceId: newSpace.id, + userId: post.Creator.id, + }) + const createFollowerRelationship = SpaceUser.create({ + relationship: 'follower', + state: 'active', + spaceId: newSpace.id, + userId: post.Creator.id, + }) + const attachToParent = await attachParentSpace( + newSpace.id, + post.Poll.spaceId + ) + Promise.all([ + markAnswerDone, + createModRelationship, + createFollowerRelationship, + attachToParent, + ]) + .then(() => resolve2()) + .catch((error) => resolve2(error)) }) - const attachToParent = await attachParentSpace( - newSpace.id, - post.Poll.spaceId - ) - Promise.all([ - markAnswerDone, - createModRelationship, - createFollowerRelationship, - attachToParent, - ]) - .then(() => resolve2()) - .catch((error) => resolve2(error)) - }) - : null - - Promise.all([createSpace]) - .then(() => resolve1()) - .catch((error) => resolve1(error)) - }) - ) - ) + : null + + Promise.all([createSpace]) + .then(() => resolve1()) + .catch((error) => resolve1(error)) + }) + ) + ) : null const skipNotification = post.Creator.id === accountId @@ -2972,27 +2983,27 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { const createNotification = skipNotification ? null : await Notification.create({ - ownerId: post.Creator.id, - type: 'poll-vote', - seen: false, - userId: accountId, - postId, - }) + ownerId: post.Creator.id, + type: 'poll-vote', + seen: false, + userId: accountId, + postId, + }) const sendEmail = skipEmail ? null : await sgMail.send({ - to: post.Creator.email, - from: { - email: 'admin@weco.io', - name: 'we { collective }', - }, - subject: 'New notification', - text: ` + to: post.Creator.email, + from: { + email: 'admin@weco.io', + name: 'we { collective }', + }, + subject: 'New notification', + text: ` Hi ${post.Creator.name}, ${userName} just voted on your Poll: http://${appURL}/p/${postId} `, - html: ` + html: `

Hi ${post.Creator.name},
@@ -3001,7 +3012,7 @@ router.post('/vote-on-poll', authenticateToken, async (req, res) => { Poll

`, - }) + }) const updateLastPostActivity = await Post.update( { lastActivity: new Date() }, @@ -3174,23 +3185,23 @@ router.post('/delete-post', authenticateToken, async (req, res) => { ) await Link.update( - { state: 'deleted', }, + { state: 'deleted' }, { where: { state: 'active', [Op.or]: [ { itemAType: 'post', - itemAId: postId + itemAId: postId, }, { itemBType: 'post', - itemBId: postId - } - ] - } + itemBId: postId, + }, + ], + }, } - ); + ) const updateSpaceStats = await Promise.all( post.AllPostSpaces.map( @@ -3210,8 +3221,8 @@ router.post('/delete-post', authenticateToken, async (req, res) => { }) const updateSpaceUserStat = spaceUserStat ? await spaceUserStat.update({ - totalPostLikes: spaceUserStat.totalPostLikes - post.totalLikes, - }) + totalPostLikes: spaceUserStat.totalPostLikes - post.totalLikes, + }) : null Promise.all([updateSpace, updateSpaceUserStat]) .then(() => resolve()) @@ -3242,21 +3253,21 @@ router.post('/delete-comment', authenticateToken, async (req, res) => { { where: { id: postId, creatorId: accountId } } ) await Link.update( - { state: 'deleted', }, + { state: 'deleted' }, { where: { state: 'active', [Op.or]: [ { - itemAId: postId + itemAId: postId, }, { - itemBId: postId - } - ] - } + itemBId: postId, + }, + ], + }, } - ); + ) // get links & root post for tally updates const rootLink = await Link.findOne({ where: { itemBId: postId, itemBType: 'comment', relationship: 'root' }, From a7ef559477642a06814394c7e866e968e59799e1 Mon Sep 17 00:00:00 2001 From: jhweir Date: Sun, 21 Apr 2024 17:18:08 +0100 Subject: [PATCH 19/21] script syntax update to work on windows --- docs/notes.md | 4 ++ package-lock.json | 151 +++++++++++++++++++++++++++++----------------- package.json | 2 +- 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/docs/notes.md b/docs/notes.md index 6293140..d35bfd4 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -30,3 +30,7 @@ sudo du -x -h / | sort -h | tail -40 # flush pm2 logs pm2 flush + +# deployment notes + +Change dev script "concurrently -n node,ts \"nodemon Server.js\" \"tsc --watch\"" to "concurrently -n node,ts 'nodemon Server.js' 'tsc --watch'" to work on linux server diff --git a/package-lock.json b/package-lock.json index 8e00e5a..b7570be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2992,12 +2992,12 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -3005,7 +3005,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3405,9 +3405,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3932,13 +3932,14 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -4410,6 +4411,25 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esniff/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4495,16 +4515,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -4800,9 +4820,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -6419,9 +6439,9 @@ } }, "node_modules/mysql2": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.8.0.tgz", - "integrity": "sha512-rC9J/Wy9TCaoQWhk/p4J0Jd+WCDYghniuawi7pheDqhQOEJyDfiWGiWOR3iPgTFJaOK3GezC7dmCki7cP1HFkQ==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", + "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -7236,9 +7256,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -8157,9 +8177,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -11281,12 +11301,12 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -11294,7 +11314,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -11597,9 +11617,9 @@ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "cookie-signature": { "version": "1.0.6", @@ -11999,12 +12019,13 @@ } }, "es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "requires": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, @@ -12358,6 +12379,24 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, + "esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "dependencies": { + "type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + } + } + }, "espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -12419,16 +12458,16 @@ "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -12677,9 +12716,9 @@ } }, "follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -13859,9 +13898,9 @@ } }, "mysql2": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.8.0.tgz", - "integrity": "sha512-rC9J/Wy9TCaoQWhk/p4J0Jd+WCDYghniuawi7pheDqhQOEJyDfiWGiWOR3iPgTFJaOK3GezC7dmCki7cP1HFkQ==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", + "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", "requires": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -14440,9 +14479,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -15119,9 +15158,9 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/package.json b/package.json index 400c664..b4e3015 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "deps": "docker-compose up --build mysql", "migrate": "sequelize db:migrate", "seed": "sequelize db:seed:all", - "dev": "concurrently -n node,ts 'nodemon Server.js' 'tsc --watch'", + "dev": "concurrently -n node,ts \"nodemon Server.js\" \"tsc --watch\"", "start": "node Server.js" }, "keywords": [], From 650db8c706cc4219f18ef06e8e2d9d28cfba8b3f Mon Sep 17 00:00:00 2001 From: jhweir Date: Sun, 28 Apr 2024 20:18:08 +0100 Subject: [PATCH 20/21] 'move' value added to create post migration --- migrations/create-post.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/migrations/create-post.js b/migrations/create-post.js index 9172eee..0b960b2 100644 --- a/migrations/create-post.js +++ b/migrations/create-post.js @@ -62,6 +62,9 @@ module.exports = { game: { type: Sequelize.JSON, }, + move: { + type: Sequelize.JSON, + }, lastActivity: { type: Sequelize.DATE, }, From 447019fa758444151280cab88ef0ac94c70de12e Mon Sep 17 00:00:00 2001 From: jhweir Date: Mon, 29 Apr 2024 19:38:54 +0100 Subject: [PATCH 21/21] Upload limit changes --- Helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Helpers.js b/Helpers.js index e87ad47..38a3b39 100644 --- a/Helpers.js +++ b/Helpers.js @@ -42,8 +42,8 @@ const sgMail = require('@sendgrid/mail') const { uniq } = require('lodash') sgMail.setApiKey(process.env.SENDGRID_API_KEY) -const imageMBLimit = 10 -const audioMBLimit = 30 +const imageMBLimit = 20 +const audioMBLimit = 100 const defaultPostValues = { state: 'active', watermark: false,