diff --git a/GameServer.js b/GameServer.js
new file mode 100644
index 0000000..9ebb7a9
--- /dev/null
+++ b/GameServer.js
@@ -0,0 +1,468 @@
+"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.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"));
+const sequelize_1 = __importDefault(require("sequelize"));
+const { Post, Link } = require('./models');
+const { createPost } = require('./Helpers');
+const { Op } = sequelize_1.default;
+const POST_TYPE = [
+ 'post',
+ '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({
+ raw: true,
+ nest: true,
+ where: {
+ state: 'active',
+ id
+ },
+ });
+ if (!post) {
+ throw new Error(`Post ${id} not found`);
+ }
+ return post;
+ });
+}
+function updatePost(post, data) {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield Post.update(data, { where: { id: post.id } });
+ return Object.assign(Object.assign({}, post), data);
+ });
+}
+const getFirstMove = (step, variables, players) => {
+ var _a;
+ if (!step) {
+ return undefined;
+ }
+ switch (step.type) {
+ case 'move':
+ return { step, variables };
+ case 'sequence': {
+ 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;
+ }
+ if (!step.repeat) {
+ return firstStep;
+ }
+ 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 : players[0]) }),
+ };
+ }
+ default: {
+ const exhaustivenessCheck = step;
+ throw exhaustivenessCheck;
+ }
+ }
+};
+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, players);
+ if (nextStep) {
+ return { current, next: nextStep.step, variables: nextStep.variables };
+ }
+ }
+ else {
+ switch (step.type) {
+ case 'move':
+ if (step.id === stepId) {
+ current = step;
+ }
+ break;
+ case 'sequence': {
+ const result = getTransition(step.steps, stepId, currentVariables, players);
+ if (!result) {
+ break;
+ }
+ if (result.next) {
+ return result;
+ }
+ 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 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 }), players);
+ 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;
+ });
+}
+const variableRegex = /\[([^\]]+)\]/;
+function insertVariables(text, variables) {
+ 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') {
+ return `@${value.name}`;
+ }
+ return `${variables[variableName]}`;
+ }
+ return substring;
+ });
+}
+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 + ((_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: '',
+ title: insertVariables(step.title, variables),
+ text: insertVariables(step.text, variables),
+ move
+ }, gamePost);
+ scheduleMoveTimeout(movePost, io);
+ const changedGamePost = yield updatePost(gamePost, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ status: 'started',
+ step,
+ moveId: movePost.id,
+ variables: variables,
+ } })
+ });
+ return { changedGamePost, changedMoves: [movePost] };
+ });
+}
+function nextMove(gamePost, io) {
+ return __awaiter(this, void 0, void 0, function* () {
+ var _a;
+ const game = gamePost.game;
+ const play = game.play;
+ if (play.status !== 'started' && play.status !== 'paused') {
+ return;
+ }
+ 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);
+ }
+ else {
+ const changedGamePost = yield updatePost(gamePost, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ status: 'paused',
+ step: transition.next,
+ variables: transition.variables,
+ } })
+ });
+ return {
+ changedGamePost,
+ };
+ }
+ }
+ else {
+ const changedGamePost = yield updatePost(gamePost, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ 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(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;
+ }
+ yield moveTimeout(currentMovePost, io);
+ }));
+}
+const EVENTS = {
+ outgoing: {
+ 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'
+ }
+};
+function initializeGameServerTasks(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;
+ }
+ if (move.timeout < +new Date()) {
+ yield moveTimeout(post, io);
+ }
+ else {
+ 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 }) {
+ const post = yield getPost(id);
+ const changedGamePost = yield updatePost(post, {
+ game
+ });
+ emitChanges(io, { changedGamePost });
+ }));
+ socket.on(EVENTS.outgoing.start, (_b) => __awaiter(this, [_b], void 0, function* ({ id }) {
+ const gamePost = yield getPost(id);
+ const game = gamePost.game;
+ const play = game.play;
+ if (play.status === 'started') {
+ return;
+ }
+ let changes;
+ if (play.status === 'paused') {
+ const moveId = play.moveId;
+ if (!moveId) {
+ changes = yield startNewMove(gamePost, play.step, play.variables, io);
+ }
+ else {
+ const changedGamePost = yield updatePost(gamePost, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ status: 'started',
+ moveId,
+ 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 newMovePost = yield updatePost(movePost, {
+ move: Object.assign(Object.assign({}, move), { status: 'started', elapsedTime: move.elapsedTime, startedAt: now, timeout })
+ });
+ scheduleMoveTimeout(newMovePost, io);
+ changes = {
+ changedGamePost,
+ changedMoves: [newMovePost]
+ };
+ }
+ else {
+ changes = {
+ changedGamePost
+ };
+ }
+ }
+ }
+ else {
+ 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
+ };
+ }
+ }
+ emitChanges(io, changes);
+ }));
+ 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) {
+ const movePost = yield getPost(play.moveId);
+ const move = movePost.move;
+ if (move.status === 'started' || move.status === 'paused') {
+ skippedMovePost = yield updatePost(movePost, {
+ move: Object.assign(Object.assign({}, move), { status: 'skipped' })
+ });
+ }
+ }
+ const changes = yield nextMove(gamePost, io);
+ 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, (_g) => __awaiter(this, [_g], void 0, function* ({ id }) {
+ const post = yield getPost(id);
+ const game = post.game;
+ const play = game.play;
+ if (play.status !== 'started') {
+ return;
+ }
+ const changedGamePost = yield updatePost(post, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ status: 'paused',
+ step: play.step,
+ variables: play.variables,
+ moveId: play.moveId,
+ } })
+ });
+ const movePost = yield getPost(play.moveId);
+ const move = movePost.move;
+ let pausedMovePost;
+ if (move.status === 'started') {
+ const now = +new Date();
+ pausedMovePost = yield updatePost(movePost, {
+ move: Object.assign(Object.assign({}, move), { status: 'paused', elapsedTime: move.elapsedTime + now - move.startedAt, remainingTime: move.timeout - now })
+ });
+ }
+ emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] });
+ }));
+ 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;
+ if (play.status !== 'started' && play.status !== 'paused') {
+ return;
+ }
+ const changedGamePost = yield updatePost(post, {
+ game: Object.assign(Object.assign({}, game), { play: {
+ status: 'stopped',
+ variables: play.variables,
+ } })
+ });
+ let stoppedMovePost;
+ if (play.moveId) {
+ const movePost = yield getPost(play.moveId);
+ const move = movePost.move;
+ if (move.status === 'started' || move.status === 'paused') {
+ stoppedMovePost = yield updatePost(movePost, {
+ move: Object.assign(Object.assign({}, move), { status: 'stopped' })
+ });
+ }
+ }
+ emitChanges(io, { changedGamePost, changedMoves: stoppedMovePost && [stoppedMovePost] });
+ }));
+ });
+}
+exports.registerGameServerEvents = registerGameServerEvents;
diff --git a/GameServer.ts b/GameServer.ts
new file mode 100644
index 0000000..9f0de02
--- /dev/null
+++ b/GameServer.ts
@@ -0,0 +1,649 @@
+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',
+ '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
+ move?: Move
+}
+
+export type Step = {
+ id: string
+ name: string
+ originalStep?: { gameId: number; stepId: string }
+} & (
+ | {
+ type: 'move'
+ title?: string
+ text: string
+ timeout: string
+ submission?: { player: string } & ({
+ type: 'audio'
+ maxDuration: string
+ }
+ | { type: 'text' })
+ }
+ | {
+ type: 'sequence'
+ repeat?: { type: 'rounds'; amount: number } | { type: 'turns' }
+ steps: Step[]
+ }
+ )
+
+type MoveStep = Extract
+
+type Game = {
+ steps: Step[]
+ play: Play
+ players: BaseUser[]
+}
+
+export type BaseUser = {
+ id: number
+ handle: string
+ name: string
+ flagImagePath: string
+}
+
+export type Move = (
+ | { status: 'skipped' | 'ended' | 'stopped' | 'timeout' }
+ | { status: 'paused'; elapsedTime: number; remainingTime: number }
+ | {
+ status: 'started'
+ elapsedTime: number
+ startedAt: number
+ timeout: number
+ }
+) & { 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
+
+export type Play = {
+ variables: PlayVariables
+} & (
+ | { status: 'waiting' | 'stopped' | 'ended' }
+ | { status: 'paused'; step: MoveStep; moveId?: number }
+ | { status: 'started' | 'paused'; step: MoveStep; moveId: number }
+ )
+
+async function getPost(id: number): Promise {
+ const post = await Post.findOne({
+ raw: true,
+ nest: true,
+ where: {
+ state: 'active',
+ id
+ },
+ })
+
+ if (!post) {
+ throw new Error(`Post ${id} not found`)
+ }
+
+ return post;
+}
+
+async function updatePost(post: Post, data: Partial): Promise {
+ await Post.update(data, { where: { id: post.id } })
+ return {
+ ...post,
+ ...data,
+ }
+}
+
+const getFirstMove = (
+ step: Step | undefined,
+ variables: PlayVariables,
+ players: BaseUser[]
+): undefined | { step: MoveStep; variables: PlayVariables } => {
+ if (!step) {
+ return undefined
+ }
+ switch (step.type) {
+ case 'move':
+ return { step, variables }
+ case 'sequence': {
+ 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
+ }
+ if (!step.repeat) {
+ return firstStep
+ }
+
+ return {
+ step: firstStep.step,
+ variables: {
+ ...firstStep.variables,
+ [step.name]: firstStep.variables[step.name] ?? (step.repeat.type === 'rounds' ? 1 : players[0]),
+ },
+ }
+ }
+ default: {
+ const exhaustivenessCheck: never = step
+ throw exhaustivenessCheck
+ }
+ }
+}
+
+const getTransition = (
+ steps: Step[],
+ stepId: string,
+ variables: PlayVariables,
+ players: BaseUser[]
+): 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, players)
+ if (nextStep) {
+ return { current, next: nextStep.step, variables: nextStep.variables }
+ }
+ } else {
+ switch (step.type) {
+ case 'move':
+ if (step.id === stepId) {
+ current = step
+ }
+ break
+ case 'sequence': {
+ const result = getTransition(step.steps, stepId, currentVariables, players)
+ if (!result) {
+ break
+ }
+
+ if (result.next) {
+ return result
+ }
+
+ 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 player = currentVariables[step.name] as BaseUser
+ const playerIndex = players.findIndex(p => p.id === player.id)
+ nextValue = players[playerIndex + 1]
+ }
+ if (nextValue) {
+ const firstStep = getFirstMove(
+ step,
+ {
+ ...result.variables,
+ [step.name]: nextValue,
+ },
+ players
+ )
+ 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): Promise {
+ 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
+}
+
+const variableRegex = /\[([^\]]+)\]/
+
+function insertVariables(text: string | undefined, variables: PlayVariables) {
+ return text?.replace(variableRegex, (substring, variableName) => {
+ if (variableName in variables) {
+ const value = variables[variableName]
+ if (typeof value === 'object') {
+ return `@${value.name}`
+ }
+ return `${variables[variableName]}`
+ }
+ return substring
+ })
+}
+
+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();
+ const timeout = now + (parseDuration(step.timeout) ?? 0)
+
+
+ const move: Move = {
+ status: 'started',
+ elapsedTime: 0,
+ startedAt: now,
+ timeout,
+ gameId: gamePost.id,
+ ...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({
+ type: 'post',
+ mediaTypes: '',
+ title: insertVariables(step.title, variables),
+ text: insertVariables(step.text, variables),
+ move
+ }, gamePost)
+ scheduleMoveTimeout(movePost, io)
+
+ const changedGamePost = await updatePost(gamePost, {
+ game: {
+ ...game,
+ play: {
+ status: 'started',
+ step,
+ moveId: movePost.id,
+ variables: variables,
+ }
+ }
+ })
+
+ return { changedGamePost, changedMoves: [movePost] }
+}
+
+async function nextMove(gamePost: Post, io: Server): Promise {
+ const game = gamePost.game!;
+ const play = game.play!;
+ if (play.status !== 'started' && play.status !== 'paused') {
+ return;
+ }
+ const transition = getTransition(
+ game.steps,
+ play.step.id,
+ play.variables,
+ game.players
+ )
+
+ if (transition?.next) {
+ if (play.status === 'started') {
+ return await startNewMove(gamePost, transition.next, transition.variables, io)
+ } else {
+ const changedGamePost = await updatePost(gamePost, {
+ game: {
+ ...game,
+ play: {
+ status: 'paused',
+ step: transition.next,
+ variables: transition.variables,
+ }
+ }
+ })
+ return {
+ changedGamePost,
+ }
+ }
+ } else {
+ const changedGamePost = await updatePost(gamePost, {
+ game: {
+ ...game,
+ play: {
+ status: 'ended',
+ variables: transition?.variables ?? play.variables
+ }
+ }
+ })
+ return { changedGamePost }
+ }
+}
+
+
+async function moveTimeout(movePost: Post, io: Server) {
+ const move = movePost.move!
+ const newMovePost = await updatePost(movePost, {
+ move: {
+ ...move,
+ status: 'ended'
+ }
+ })
+ if (move.gameId) {
+ const gamePost = await getPost(move.gameId);
+ const changes = await nextMove(gamePost, io)
+ if (changes) {
+ emitChanges(io, { ...changes!, changedMoves: [newMovePost, ...changes.changedMoves ?? []] })
+ }
+ }
+}
+
+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
+ }
+ await moveTimeout(currentMovePost, io)
+ })
+}
+
+const EVENTS = {
+ outgoing: {
+ 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'
+ }
+}
+
+export async function initializeGameServerTasks(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
+ }
+
+ if (move.timeout < +new Date()) {
+ await moveTimeout(post, io)
+ } else {
+ 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 }) => {
+ const post = await getPost(id)
+ const changedGamePost = await updatePost(post, {
+ game
+ })
+
+ emitChanges(io, { changedGamePost })
+ })
+
+ socket.on(EVENTS.outgoing.start, async ({ id }: { id: number }) => {
+ const gamePost: Post = await getPost(id)
+ const game = gamePost.game!
+ const play = game.play;
+
+ if (play.status === 'started') {
+ return
+ }
+
+ let changes: Changes;
+ if (play.status === 'paused') {
+ const moveId = play.moveId;
+ if (!moveId) {
+ changes = await startNewMove(gamePost, play.step, play.variables, io)
+ } else {
+ const changedGamePost = await updatePost(gamePost, {
+ game: {
+ ...game,
+ play: {
+ status: 'started',
+ moveId,
+ step: play.step,
+ variables: play.variables,
+ }
+ }
+ })
+ 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 newMovePost = await updatePost(movePost, {
+ move: {
+ ...move,
+ status: 'started',
+ elapsedTime: move.elapsedTime,
+ startedAt: now,
+ timeout,
+ }
+ })
+ scheduleMoveTimeout(newMovePost, io)
+ changes = {
+ changedGamePost,
+ changedMoves: [newMovePost]
+ }
+ } else {
+ changes = {
+ changedGamePost
+ }
+ }
+ }
+ } else {
+ const variables = {}
+ const step = getFirstMove(game.steps[0], variables, game.players);
+ if (step) {
+ changes = await startNewMove(gamePost, step.step, step.variables, io)
+ } else {
+ const changedGamePost = await updatePost(gamePost, {
+ game: {
+ ...game,
+ play: {
+ status: 'ended',
+ variables
+ }
+ }
+ })
+ changes = {
+ changedGamePost
+ }
+ }
+ }
+ 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
+ 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') {
+ skippedMovePost = await updatePost(movePost, {
+ move: {
+ ...move,
+ status: 'skipped',
+ }
+ })
+ }
+ }
+ const changes = await nextMove(gamePost, io)
+ emitChanges(io, { ...changes!, changedMoves: skippedMovePost && [skippedMovePost, ...changes?.changedMoves ?? []] })
+ })
+
+ socket.on(EVENTS.outgoing.pause, async ({ id }: { id: number }) => {
+ const post = await getPost(id)
+ const game = post.game!
+ const play = game.play;
+
+ if (play.status !== 'started') {
+ return;
+ }
+
+ const changedGamePost = await updatePost(post, {
+ game: {
+ ...game,
+ play: {
+ status: 'paused',
+ step: play.step,
+ variables: play.variables,
+ moveId: play.moveId,
+ }
+ }
+ })
+
+ const movePost = await getPost(play.moveId);
+ const move = movePost.move!;
+ let pausedMovePost: Post | undefined;
+ if (move.status === 'started') {
+ const now = + new Date();
+ pausedMovePost = await updatePost(movePost, {
+ move: {
+ ...move,
+ status: 'paused',
+ elapsedTime: move.elapsedTime + now - move.startedAt,
+ remainingTime: move.timeout - now
+ }
+ })
+ }
+
+ emitChanges(io, { changedGamePost, changedMoves: pausedMovePost && [pausedMovePost] })
+ })
+
+ socket.on(EVENTS.outgoing.stop, async ({ id }: { id: number }) => {
+ const post = await getPost(id)
+ const game = post.game!
+ const play = game.play;
+
+ if (play.status !== 'started' && play.status !== 'paused') {
+ return
+ }
+
+ const changedGamePost = await updatePost(post, {
+ game: {
+ ...game,
+ play: {
+ status: 'stopped',
+ variables: play.variables,
+ }
+ }
+ })
+
+ let stoppedMovePost: Post | undefined;
+ if (play.moveId) {
+ const movePost = await getPost(play.moveId);
+ const move = movePost.move!;
+ if (move.status === 'started' || move.status === 'paused') {
+ stoppedMovePost = await updatePost(movePost, {
+ move: {
+ ...move,
+ status: 'stopped',
+ }
+ })
+ }
+ }
+
+ emitChanges(io, { changedGamePost, changedMoves: stoppedMovePost && [stoppedMovePost] })
+ })
+}
diff --git a/Helpers.js b/Helpers.js
index a01f688..38a3b39 100644
--- a/Helpers.js
+++ b/Helpers.js
@@ -39,10 +39,11 @@ 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
-const audioMBLimit = 30
+const imageMBLimit = 20
+const audioMBLimit = 100
const defaultPostValues = {
state: 'active',
watermark: false,
@@ -965,6 +966,8 @@ const fullPostAttributes = [
'totalReposts',
'totalRatings',
'totalLinks',
+ 'game',
+ 'move',
]
// todo: replace all use cases with const fullPostAttributes above
@@ -984,6 +987,8 @@ function findFullPostAttributes(model, accountId) {
'totalReposts',
'totalRatings',
'totalLinks',
+ 'game',
+ 'move',
// accountLike('post', model, accountId),
// accountComment('post', model, accountId),
// accountLink('post', model, accountId),
@@ -1142,6 +1147,28 @@ function findPostInclude(accountId) {
},
},
},
+ {
+ model: Link,
+ as: 'Originals',
+ required: false,
+ where: { relationship: 'remix', state: 'active' },
+ include: {
+ model: Post,
+ as: 'Parent',
+ attributes: ['id', 'title', 'game', 'state'],
+ },
+ },
+ {
+ model: Link,
+ as: 'Remixes',
+ separate: true,
+ where: { relationship: 'remix', state: 'active' },
+ order: [['index', 'ASC']],
+ include: {
+ model: Post,
+ attributes: ['id', 'title', 'game', 'state'],
+ },
+ },
{
model: Reaction,
where: { creatorId: accountId, state: 'active' },
@@ -1683,6 +1710,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) {
@@ -1700,6 +1771,8 @@ function createPost(data, files, accountId) {
event,
poll,
glassBeadGame,
+ game,
+ move,
card,
color,
watermark,
@@ -1721,10 +1794,16 @@ function createPost(data, files, accountId) {
color: color || null,
watermark: !!watermark,
lastActivity: new Date(),
+ game,
+ move,
})
+ if (game) {
+ await addRemixes(accountId, game, post.id)
+ }
+
// 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' },
@@ -1946,6 +2025,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,
@@ -2217,6 +2310,7 @@ module.exports = {
accountLink,
uploadFiles,
createPost,
+ addRemixes,
attachComment,
scheduleNextBeadDeadline,
pushNotification,
diff --git a/ScheduledTasks.js b/ScheduledTasks.js
index 4671631..04f3135 100644
--- a/ScheduledTasks.js
+++ b/ScheduledTasks.js
@@ -5,8 +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 io = require('./Socket')
+const { initializeGameServerTasks } = require('./GameServer')
-function scheduleEventNotification(data) {
+async function scheduleEventNotification(data) {
const {
type,
postId,
@@ -344,6 +346,8 @@ async function initializeScheduledTasks() {
if (nextPlayer) scheduleGBGMoveJobs(id, nextPlayer, moveNumber, nextMoveDeadline)
}
})
+
+ await initializeGameServerTasks(io)
}
module.exports = {
diff --git a/Socket.js b/Socket.js
index 9f23168..6d7b37d 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 { registerGameServerEvents } = require('./GameServer')
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)
// }
// })
+
+ registerGameServerEvents(socket, io)
})
socketServer.listen(5001)
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/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/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/migrations/create-post.js b/migrations/create-post.js
index 42e91fa..0b960b2 100644
--- a/migrations/create-post.js
+++ b/migrations/create-post.js
@@ -59,6 +59,12 @@ module.exports = {
totalGlassBeadGames: {
type: Sequelize.INTEGER,
},
+ game: {
+ type: Sequelize.JSON,
+ },
+ move: {
+ type: Sequelize.JSON,
+ },
lastActivity: {
type: Sequelize.DATE,
},
diff --git a/models/Post.js b/models/Post.js
index c27f000..0c70f69 100644
--- a/models/Post.js
+++ b/models/Post.js
@@ -25,6 +25,8 @@ module.exports = (sequelize, DataTypes) => {
totalReposts: DataTypes.INTEGER,
totalRatings: DataTypes.INTEGER,
totalGlassBeadGames: DataTypes.INTEGER,
+ game: DataTypes.JSON,
+ move: DataTypes.JSON,
lastActivity: DataTypes.DATE,
},
{}
@@ -48,6 +50,9 @@ 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: '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/package-lock.json b/package-lock.json
index bf54eb1..b7570be 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",
@@ -36,8 +37,12 @@
"web-push": "^3.6.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"
+ "eslint-config-airbnb-typescript-prettier": "^5.0.0",
+ "typescript": "^5.4.2"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2203,6 +2208,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",
@@ -2216,6 +2227,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",
@@ -2972,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",
@@ -2985,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"
},
@@ -3271,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",
@@ -3325,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"
}
@@ -3418,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",
@@ -3836,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": {
@@ -4314,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",
@@ -4399,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",
@@ -4704,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",
@@ -6323,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",
@@ -6717,6 +6833,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",
@@ -7135,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",
@@ -7310,6 +7431,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",
@@ -7689,6 +7819,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",
@@ -7789,6 +7928,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",
@@ -8032,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",
@@ -8176,6 +8321,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",
@@ -8326,11 +8480,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"
@@ -10583,6 +10736,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",
@@ -10596,6 +10755,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",
@@ -11133,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",
@@ -11146,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"
},
@@ -11364,6 +11532,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",
@@ -11406,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",
@@ -11481,6 +11692,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",
@@ -11799,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"
}
},
@@ -12158,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",
@@ -12219,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",
@@ -12477,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",
@@ -13659,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",
@@ -13948,6 +14187,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",
@@ -14235,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",
@@ -14358,6 +14602,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",
@@ -14631,6 +14884,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",
@@ -14704,6 +14963,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",
@@ -14893,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",
@@ -15015,6 +15280,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",
@@ -15131,11 +15402,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 b03271e..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": "nodemon Server.js",
+ "dev": "concurrently -n node,ts \"nodemon Server.js\" \"tsc --watch\"",
"start": "node Server.js"
},
"keywords": [],
@@ -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",
@@ -41,7 +42,11 @@
"web-push": "^3.6.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"
+ "eslint-config-airbnb-typescript-prettier": "^5.0.0",
+ "typescript": "^5.4.2"
}
}
diff --git a/routes/Post.js b/routes/Post.js
index be14a45..653b5cb 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,34 @@ 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, ids } = 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)
})
@@ -685,7 +700,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 +723,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 +742,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 +763,7 @@ router.get('/post-comments', async (req, res) => {
})
}
- getChildComments(post, 0)
+ getChildComments(post, 0, +limit)
.then(() =>
res.status(200).json({
totalChildren: post.totalChildComments,
@@ -758,6 +773,91 @@ 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, childrenIds } = req.query
+
+ const query = {
+ order: [['createdAt', 'DESC']],
+ attributes: ['state'],
+ include: [
+ {
+ model: Post,
+ attributes: fullPostAttributes,
+ include: [
+ {
+ 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',
+ ],
+ },
+ {
+ 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'],
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ where: {
+ state: 'active',
+ relationship: 'parent',
+ 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) })
+})
+
router.get('/post-indirect-spaces', async (req, res) => {
const { postId } = req.query
const post = await Post.findOne({
@@ -1350,7 +1450,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,
@@ -1393,7 +1493,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',
@@ -1437,6 +1537,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))
@@ -1572,107 +1678,110 @@ 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},
@@ -1681,47 +1790,60 @@ 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(
+ })
+ Promise.all([notifyPlayer, emailPlayer])
+ .then(() => resolve2())
+ .catch((error) => resolve2(error))
+ })
+ )
+ )
+ // schedule next deadline
+ if (moveTimeWindow) {
+ newDeadline = await scheduleNextBeadDeadline(
parent.id,
gamePost.GlassBeadGame,
gamePost.Players
)
- : null
-
- Promise.all([sendNotifications, scheduleNewDeadline])
- .then((data) => resolve(data[1]))
- .catch((error) => resolve(error))
- })
- : null
+ }
+ })
+ }
- const incrementTotalBeads = await GlassBeadGame.increment('totalBeads', {
- where: { postId: parent.id },
- })
+ 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 })
}
})
// 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 +1855,83 @@ router.post('/update-post', authenticateToken, async (req, res) => {
})
if (!post) res.status(401).json({ message: 'Unauthorized' })
else {
- const updatePost = await Post.update(
- { mediaTypes, title, text, searchableText },
- { 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'] },
+ 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 } })
+ 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
+ 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 +1979,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 +2029,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 })
+ })
}
})
@@ -3044,6 +3184,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) =>
@@ -3093,6 +3252,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
new file mode 100644
index 0000000..c946444
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "module": "commonjs",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowJs": false,
+ "outDir": "./",
+ "noEmitOnError": false,
+ },
+ "include": [
+ "./**/*"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}