From 1ca85403491ee6f7e762692453166d228d600a0d Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Tue, 10 Sep 2024 13:20:05 -0400 Subject: [PATCH 01/30] first pass --- libs/model/src/thread/UpdateThread.command.ts | 753 ++++++++++++++++++ libs/model/src/thread/index.ts | 1 + .../test/thread/thread-lifecycle.spec.ts | 62 ++ libs/schemas/src/commands/thread.schemas.ts | 25 + .../scripts/state/api/threads/editThread.ts | 22 +- .../views/modals/ArchiveThreadModal.tsx | 6 +- .../modals/change_thread_topic_modal.tsx | 5 +- .../views/modals/edit_collaborators_modal.tsx | 4 +- .../modals/update_proposal_status_modal.tsx | 8 +- .../AdminActions/AdminActions.tsx | 27 +- .../views/pages/view_thread/edit_body.tsx | 4 +- .../server/api/external-router.ts | 5 +- .../server/api/integration-router.ts | 4 +- packages/commonwealth/server/api/threads.ts | 1 + .../controllers/server_threads_controller.ts | 12 - .../routes/threads/update_thread_handler.ts | 100 --- .../commonwealth/server/routing/router.ts | 10 - .../test/integration/api/threads.spec.ts | 148 ++-- ...r_threads_controller.update_thread.spec.ts | 67 -- 19 files changed, 957 insertions(+), 307 deletions(-) create mode 100644 libs/model/src/thread/UpdateThread.command.ts delete mode 100644 packages/commonwealth/server/routes/threads/update_thread_handler.ts diff --git a/libs/model/src/thread/UpdateThread.command.ts b/libs/model/src/thread/UpdateThread.command.ts new file mode 100644 index 00000000000..03dce16b4a8 --- /dev/null +++ b/libs/model/src/thread/UpdateThread.command.ts @@ -0,0 +1,753 @@ +import { type Command } from '@hicommonwealth/core'; +import * as schemas from '@hicommonwealth/schemas'; +import { models } from '../database'; +import { isAuthorized, type AuthContext } from '../middleware'; +import { mustBeAuthorizedThread } from '../middleware/guards'; + +export const UpdateThreadErrors = { + // ThreadNotFound: 'Thread not found', + // BanError: 'Ban error', + // NoTitle: 'Must provide title', + // NoBody: 'Must provide body', + // InvalidLink: 'Invalid thread URL', + // ParseMentionsFailed: 'Failed to parse mentions', + // Unauthorized: 'Unauthorized', + // InvalidStage: 'Please Select a Stage', + // FailedToParse: 'Failed to parse custom stages', + // InvalidTopic: 'Invalid topic', + // MissingCollaborators: 'Failed to find all provided collaborators', + // CollaboratorsOverlap: + // 'Cannot overlap addresses when adding/removing collaborators', + // ContestLock: 'Cannot edit thread that is in a contest', +}; + +export function UpdateThread(): Command< + typeof schemas.UpdateThread, + AuthContext +> { + return { + ...schemas.UpdateThread, + auth: [ + isAuthorized({ action: schemas.PermissionEnum.CREATE_THREAD }), + //verifyThreadSignature, + ], + body: async ({ actor, payload, auth }) => { + const { thread } = mustBeAuthorizedThread(actor, auth); + + //const body = sanitizeQuillText(payload.body); + //const plaintext = kind === 'discussion' ? quillToPlain(body) : body; + //const mentions = uniqueMentions(parseUserMentions(body)); + + // == mutation transaction boundary == + await models.sequelize.transaction(async (transaction) => {}); + // == end of transaction boundary == + + return thread!.toJSON(); + }, + }; +} + +/* +import { AppError, ServerError } from '@hicommonwealth/core'; +import { + AddressInstance, + CommentAttributes, + CommunityInstance, + DB, + ThreadAttributes, + ThreadInstance, + UserInstance, + emitMentions, + findMentionDiff, + parseUserMentions, +} from '@hicommonwealth/model'; +import { renderQuillDeltaToText } from '@hicommonwealth/shared'; +import _ from 'lodash'; +import { + Op, + QueryTypes, + Sequelize, + Transaction, + WhereOptions, +} from 'sequelize'; +import { MixpanelCommunityInteractionEvent } from '../../../shared/analytics/types'; +import { validURL } from '../../../shared/utils'; +import { findAllRoles } from '../../util/roles'; +import { TrackOptions } from '../server_analytics_controller'; +import { ServerThreadsController } from '../server_threads_controller'; + +export async function __updateThread( + this: ServerThreadsController, + { + user, + address, + threadId, + title, + body, + stage, + url, + locked, + pinned, + archived, + spam, + topicId, + collaborators, + canvasHash, + canvasSignedData, + discordMeta, + }: UpdateThreadOptions, +): Promise { + // Discobot handling + + const threadWhere: WhereOptions = {}; + if (threadId) { + threadWhere.id = threadId; + } + if (discordMeta) { + threadWhere.discord_meta = discordMeta; + } + + const thread = await this.models.Thread.findOne({ + where: threadWhere, + include: { + model: this.models.Address, + as: 'collaborators', + required: false, + }, + }); + + if (!thread) { + throw new AppError(Errors.ThreadNotFound); + } + + // check if thread is part of a contest topic + const contestManagers = await this.models.sequelize.query( + ` + SELECT cm.contest_address FROM "Threads" t + JOIN "ContestTopics" ct on ct.topic_id = t.topic_id + JOIN "ContestManagers" cm on cm.contest_address = ct.contest_address + WHERE t.id = :thread_id + `, + { + type: QueryTypes.SELECT, + replacements: { + thread_id: thread!.id, + }, + }, + ); + const isContestThread = contestManagers.length > 0; + + if (address.is_banned) throw new AppError('Banned User'); + + // get various permissions + const userOwnedAddressIds = (await user.getAddresses()) + .filter((addr) => !!addr.verified) + .map((addr) => addr.id); + const roles = await findAllRoles( + this.models, + // @ts-expect-error StrictNullChecks + { where: { address_id: { [Op.in]: userOwnedAddressIds } } }, + thread.community_id, + ['moderator', 'admin'], + ); + + const isCollaborator = !!thread.collaborators?.find( + (a) => a.address === address.address, + ); + const isThreadOwner = userOwnedAddressIds.includes(thread.address_id); + const isMod = !!roles.find( + (r) => + r.community_id === thread.community_id && r.permission === 'moderator', + ); + const isAdmin = !!roles.find( + (r) => r.community_id === thread.community_id && r.permission === 'admin', + ); + const isSuperAdmin = user.isAdmin ?? false; + if ( + !isThreadOwner && + !isMod && + !isAdmin && + !isSuperAdmin && + !isCollaborator + ) { + throw new AppError(Errors.Unauthorized); + } + + // build analytics + const allAnalyticsOptions: TrackOptions[] = []; + + const community = await this.models.Community.findByPk(thread.community_id); + + // patch thread properties + const transaction = await this.models.sequelize.transaction(); + + try { + const toUpdate: Partial = {}; + + const permissions = { + isThreadOwner, + isMod, + isAdmin, + isSuperAdmin, + isCollaborator, + }; + + await setThreadAttributes( + permissions, + thread, + { + title, + body, + url, + canvasHash, + canvasSignedData, + }, + isContestThread, + toUpdate, + ); + + await setThreadPinned(permissions, pinned, toUpdate); + + await setThreadSpam(permissions, spam, toUpdate); + + await setThreadLocked(permissions, locked, toUpdate); + + await setThreadArchived(permissions, archived, toUpdate); + + await setThreadStage( + permissions, + stage, + community!, + allAnalyticsOptions, + toUpdate, + ); + + await setThreadTopic( + permissions, + community!, + topicId!, + this.models, + isContestThread, + toUpdate, + ); + + await thread.update( + { + ...toUpdate, + last_edited: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + { transaction }, + ); + + const latestVersionHistory = await this.models.ThreadVersionHistory.findOne( + { + where: { + thread_id: thread.id, + }, + order: [['timestamp', 'DESC']], + transaction, + }, + ); + + // if the modification was different from the original body, create a version history for it + if (body && latestVersionHistory?.body !== body) { + await this.models.ThreadVersionHistory.create( + { + thread_id: threadId!, + address: address.address, + body, + timestamp: new Date(), + }, + { + transaction, + }, + ); + } + + await updateThreadCollaborators( + permissions, + thread, + collaborators, + isContestThread, + this.models, + transaction, + ); + + const previousDraftMentions = parseUserMentions(latestVersionHistory?.body); + const currentDraftMentions = parseUserMentions(decodeURIComponent(body)); + + const mentions = findMentionDiff( + previousDraftMentions, + currentDraftMentions, + ); + + await emitMentions(this.models, transaction, { + authorAddressId: address.id!, + authorUserId: user.id!, + authorAddress: address.address, + mentions, + thread, + community_id: thread.community_id, + }); + + await transaction.commit(); + } catch (err) { + console.error(err); + await transaction.rollback(); + if (err instanceof AppError || err instanceof ServerError) { + throw err; + } + throw new ServerError(`transaction failed: ${err.message}`); + } + + // --- + + address + .update({ + last_active: Sequelize.literal('CURRENT_TIMESTAMP'), + }) + .catch(console.error); + + const finalThread = await this.models.Thread.findOne({ + where: { id: thread.id }, + include: [ + { + model: this.models.Address, + as: 'Address', + include: [ + { + model: this.models.User, + as: 'User', + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + { + model: this.models.Address, + as: 'collaborators', + include: [ + { + model: this.models.User, + as: 'User', + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + { model: this.models.Topic, as: 'topic' }, + { + model: this.models.Reaction, + as: 'reactions', + include: [ + { + model: this.models.Address, + as: 'Address', + required: true, + include: [ + { + model: this.models.User, + as: 'User', + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + ], + }, + { + model: this.models.Comment, + limit: 3, // This could me made configurable, atm we are using 3 recent comments with threads in frontend. + order: [['created_at', 'DESC']], + attributes: [ + 'id', + 'address_id', + 'text', + ['plaintext', 'plainText'], + 'created_at', + 'updated_at', + 'deleted_at', + 'marked_as_spam_at', + 'discord_meta', + ], + include: [ + { + model: this.models.Address, + attributes: ['address'], + include: [ + { + model: this.models.User, + attributes: ['profile'], + }, + ], + }, + ], + }, + { + model: this.models.ThreadVersionHistory, + }, + ], + }); + + const updatedThreadWithComments = { + // @ts-expect-error StrictNullChecks + ...finalThread.toJSON(), + } as ThreadAttributes & { + Comments?: CommentAttributes[]; + recentComments?: CommentAttributes[]; + }; + updatedThreadWithComments.recentComments = ( + updatedThreadWithComments.Comments || [] + ).map((c) => { + const temp = { + ...c, + address: c?.Address?.address || '', + }; + + if (temp.Address) delete temp.Address; + + return temp; + }); + + delete updatedThreadWithComments.Comments; + + return [updatedThreadWithComments, allAnalyticsOptions]; +} + +// ----- + +export type UpdateThreadPermissions = { + isThreadOwner: boolean; + isMod: boolean; + isAdmin: boolean; + isSuperAdmin: boolean; + isCollaborator: boolean; +}; + + +export function validatePermissions( + permissions: UpdateThreadPermissions, + flags: Partial, +) { + const keys = [ + 'isThreadOwner', + 'isMod', + 'isAdmin', + 'isSuperAdmin', + 'isCollaborator', + ]; + for (const k of keys) { + if (flags[k] && permissions[k]) { + // at least one flag is satisfied + return; + } + } + // no flags were satisfied + throw new AppError(Errors.Unauthorized); +} + +export type UpdatableThreadAttributes = { + title?: string; + body?: string; + url?: string; + canvasSignedData?: string; + canvasHash?: string; +}; + +async function setThreadAttributes( + permissions: UpdateThreadPermissions, + thread: ThreadInstance, + { title, body, url, canvasSignedData, canvasHash }: UpdatableThreadAttributes, + isContestThread: boolean, + toUpdate: Partial, +) { + if ( + typeof title !== 'undefined' || + typeof body !== 'undefined' || + typeof url !== 'undefined' + ) { + if (isContestThread) { + throw new AppError(Errors.ContestLock); + } + validatePermissions(permissions, { + isThreadOwner: true, + isMod: true, + isAdmin: true, + isSuperAdmin: true, + isCollaborator: true, + }); + + // title + if (typeof title !== 'undefined') { + if (!title) { + throw new AppError(Errors.NoTitle); + } + toUpdate.title = title; + } + + // body + if (typeof body !== 'undefined') { + if (thread.kind === 'discussion' && (!body || !body.trim())) { + throw new AppError(Errors.NoBody); + } + toUpdate.body = body; + toUpdate.plaintext = (() => { + try { + return renderQuillDeltaToText(JSON.parse(decodeURIComponent(body))); + } catch (e) { + return decodeURIComponent(body); + } + })(); + } + + // url + if (typeof url !== 'undefined' && thread.kind === 'link') { + if (!validURL(url)) { + throw new AppError(Errors.InvalidLink); + } + toUpdate.url = url; + } + + toUpdate.canvas_signed_data = canvasSignedData; + toUpdate.canvas_hash = canvasHash; + } +} + + +async function setThreadPinned( + permissions: UpdateThreadPermissions, + pinned: boolean | undefined, + toUpdate: Partial, +) { + if (typeof pinned !== 'undefined') { + validatePermissions(permissions, { + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + + toUpdate.pinned = pinned; + } +} + + +async function setThreadLocked( + permissions: UpdateThreadPermissions, + locked: boolean | undefined, + toUpdate: Partial, +) { + if (typeof locked !== 'undefined') { + validatePermissions(permissions, { + isThreadOwner: true, + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + + toUpdate.read_only = locked; + toUpdate.locked_at = locked + ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) + : null; + } +} + + +async function setThreadArchived( + permissions: UpdateThreadPermissions, + archive: boolean | undefined, + toUpdate: Partial, +) { + if (typeof archive !== 'undefined') { + validatePermissions(permissions, { + isThreadOwner: true, + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + + toUpdate.archived_at = archive + ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) + : null; + } +} + + +async function setThreadSpam( + permissions: UpdateThreadPermissions, + spam: boolean | undefined, + toUpdate: Partial, +) { + if (typeof spam !== 'undefined') { + validatePermissions(permissions, { + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + + toUpdate.marked_as_spam_at = spam + ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) + : null; + } +} + + +async function setThreadStage( + permissions: UpdateThreadPermissions, + stage: string | undefined, + community: CommunityInstance, + allAnalyticsOptions: TrackOptions[], + toUpdate: Partial, +) { + if (typeof stage !== 'undefined') { + validatePermissions(permissions, { + isThreadOwner: true, + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + + // fetch available stages + let customStages = []; + try { + const communityStages = community.custom_stages; + if (Array.isArray(communityStages)) { + customStages = Array.from(communityStages) + .map((s) => s.toString()) + .filter((s) => s); + } + if (customStages.length === 0) { + customStages = [ + 'discussion', + 'proposal_in_review', + 'voting', + 'passed', + 'failed', + ]; + } + } catch (e) { + throw new AppError(Errors.FailedToParse); + } + + // validate stage + if (!customStages.includes(stage)) { + throw new AppError(Errors.InvalidStage); + } + + toUpdate.stage = stage; + + allAnalyticsOptions.push({ + event: MixpanelCommunityInteractionEvent.UPDATE_STAGE, + }); + } +} + + +async function setThreadTopic( + permissions: UpdateThreadPermissions, + community: CommunityInstance, + topicId: number, + models: DB, + isContestThread: boolean, + toUpdate: Partial, +) { + if (typeof topicId !== 'undefined') { + if (isContestThread) { + throw new AppError(Errors.ContestLock); + } + validatePermissions(permissions, { + isThreadOwner: true, + isMod: true, + isAdmin: true, + isSuperAdmin: true, + }); + const topic = await models.Topic.findOne({ + where: { + id: topicId, + community_id: community.id, + }, + }); + + if (!topic) { + throw new AppError(Errors.InvalidTopic); + } + toUpdate.topic_id = topic.id; + } +} + + +async function updateThreadCollaborators( + permissions: UpdateThreadPermissions, + thread: ThreadInstance, + collaborators: + | { + toAdd?: number[]; + toRemove?: number[]; + } + | undefined, + isContestThread: boolean, + models: DB, + transaction: Transaction, +) { + const { toAdd, toRemove } = collaborators || {}; + if (Array.isArray(toAdd) || Array.isArray(toRemove)) { + if (isContestThread) { + throw new AppError(Errors.ContestLock); + } + + validatePermissions(permissions, { + isThreadOwner: true, + isSuperAdmin: true, + }); + + const toAddUnique = _.uniq(toAdd || []); + const toRemoveUnique = _.uniq(toRemove || []); + + // check for overlap between toAdd and toRemove + for (const r of toRemoveUnique) { + if (toAddUnique.includes(r)) { + throw new AppError(Errors.CollaboratorsOverlap); + } + } + + // add collaborators + if (toAddUnique.length > 0) { + const collaboratorAddresses = await models.Address.findAll({ + where: { + community_id: thread.community_id, + id: { + [Op.in]: toAddUnique, + }, + }, + }); + if (collaboratorAddresses.length !== toAddUnique.length) { + throw new AppError(Errors.MissingCollaborators); + } + await Promise.all( + collaboratorAddresses.map(async (address) => { + return models.Collaboration.findOrCreate({ + where: { + thread_id: thread.id, + address_id: address.id, + }, + transaction, + }); + }), + ); + } + + // remove collaborators + if (toRemoveUnique.length > 0) { + await models.Collaboration.destroy({ + where: { + thread_id: thread.id, + address_id: { + [Op.in]: toRemoveUnique, + }, + }, + transaction, + }); + } + } +} + +*/ diff --git a/libs/model/src/thread/index.ts b/libs/model/src/thread/index.ts index ff12bc1eaa2..7fbd5666ef7 100644 --- a/libs/model/src/thread/index.ts +++ b/libs/model/src/thread/index.ts @@ -1,3 +1,4 @@ export * from './CreateThread.command'; export * from './CreateThreadReaction.command'; export * from './GetThread.query'; +export * from './UpdateThread.command'; diff --git a/libs/model/test/thread/thread-lifecycle.spec.ts b/libs/model/test/thread/thread-lifecycle.spec.ts index 11a50ab1417..ce82375e314 100644 --- a/libs/model/test/thread/thread-lifecycle.spec.ts +++ b/libs/model/test/thread/thread-lifecycle.spec.ts @@ -205,6 +205,68 @@ describe('Thread lifecycle', () => { } }); + describe('updates', () => { + it('should patch update thread attributes', async () => { + // const address = { + // id: 1, + // address: '0x1234', + // role: 'admin', + // community_id: 'ethereum', + // verified: true, + // update: async () => ({}), + // }; + // const attributes: UpdateThreadOptions = { + // user: { + // getAddresses: async () => [address], + // } as any, + // address: address as any, + // threadId: 1, + // title: 'hello', + // body: 'wasup', + // url: 'https://example.com', + // }; + // const db: any = { + // Thread: { + // findOne: async () => ({ + // Address: address, + // address_id: address.id, + // update: () => ({ id: 1, created_at: Date.now() }), + // toJSON: () => ({}), + // }), + // update: () => ({ id: 1, created_at: Date.now() }), + // }, + // ThreadVersionHistory: { + // create: () => null, + // findOne: () => null, + // }, + // Topic: { + // findOne: async () => ({ + // id: 1, + // }), + // }, + // // for findAllRoles + // Address: { + // findAll: async () => [address], + // }, + // Community: { + // findByPk: async () => ({ id: 'ethereum' }), + // }, + // sequelize: { + // transaction: async () => ({ + // rollback: async () => ({}), + // commit: async () => ({}), + // }), + // query: () => new Promise((resolve) => resolve([])), + // }, + // }; + // const serverThreadsController = new ServerThreadsController(db); + // const [updatedThread, analyticsOptions] = + // await serverThreadsController.updateThread(attributes); + // expect(updatedThread).to.be.ok; + // expect(analyticsOptions).to.have.length(0); + }); + }); + describe('comments', () => { it('should create a thread comment as member of group with permissions', async () => { const text = 'hello'; diff --git a/libs/schemas/src/commands/thread.schemas.ts b/libs/schemas/src/commands/thread.schemas.ts index 589514b5497..2d762986c93 100644 --- a/libs/schemas/src/commands/thread.schemas.ts +++ b/libs/schemas/src/commands/thread.schemas.ts @@ -22,6 +22,31 @@ export const CreateThread = { output: Thread, }; +export const UpdateThread = { + input: z.object({ + thread_id: PG_INT, + body: z.string().optional(), + title: z.string().optional(), + topic_id: PG_INT.optional(), + stage: z.string().optional(), + url: z.string().url().optional(), + locked: z.boolean().optional(), + pinned: z.boolean().optional(), + archived: z.boolean().optional(), + spam: z.boolean().optional(), + collaborators: z + .object({ + toAdd: z.array(PG_INT).optional(), + toRemove: z.array(PG_INT).optional(), + }) + .optional(), + canvas_signed_data: z.string().optional(), + canvas_hash: z.string().optional(), + discord_meta: DiscordMetaSchema.optional(), + }), + output: Thread, +}; + export const ThreadCanvasReaction = z.object({ thread_id: PG_INT, reaction: z.enum(['like']), diff --git a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts index ec41143be8f..d84cd7c12e2 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts @@ -1,12 +1,9 @@ import { toCanvasSignedDataApiArgs } from '@hicommonwealth/shared'; -import { useMutation } from '@tanstack/react-query'; -import axios from 'axios'; import { signThread } from 'controllers/server/sessions'; import MinimumProfile from 'models/MinimumProfile'; import Thread from 'models/Thread'; import { ThreadStage } from 'models/types'; import app from 'state'; -import { SERVER_URL } from 'state/api/config'; import { trpc } from 'utils/trpcClient'; import { useAuthModalStore } from '../../ui/modals'; import { userStore } from '../../ui/user'; @@ -44,7 +41,7 @@ interface EditThreadProps { }; } -const editThread = async ({ +export const buildUpdateThreadInput = async ({ address, communityId, threadId, @@ -67,7 +64,7 @@ const editThread = async ({ topicId, // for editing thread collaborators collaborators, -}: EditThreadProps): Promise => { +}: EditThreadProps) => { const canvasSignedData = await signThread(address, { community: app.activeChainId(), title: newTitle, @@ -75,12 +72,12 @@ const editThread = async ({ link: url, topic: topicId, }); - - const response = await axios.patch(`${SERVER_URL}/threads/${threadId}`, { + return { // common payload author_community_id: communityId, address: address, community_id: communityId, + thread_id: threadId, jwt: userStore.getState().jwt, // for edit profile ...(url && { url }), @@ -102,9 +99,7 @@ const editThread = async ({ // for editing thread collaborators ...(collaborators !== undefined && { collaborators }), ...toCanvasSignedDataApiArgs(canvasSignedData), - }); - - return new Thread(response.data.result); + }; }; interface UseEditThreadMutationProps { @@ -123,9 +118,10 @@ const useEditThreadMutation = ({ const utils = trpc.useUtils(); const { checkForSessionKeyRevalidationErrors } = useAuthModalStore(); - return useMutation({ - mutationFn: editThread, - onSuccess: async (updatedThread) => { + return trpc.thread.updateThread.useMutation({ + onSuccess: async (updated) => { + // @ts-expect-error StrictNullChecks + const updatedThread = new Thread(updated); // Update community level thread counters variables if (currentStage !== updatedThread.stage) { updateThreadCountsByStageChange( diff --git a/packages/commonwealth/client/scripts/views/modals/ArchiveThreadModal.tsx b/packages/commonwealth/client/scripts/views/modals/ArchiveThreadModal.tsx index 778453a0d31..0e31bb03947 100644 --- a/packages/commonwealth/client/scripts/views/modals/ArchiveThreadModal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/ArchiveThreadModal.tsx @@ -6,6 +6,7 @@ import type Thread from '../../models/Thread'; import { CWText } from '../components/component_kit/cw_text'; import { CWButton } from '../components/component_kit/new_designs/CWButton'; +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import useUserStore from 'state/ui/user'; import { notifyError, @@ -30,13 +31,14 @@ export const ArchiveThreadModal = ({ const user = useUserStore(); const handleArchiveThread = async () => { - editThread({ + const input = await buildUpdateThreadInput({ threadId: thread.id, communityId: app.activeChainId(), archived: !thread.archivedAt, address: user.activeAccount?.address || '', pinned: false, - }) + }); + editThread(input) .then(() => { notifySuccess( `Thread has been ${thread?.archivedAt ? 'unarchived' : 'archived'}!`, diff --git a/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx b/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx index ca947068523..718597fe1d8 100644 --- a/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import useUserStore from 'state/ui/user'; import type Thread from '../../models/Thread'; import type Topic from '../../models/Topic'; @@ -47,13 +48,13 @@ export const ChangeThreadTopicModal = ({ const handleSaveChanges = async () => { try { - await editThread({ + const input = await buildUpdateThreadInput({ communityId: app.activeChainId(), address: user.activeAccount?.address || '', threadId: thread.id, topicId: activeTopic.id, }); - + await editThread(input); onModalClose && onModalClose(); } catch (err) { const error = diff --git a/packages/commonwealth/client/scripts/views/modals/edit_collaborators_modal.tsx b/packages/commonwealth/client/scripts/views/modals/edit_collaborators_modal.tsx index c199c687171..f2e6f941e8b 100644 --- a/packages/commonwealth/client/scripts/views/modals/edit_collaborators_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/edit_collaborators_modal.tsx @@ -22,6 +22,7 @@ import { } from '../components/component_kit/new_designs/CWModal'; import { User } from '../components/user/user'; +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import useUserStore from 'state/ui/user'; import '../../../styles/modals/edit_collaborators_modal.scss'; @@ -177,7 +178,7 @@ export const EditCollaboratorsModal = ({ removedCollaborators.length > 0 ) { try { - const updatedThread = await editThread({ + const input = await buildUpdateThreadInput({ threadId: thread.id, communityId: app.activeChainId(), address: user.activeAccount?.address || '', @@ -190,6 +191,7 @@ export const EditCollaboratorsModal = ({ }), }, }); + const updatedThread = await editThread(input); notifySuccess('Collaborators updated'); onCollaboratorsUpdated && // @ts-expect-error diff --git a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx index 3dce50224b9..942e50fbca0 100644 --- a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx @@ -8,6 +8,7 @@ import { parseCustomStages, threadStageToLabel } from '../../helpers'; import type Thread from '../../models/Thread'; import { ChainBase } from '@hicommonwealth/shared'; +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import { notifyError } from 'controllers/app/notifications'; import { CosmosProposal } from 'controllers/chain/cosmos/gov/v1beta1/proposal-v1beta1'; import { filterLinks, getAddedAndDeleted } from 'helpers/threads'; @@ -108,15 +109,16 @@ export const UpdateProposalStatusModal = ({ onAction: true, }); - const handleSaveChanges = () => { + const handleSaveChanges = async () => { // set stage - editThread({ + const input = await buildUpdateThreadInput({ address: user.activeAccount?.address || '', communityId: app.activeChainId(), threadId: thread.id, // @ts-expect-error stage: tempStage, - }) + }); + editThread(input) .then(() => { let links = thread.links; const { toAdd, toDelete } = getAddedAndDeleted( diff --git a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx index fa999505a6a..7e6ca369ea7 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx @@ -1,3 +1,4 @@ +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import { SessionKeyError } from 'controllers/server/sessions'; import { useCommonNavigate } from 'navigation/helpers'; import React, { useState } from 'react'; @@ -171,12 +172,13 @@ export const AdminActions = ({ onClick: async () => { const isSpam = !thread.markedAsSpamAt; try { - await editThread({ + const input = await buildUpdateThreadInput({ communityId: app.activeChainId(), threadId: thread.id, spam: isSpam, address: user.activeAccount?.address || '', - }) + }); + await editThread(input) .then((t: Thread | any) => onSpamToggle && onSpamToggle(t)) .catch(() => { notifyError( @@ -192,13 +194,14 @@ export const AdminActions = ({ }); }; - const handleThreadLockToggle = () => { - editThread({ + const handleThreadLockToggle = async () => { + const input = await buildUpdateThreadInput({ address: user.activeAccount?.address || '', threadId: thread.id, readOnly: !thread.readOnly, communityId: app.activeChainId(), - }) + }); + editThread(input) .then(() => { notifySuccess(thread?.readOnly ? 'Unlocked!' : 'Locked!'); onLockToggle?.(!thread?.readOnly); @@ -209,13 +212,14 @@ export const AdminActions = ({ }); }; - const handleThreadPinToggle = () => { - editThread({ + const handleThreadPinToggle = async () => { + const input = await buildUpdateThreadInput({ address: user.activeAccount?.address || '', threadId: thread.id, communityId: app.activeChainId(), pinned: !thread.pinned, - }) + }); + editThread(input) .then(() => { notifySuccess(thread?.pinned ? 'Unpinned!' : 'Pinned!'); onPinToggle?.(!thread.pinned); @@ -264,16 +268,17 @@ export const AdminActions = ({ ); }; - const handleArchiveThread = () => { + const handleArchiveThread = async () => { if (thread.archivedAt === null) { setIsArchiveThreadModalOpen(true); } else { - editThread({ + const input = await buildUpdateThreadInput({ threadId: thread.id, communityId: app.activeChainId(), archived: !thread.archivedAt, address: user.activeAccount?.address || '', - }) + }); + editThread(input) .then(() => { notifySuccess( `Thread has been ${ diff --git a/packages/commonwealth/client/scripts/views/pages/view_thread/edit_body.tsx b/packages/commonwealth/client/scripts/views/pages/view_thread/edit_body.tsx index e189964cd28..c60d47ebd42 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_thread/edit_body.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_thread/edit_body.tsx @@ -1,4 +1,5 @@ import { ContentType } from '@hicommonwealth/shared'; +import { buildUpdateThreadInput } from 'client/scripts/state/api/threads/editThread'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import { SessionKeyError } from 'controllers/server/sessions'; import 'pages/view_thread/edit_body.scss'; @@ -92,7 +93,7 @@ export const EditBody = (props: EditBodyProps) => { const asyncHandle = async () => { try { const newBody = JSON.stringify(contentDelta); - await editThread({ + const input = await buildUpdateThreadInput({ newBody: JSON.stringify(contentDelta) || thread.body, newTitle: title || thread.title, threadId: thread.id, @@ -100,6 +101,7 @@ export const EditBody = (props: EditBodyProps) => { address: user.activeAccount?.address || '', communityId: app.activeChainId(), }); + await editThread(input); clearEditingLocalStorage(thread.id, ContentType.Thread); notifySuccess('Thread successfully edited'); threadUpdatedCallback(title, newBody); diff --git a/packages/commonwealth/server/api/external-router.ts b/packages/commonwealth/server/api/external-router.ts index 5d5e561e658..a63c4b66ffa 100644 --- a/packages/commonwealth/server/api/external-router.ts +++ b/packages/commonwealth/server/api/external-router.ts @@ -14,10 +14,9 @@ const { getCommunity, getMembers, } = community.trpcRouter; -const { createThread, createThreadReaction } = thread.trpcRouter; +const { createThread, updateThread, createThreadReaction } = thread.trpcRouter; const { createComment, createCommentReaction, updateComment, getComments } = comment.trpcRouter; -//const { getBulkThreads } = thread.trpcRouter; const api = { createCommunity, @@ -27,8 +26,8 @@ const api = { getMembers, getComments, createThread, + updateThread, createThreadReaction, - //getBulkThreads, createComment, updateComment, createCommentReaction, diff --git a/packages/commonwealth/server/api/integration-router.ts b/packages/commonwealth/server/api/integration-router.ts index 32d697da258..595a75b1540 100644 --- a/packages/commonwealth/server/api/integration-router.ts +++ b/packages/commonwealth/server/api/integration-router.ts @@ -6,7 +6,6 @@ import { RequestHandler, Router, raw } from 'express'; import DatabaseValidationService from 'server/middleware/databaseValidationService'; import { deleteBotCommentHandler } from 'server/routes/comments/delete_comment_bot_handler'; import { deleteBotThreadHandler } from 'server/routes/threads/delete_thread_bot_handler'; -import { updateThreadHandler } from 'server/routes/threads/update_thread_handler'; import { ServerControllers } from 'server/routing/router'; const PATH = '/api/integration'; @@ -52,8 +51,7 @@ function build( router.patch( '/bot/threads', isBotUser, - isAuthor, - updateThreadHandler.bind(this, controllers), + express.command(Thread.UpdateThread()), ); router.delete( diff --git a/packages/commonwealth/server/api/threads.ts b/packages/commonwealth/server/api/threads.ts index 4c33dc5d8ed..3973e0e25f8 100644 --- a/packages/commonwealth/server/api/threads.ts +++ b/packages/commonwealth/server/api/threads.ts @@ -7,6 +7,7 @@ export const trpcRouter = trpc.router({ MixpanelCommunityInteractionEvent.CREATE_THREAD, ({ community_id }) => ({ community: community_id }), ]), + updateThread: trpc.command(Thread.UpdateThread, trpc.Tag.Thread), createThreadReaction: trpc.command( Thread.CreateThreadReaction, trpc.Tag.Thread, diff --git a/packages/commonwealth/server/controllers/server_threads_controller.ts b/packages/commonwealth/server/controllers/server_threads_controller.ts index eae316c81dc..c749513323f 100644 --- a/packages/commonwealth/server/controllers/server_threads_controller.ts +++ b/packages/commonwealth/server/controllers/server_threads_controller.ts @@ -39,11 +39,6 @@ import { SearchThreadsResult, __searchThreads, } from './server_threads_methods/search_threads'; -import { - UpdateThreadOptions, - UpdateThreadResult, - __updateThread, -} from './server_threads_methods/update_thread'; /** * Implements methods related to threads @@ -60,13 +55,6 @@ export class ServerThreadsController { return __deleteThread.call(this, options); } - async updateThread( - this: ServerThreadsController, - options: UpdateThreadOptions, - ): Promise { - return __updateThread.call(this, options); - } - async getThreadsByIds( this: ServerThreadsController, options: GetThreadsByIdOptions, diff --git a/packages/commonwealth/server/routes/threads/update_thread_handler.ts b/packages/commonwealth/server/routes/threads/update_thread_handler.ts deleted file mode 100644 index 4e8bcdf3f02..00000000000 --- a/packages/commonwealth/server/routes/threads/update_thread_handler.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { IDiscordMeta, ThreadAttributes } from '@hicommonwealth/model'; -import { ServerControllers } from '../../routing/router'; -import { TypedRequest, TypedResponse, success } from '../../types'; - -export const Errors = { - InvalidThreadID: 'Invalid thread ID', - MissingText: 'Must provide text', -}; - -type UpdateThreadRequestBody = { - title?: string; - body: string; - stage?: string; - url?: string; - locked?: boolean; - pinned?: boolean; - archived?: boolean; - spam?: boolean; - topicId?: number; - collaborators?: { - toAdd?: number[]; - toRemove?: number[]; - }; - canvasSignedData?: string; - canvasHash?: string; - discord_meta?: IDiscordMeta; // Only comes from the discord bot -}; -type UpdateThreadResponse = ThreadAttributes; - -export const updateThreadHandler = async ( - controllers: ServerControllers, - // @ts-expect-error StrictNullChecks - req: TypedRequest, - res: TypedResponse, -) => { - const { user, address } = req; - // @ts-expect-error StrictNullChecks - const { id } = req.params; - const { - // @ts-expect-error StrictNullChecks - title, - // @ts-expect-error StrictNullChecks - body, - // @ts-expect-error StrictNullChecks - stage, - // @ts-expect-error StrictNullChecks - url, - // @ts-expect-error StrictNullChecks - locked, - // @ts-expect-error StrictNullChecks - pinned, - // @ts-expect-error StrictNullChecks - archived, - // @ts-expect-error StrictNullChecks - spam, - // @ts-expect-error StrictNullChecks - topicId, - // @ts-expect-error StrictNullChecks - collaborators, - // @ts-expect-error StrictNullChecks - canvasSignedData, - // @ts-expect-error StrictNullChecks - canvasHash, - // @ts-expect-error StrictNullChecks - discord_meta: discordMeta, - } = req.body; - - const threadId = parseInt(id, 10) || null; - - // this is a patch update, so properties should be - // `undefined` if they are not intended to be updated - const [updatedThread, analyticsOptions] = - await controllers.threads.updateThread({ - // @ts-expect-error StrictNullChecks - user, - // @ts-expect-error StrictNullChecks - address, - // @ts-expect-error StrictNullChecks - threadId, - title, - body, - stage, - url, - locked, - pinned, - archived, - spam, - topicId, - collaborators, - canvasSignedData, - canvasHash, - discordMeta, - }); - - for (const a of analyticsOptions) { - controllers.analytics.track(a).catch(console.error); - } - - return success(res, updatedThread); -}; diff --git a/packages/commonwealth/server/routing/router.ts b/packages/commonwealth/server/routing/router.ts index 650fdd515ed..0509d9c8461 100644 --- a/packages/commonwealth/server/routing/router.ts +++ b/packages/commonwealth/server/routing/router.ts @@ -117,7 +117,6 @@ import { createThreadPollHandler } from '../routes/threads/create_thread_poll_ha import { deleteThreadHandler } from '../routes/threads/delete_thread_handler'; import { getThreadPollsHandler } from '../routes/threads/get_thread_polls_handler'; import { getThreadsHandler } from '../routes/threads/get_threads_handler'; -import { updateThreadHandler } from '../routes/threads/update_thread_handler'; import { createTopicHandler } from '../routes/topics/create_topic_handler'; import { deleteTopicHandler } from '../routes/topics/delete_topic_handler'; import { getTopicsHandler } from '../routes/topics/get_topics_handler'; @@ -345,15 +344,6 @@ function setupRouter( getTopUsersHandler.bind(this, serverControllers), ); - registerRoute( - router, - 'patch', - '/threads/:id', - passport.authenticate('jwt', { session: false }), - databaseValidationService.validateAuthor, - updateThreadHandler.bind(this, serverControllers), - ); - // polls registerRoute( router, diff --git a/packages/commonwealth/test/integration/api/threads.spec.ts b/packages/commonwealth/test/integration/api/threads.spec.ts index 6a64d371d9d..6750fa80259 100644 --- a/packages/commonwealth/test/integration/api/threads.spec.ts +++ b/packages/commonwealth/test/integration/api/threads.spec.ts @@ -7,9 +7,9 @@ import { dispose } from '@hicommonwealth/core'; import { Comment, Thread } from '@hicommonwealth/model'; import chai from 'chai'; import chaiHttp from 'chai-http'; +import { Express } from 'express'; import jwt from 'jsonwebtoken'; import { Errors as EditThreadErrors } from 'server/controllers/server_threads_methods/update_thread'; -import { Errors as EditThreadHandlerErrors } from 'server/routes/threads/update_thread_handler'; import { Errors as ViewCountErrors } from 'server/routes/viewCount'; import sleep from 'sleep-promise'; import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; @@ -20,6 +20,19 @@ import { markdownComment } from '../../util/fixtures/markdownComment'; chai.use(chaiHttp); const { expect } = chai; +async function update( + app: Express, + address: string, + payload: Record, +) { + return await chai + .request(app) + .post(`/api/v1/UpdateThread`) + .set('Accept', 'application/json') + .set('address', address) + .send(payload); +} + // @TODO 1/10/24: was not running previously, so set to skip -- needs cleaning describe.skip('Thread Tests', () => { const chain = 'ethereum'; @@ -487,20 +500,16 @@ describe.skip('Thread Tests', () => { const thread_kind = thread.kind; const thread_stage = thread.stage; const readOnly = false; - const res = await chai - .request(server.app) - .put('/api/editThread') - .set('Accept', 'application/json') - .send({ - chain, - address: adminAddress, - author_chain: chain, - kind: thread_kind, - stage: thread_stage, - body: thread.body, - read_only: readOnly, - jwt: userJWT, - }); + const res = await update(server.app, userAddress, { + chain, + address: adminAddress, + author_chain: chain, + kind: thread_kind, + stage: thread_stage, + body: thread.body, + read_only: readOnly, + jwt: userJWT, + }); expect(res.body.error).to.not.be.null; expect(res.status).to.be.equal(400); }); @@ -509,26 +518,19 @@ describe.skip('Thread Tests', () => { const thread_kind = thread.kind; const thread_stage = thread.stage; const readOnly = false; - const res = await chai - .request(server.app) - .put('/api/editThread') - .set('Accept', 'application/json') - .send({ - chain, - address: adminAddress, - author_chain: chain, - thread_id: null, - kind: thread_kind, - stage: thread_stage, - body: thread.body, - read_only: readOnly, - jwt: adminJWT, - }); + const res = await update(server.app, userAddress, { + chain, + address: adminAddress, + author_chain: chain, + thread_id: null, + kind: thread_kind, + stage: thread_stage, + body: thread.body, + read_only: readOnly, + jwt: adminJWT, + }); expect(res.body.error).to.not.be.null; expect(res.status).to.be.equal(400); - expect(res.body.error).to.be.equal( - EditThreadHandlerErrors.InvalidThreadID, - ); }); test('should fail to edit a thread without passing a body', async () => { @@ -536,21 +538,17 @@ describe.skip('Thread Tests', () => { const thread_kind = thread.kind; const thread_stage = thread.stage; const readOnly = false; - const res = await chai - .request(server.app) - .put('/api/editThread') - .set('Accept', 'application/json') - .send({ - chain, - address: adminAddress, - author_chain: chain, - thread_id, - kind: thread_kind, - stage: thread_stage, - body: null, - read_only: readOnly, - jwt: adminJWT, - }); + const res = await update(server.app, userAddress, { + chain, + address: adminAddress, + author_chain: chain, + thread_id, + kind: thread_kind, + stage: thread_stage, + body: null, + read_only: readOnly, + jwt: adminJWT, + }); expect(res.body.error).to.not.be.null; expect(res.status).to.be.equal(400); expect(res.body.error).to.be.equal(EditThreadErrors.NoBody); @@ -565,21 +563,17 @@ describe.skip('Thread Tests', () => { const thread_stage = thread.stage; const newBody = 'new Body'; const readOnly = false; - const res = await chai.request - .agent(server.app) - .put('/api/editThread') - .set('Accept', 'application/json') - .send({ - chain, - address: adminAddress, - author_chain: chain, - thread_id, - kind: thread_kind, - stage: thread_stage, - body: newBody, - read_only: readOnly, - jwt: adminJWT, - }); + const res = await update(server.app, userAddress, { + chain, + address: adminAddress, + author_chain: chain, + thread_id, + kind: thread_kind, + stage: thread_stage, + body: newBody, + read_only: readOnly, + jwt: adminJWT, + }); expect(res.status).to.be.equal(200); expect(res.body.result.body).to.be.equal(newBody); }, @@ -591,22 +585,18 @@ describe.skip('Thread Tests', () => { const thread_stage = thread.stage; const newTitle = 'new Title'; const readOnly = false; - const res = await chai - .request(server.app) - .put('/api/editThread') - .set('Accept', 'application/json') - .send({ - chain, - address: adminAddress, - author_chain: chain, - thread_id, - kind: thread_kind, - stage: thread_stage, - body: thread.body, - title: newTitle, - read_only: readOnly, - jwt: adminJWT, - }); + const res = await update(server.app, userAddress, { + chain, + address: adminAddress, + author_chain: chain, + thread_id, + kind: thread_kind, + stage: thread_stage, + body: thread.body, + title: newTitle, + read_only: readOnly, + jwt: adminJWT, + }); expect(res.status).to.be.equal(200); expect(res.body.result.title).to.be.equal(newTitle); }); diff --git a/packages/commonwealth/test/unit/server_controllers/server_threads_controller.update_thread.spec.ts b/packages/commonwealth/test/unit/server_controllers/server_threads_controller.update_thread.spec.ts index 9665ddef75e..dd23f90702b 100644 --- a/packages/commonwealth/test/unit/server_controllers/server_threads_controller.update_thread.spec.ts +++ b/packages/commonwealth/test/unit/server_controllers/server_threads_controller.update_thread.spec.ts @@ -1,7 +1,5 @@ import { expect } from 'chai'; -import { ServerThreadsController } from 'server/controllers/server_threads_controller'; import { - UpdateThreadOptions, UpdateThreadPermissions, validatePermissions, } from 'server/controllers/server_threads_methods/update_thread'; @@ -75,69 +73,4 @@ describe('ServerThreadsController', () => { ).to.not.throw(); }); }); - - describe('#updateThread', () => { - test('should patch update thread attributes', async () => { - const address = { - id: 1, - address: '0x1234', - role: 'admin', - community_id: 'ethereum', - verified: true, - update: async () => ({}), - }; - const attributes: UpdateThreadOptions = { - user: { - getAddresses: async () => [address], - } as any, - address: address as any, - threadId: 1, - title: 'hello', - body: 'wasup', - url: 'https://example.com', - }; - - const db: any = { - Thread: { - findOne: async () => ({ - Address: address, - address_id: address.id, - update: () => ({ id: 1, created_at: Date.now() }), - toJSON: () => ({}), - }), - update: () => ({ id: 1, created_at: Date.now() }), - }, - ThreadVersionHistory: { - create: () => null, - findOne: () => null, - }, - Topic: { - findOne: async () => ({ - id: 1, - }), - }, - // for findAllRoles - Address: { - findAll: async () => [address], - }, - Community: { - findByPk: async () => ({ id: 'ethereum' }), - }, - sequelize: { - transaction: async () => ({ - rollback: async () => ({}), - commit: async () => ({}), - }), - query: () => new Promise((resolve) => resolve([])), - }, - }; - - const serverThreadsController = new ServerThreadsController(db); - const [updatedThread, analyticsOptions] = - await serverThreadsController.updateThread(attributes); - - expect(updatedThread).to.be.ok; - expect(analyticsOptions).to.have.length(0); - }); - }); }); From a04bb3df00365831a31f3889b4492ce85eb252a0 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Tue, 10 Sep 2024 17:24:27 -0400 Subject: [PATCH 02/30] second pass --- libs/adapters/src/trpc/handlers.ts | 2 +- libs/model/src/middleware/authorization.ts | 19 +- libs/model/src/models/collaboration.ts | 9 +- libs/model/src/thread/UpdateThread.command.ts | 1014 ++++++----------- .../test/thread/thread-lifecycle.spec.ts | 71 +- .../scripts/state/api/threads/editThread.ts | 2 +- .../modals/change_thread_topic_modal.tsx | 2 +- packages/commonwealth/server/api/threads.ts | 8 +- 8 files changed, 368 insertions(+), 759 deletions(-) diff --git a/libs/adapters/src/trpc/handlers.ts b/libs/adapters/src/trpc/handlers.ts index ef87cf00778..f4f9c18c7bd 100644 --- a/libs/adapters/src/trpc/handlers.ts +++ b/libs/adapters/src/trpc/handlers.ts @@ -41,7 +41,7 @@ const trpcerror = (error: unknown): TRPCError => { * Builds tRPC command POST endpoint * @param factory command factory * @param tag command tag used for OpenAPI spec grouping - * @param track analytics tracking metadata as tuple of [event, output mapper] + * @param track analytics tracking metadata as tuple of [event, output mapper] or (input,output) => Promise<[event, data]|undefined> * @returns tRPC mutation procedure */ export const command = < diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 589aad13b92..c934ed34407 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -128,6 +128,11 @@ async function authorizeAddress( } else { auth.thread = await models.Thread.findOne({ where: { id: auth.thread_id }, + include: { + model: models.Address, + as: 'collaborators', + required: false, + }, }); if (!auth.thread) throw new InvalidInput('Must provide a valid thread id'); @@ -232,25 +237,35 @@ export const isSuperAdmin: AuthHandler = async (ctx) => { * - **not banned**: Reject if user is banned * - **author**: Allow when the user is the creator of the entity * - **topic group**: Allow when user has group permissions in topic + * - **collaborators**: Allow collaborators * * @param roles specific community roles - all by default * @param action specific group permission action + * @param collaborators authorize thread collaborators * @throws InvalidActor when not authorized */ export function isAuthorized({ roles = ['admin', 'moderator', 'member'], action, + collaborators = false, }: { roles?: Role[]; action?: GroupPermissionAction; + collaborators?: boolean; }): AuthHandler { return async (ctx) => { if (ctx.actor.user.isAdmin) return; const auth = await authorizeAddress(ctx, roles); + auth.is_author = auth.address!.id === auth.author_address_id; if (auth.address!.is_banned) throw new BannedActor(ctx.actor); - if (auth.author_address_id && auth.address!.id === auth.author_address_id) - return; // author + if (auth.is_author) return; if (action && auth.address!.role === 'member') await isTopicMember(ctx.actor, auth, action); + if (collaborators) { + const found = auth.thread?.collaborators?.find( + (a) => a.address === ctx.actor.address, + ); + if (!found) throw new InvalidActor(ctx.actor, 'Not authorized'); + } }; } diff --git a/libs/model/src/models/collaboration.ts b/libs/model/src/models/collaboration.ts index 1eaf103a5cc..74c93d213d4 100644 --- a/libs/model/src/models/collaboration.ts +++ b/libs/model/src/models/collaboration.ts @@ -6,11 +6,12 @@ import type { ModelInstance } from './types'; export type CollaborationAttributes = { address_id: number; thread_id: number; - created_at: Date; - updated_at: Date; - Address: AddressAttributes; - Thread: ThreadAttributes; + created_at?: Date; + updated_at?: Date; + + Address?: AddressAttributes; + Thread?: ThreadAttributes; }; export type CollaborationInstance = ModelInstance & { diff --git a/libs/model/src/thread/UpdateThread.command.ts b/libs/model/src/thread/UpdateThread.command.ts index 03dce16b4a8..1b65c35fba6 100644 --- a/libs/model/src/thread/UpdateThread.command.ts +++ b/libs/model/src/thread/UpdateThread.command.ts @@ -1,753 +1,383 @@ -import { type Command } from '@hicommonwealth/core'; +import { + Actor, + InvalidActor, + InvalidInput, + type Command, +} from '@hicommonwealth/core'; import * as schemas from '@hicommonwealth/schemas'; +import { Op, QueryTypes, Sequelize } from 'sequelize'; +import { z } from 'zod'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; -import { mustBeAuthorizedThread } from '../middleware/guards'; - -export const UpdateThreadErrors = { - // ThreadNotFound: 'Thread not found', - // BanError: 'Ban error', - // NoTitle: 'Must provide title', - // NoBody: 'Must provide body', - // InvalidLink: 'Invalid thread URL', - // ParseMentionsFailed: 'Failed to parse mentions', - // Unauthorized: 'Unauthorized', - // InvalidStage: 'Please Select a Stage', - // FailedToParse: 'Failed to parse custom stages', - // InvalidTopic: 'Invalid topic', - // MissingCollaborators: 'Failed to find all provided collaborators', - // CollaboratorsOverlap: - // 'Cannot overlap addresses when adding/removing collaborators', - // ContestLock: 'Cannot edit thread that is in a contest', -}; - -export function UpdateThread(): Command< - typeof schemas.UpdateThread, - AuthContext -> { - return { - ...schemas.UpdateThread, - auth: [ - isAuthorized({ action: schemas.PermissionEnum.CREATE_THREAD }), - //verifyThreadSignature, - ], - body: async ({ actor, payload, auth }) => { - const { thread } = mustBeAuthorizedThread(actor, auth); - - //const body = sanitizeQuillText(payload.body); - //const plaintext = kind === 'discussion' ? quillToPlain(body) : body; - //const mentions = uniqueMentions(parseUserMentions(body)); - - // == mutation transaction boundary == - await models.sequelize.transaction(async (transaction) => {}); - // == end of transaction boundary == - - return thread!.toJSON(); - }, - }; -} - -/* -import { AppError, ServerError } from '@hicommonwealth/core'; +import { mustBeAuthorized, mustExist } from '../middleware/guards'; +import type { ThreadAttributes, ThreadInstance } from '../models/thread'; import { - AddressInstance, - CommentAttributes, - CommunityInstance, - DB, - ThreadAttributes, - ThreadInstance, - UserInstance, emitMentions, findMentionDiff, parseUserMentions, -} from '@hicommonwealth/model'; -import { renderQuillDeltaToText } from '@hicommonwealth/shared'; -import _ from 'lodash'; -import { - Op, - QueryTypes, - Sequelize, - Transaction, - WhereOptions, -} from 'sequelize'; -import { MixpanelCommunityInteractionEvent } from '../../../shared/analytics/types'; -import { validURL } from '../../../shared/utils'; -import { findAllRoles } from '../../util/roles'; -import { TrackOptions } from '../server_analytics_controller'; -import { ServerThreadsController } from '../server_threads_controller'; - -export async function __updateThread( - this: ServerThreadsController, + quillToPlain, + sanitizeQuillText, +} from '../utils'; + +export const UpdateThreadErrors = { + ThreadNotFound: 'Thread not found', + InvalidStage: 'Please Select a Stage', + MissingCollaborators: 'Failed to find all provided collaborators', + CollaboratorsOverlap: + 'Cannot overlap addresses when adding/removing collaborators', + ContestLock: 'Cannot edit thread that is in a contest', +}; + +function getContentPatch( + thread: ThreadInstance, { - user, - address, - threadId, title, body, - stage, url, - locked, - pinned, - archived, - spam, - topicId, - collaborators, - canvasHash, - canvasSignedData, - discordMeta, - }: UpdateThreadOptions, -): Promise { - // Discobot handling - - const threadWhere: WhereOptions = {}; - if (threadId) { - threadWhere.id = threadId; - } - if (discordMeta) { - threadWhere.discord_meta = discordMeta; - } - - const thread = await this.models.Thread.findOne({ - where: threadWhere, - include: { - model: this.models.Address, - as: 'collaborators', - required: false, - }, - }); + canvas_hash, + canvas_signed_data, + }: z.infer, +) { + const patch: Partial = {}; - if (!thread) { - throw new AppError(Errors.ThreadNotFound); - } + typeof title !== 'undefined' && (patch.title = title); - // check if thread is part of a contest topic - const contestManagers = await this.models.sequelize.query( - ` - SELECT cm.contest_address FROM "Threads" t - JOIN "ContestTopics" ct on ct.topic_id = t.topic_id - JOIN "ContestManagers" cm on cm.contest_address = ct.contest_address - WHERE t.id = :thread_id - `, - { - type: QueryTypes.SELECT, - replacements: { - thread_id: thread!.id, - }, - }, - ); - const isContestThread = contestManagers.length > 0; - - if (address.is_banned) throw new AppError('Banned User'); - - // get various permissions - const userOwnedAddressIds = (await user.getAddresses()) - .filter((addr) => !!addr.verified) - .map((addr) => addr.id); - const roles = await findAllRoles( - this.models, - // @ts-expect-error StrictNullChecks - { where: { address_id: { [Op.in]: userOwnedAddressIds } } }, - thread.community_id, - ['moderator', 'admin'], - ); - - const isCollaborator = !!thread.collaborators?.find( - (a) => a.address === address.address, - ); - const isThreadOwner = userOwnedAddressIds.includes(thread.address_id); - const isMod = !!roles.find( - (r) => - r.community_id === thread.community_id && r.permission === 'moderator', - ); - const isAdmin = !!roles.find( - (r) => r.community_id === thread.community_id && r.permission === 'admin', - ); - const isSuperAdmin = user.isAdmin ?? false; - if ( - !isThreadOwner && - !isMod && - !isAdmin && - !isSuperAdmin && - !isCollaborator - ) { - throw new AppError(Errors.Unauthorized); + if (typeof body !== 'undefined' && thread.kind === 'discussion') { + patch.body = sanitizeQuillText(body); + patch.plaintext = quillToPlain(patch.body); } - // build analytics - const allAnalyticsOptions: TrackOptions[] = []; - - const community = await this.models.Community.findByPk(thread.community_id); - - // patch thread properties - const transaction = await this.models.sequelize.transaction(); - - try { - const toUpdate: Partial = {}; - - const permissions = { - isThreadOwner, - isMod, - isAdmin, - isSuperAdmin, - isCollaborator, - }; - - await setThreadAttributes( - permissions, - thread, - { - title, - body, - url, - canvasHash, - canvasSignedData, - }, - isContestThread, - toUpdate, - ); - - await setThreadPinned(permissions, pinned, toUpdate); - - await setThreadSpam(permissions, spam, toUpdate); - - await setThreadLocked(permissions, locked, toUpdate); - - await setThreadArchived(permissions, archived, toUpdate); - - await setThreadStage( - permissions, - stage, - community!, - allAnalyticsOptions, - toUpdate, - ); - - await setThreadTopic( - permissions, - community!, - topicId!, - this.models, - isContestThread, - toUpdate, - ); - - await thread.update( - { - ...toUpdate, - last_edited: Sequelize.literal('CURRENT_TIMESTAMP'), - }, - { transaction }, - ); + typeof url !== 'undefined' && thread.kind === 'link' && (patch.url = url); - const latestVersionHistory = await this.models.ThreadVersionHistory.findOne( - { - where: { - thread_id: thread.id, - }, - order: [['timestamp', 'DESC']], - transaction, - }, - ); - - // if the modification was different from the original body, create a version history for it - if (body && latestVersionHistory?.body !== body) { - await this.models.ThreadVersionHistory.create( - { - thread_id: threadId!, - address: address.address, - body, - timestamp: new Date(), - }, - { - transaction, - }, - ); - } - - await updateThreadCollaborators( - permissions, - thread, - collaborators, - isContestThread, - this.models, - transaction, - ); - - const previousDraftMentions = parseUserMentions(latestVersionHistory?.body); - const currentDraftMentions = parseUserMentions(decodeURIComponent(body)); - - const mentions = findMentionDiff( - previousDraftMentions, - currentDraftMentions, - ); - - await emitMentions(this.models, transaction, { - authorAddressId: address.id!, - authorUserId: user.id!, - authorAddress: address.address, - mentions, - thread, - community_id: thread.community_id, - }); - - await transaction.commit(); - } catch (err) { - console.error(err); - await transaction.rollback(); - if (err instanceof AppError || err instanceof ServerError) { - throw err; - } - throw new ServerError(`transaction failed: ${err.message}`); + if (Object.keys(patch).length > 0) { + patch.canvas_hash = canvas_hash; + patch.canvas_signed_data = canvas_signed_data; } - - // --- - - address - .update({ - last_active: Sequelize.literal('CURRENT_TIMESTAMP'), - }) - .catch(console.error); - - const finalThread = await this.models.Thread.findOne({ - where: { id: thread.id }, - include: [ - { - model: this.models.Address, - as: 'Address', - include: [ - { - model: this.models.User, - as: 'User', - required: true, - attributes: ['id', 'profile'], - }, - ], - }, - { - model: this.models.Address, - as: 'collaborators', - include: [ - { - model: this.models.User, - as: 'User', - required: true, - attributes: ['id', 'profile'], - }, - ], - }, - { model: this.models.Topic, as: 'topic' }, - { - model: this.models.Reaction, - as: 'reactions', - include: [ - { - model: this.models.Address, - as: 'Address', - required: true, - include: [ - { - model: this.models.User, - as: 'User', - required: true, - attributes: ['id', 'profile'], - }, - ], - }, - ], - }, - { - model: this.models.Comment, - limit: 3, // This could me made configurable, atm we are using 3 recent comments with threads in frontend. - order: [['created_at', 'DESC']], - attributes: [ - 'id', - 'address_id', - 'text', - ['plaintext', 'plainText'], - 'created_at', - 'updated_at', - 'deleted_at', - 'marked_as_spam_at', - 'discord_meta', - ], - include: [ - { - model: this.models.Address, - attributes: ['address'], - include: [ - { - model: this.models.User, - attributes: ['profile'], - }, - ], - }, - ], - }, - { - model: this.models.ThreadVersionHistory, - }, - ], - }); - - const updatedThreadWithComments = { - // @ts-expect-error StrictNullChecks - ...finalThread.toJSON(), - } as ThreadAttributes & { - Comments?: CommentAttributes[]; - recentComments?: CommentAttributes[]; - }; - updatedThreadWithComments.recentComments = ( - updatedThreadWithComments.Comments || [] - ).map((c) => { - const temp = { - ...c, - address: c?.Address?.address || '', - }; - - if (temp.Address) delete temp.Address; - - return temp; - }); - - delete updatedThreadWithComments.Comments; - - return [updatedThreadWithComments, allAnalyticsOptions]; + return patch; } -// ----- - -export type UpdateThreadPermissions = { - isThreadOwner: boolean; - isMod: boolean; - isAdmin: boolean; - isSuperAdmin: boolean; - isCollaborator: boolean; -}; - - -export function validatePermissions( - permissions: UpdateThreadPermissions, - flags: Partial, +async function getCollaboratorsPatch( + actor: Actor, + auth: AuthContext, + { collaborators }: z.infer, ) { - const keys = [ - 'isThreadOwner', - 'isMod', - 'isAdmin', - 'isSuperAdmin', - 'isCollaborator', - ]; - for (const k of keys) { - if (flags[k] && permissions[k]) { - // at least one flag is satisfied - return; - } - } - // no flags were satisfied - throw new AppError(Errors.Unauthorized); -} + const removeSet = new Set(collaborators?.toRemove ?? []); + const add = [...new Set(collaborators?.toAdd ?? [])]; + const remove = [...removeSet]; + const intersection = add.filter((item) => removeSet.has(item)); -export type UpdatableThreadAttributes = { - title?: string; - body?: string; - url?: string; - canvasSignedData?: string; - canvasHash?: string; -}; + if (intersection.length > 0) + throw new InvalidInput(UpdateThreadErrors.CollaboratorsOverlap); -async function setThreadAttributes( - permissions: UpdateThreadPermissions, - thread: ThreadInstance, - { title, body, url, canvasSignedData, canvasHash }: UpdatableThreadAttributes, - isContestThread: boolean, - toUpdate: Partial, -) { - if ( - typeof title !== 'undefined' || - typeof body !== 'undefined' || - typeof url !== 'undefined' - ) { - if (isContestThread) { - throw new AppError(Errors.ContestLock); - } - validatePermissions(permissions, { - isThreadOwner: true, - isMod: true, - isAdmin: true, - isSuperAdmin: true, - isCollaborator: true, + if (add.length > 0) { + const addresses = await models.Address.findAll({ + where: { + community_id: auth.community_id!, + id: { + [Op.in]: add, + }, + }, }); - - // title - if (typeof title !== 'undefined') { - if (!title) { - throw new AppError(Errors.NoTitle); - } - toUpdate.title = title; - } - - // body - if (typeof body !== 'undefined') { - if (thread.kind === 'discussion' && (!body || !body.trim())) { - throw new AppError(Errors.NoBody); - } - toUpdate.body = body; - toUpdate.plaintext = (() => { - try { - return renderQuillDeltaToText(JSON.parse(decodeURIComponent(body))); - } catch (e) { - return decodeURIComponent(body); - } - })(); - } - - // url - if (typeof url !== 'undefined' && thread.kind === 'link') { - if (!validURL(url)) { - throw new AppError(Errors.InvalidLink); - } - toUpdate.url = url; - } - - toUpdate.canvas_signed_data = canvasSignedData; - toUpdate.canvas_hash = canvasHash; + if (addresses.length !== add.length) + throw new InvalidInput(UpdateThreadErrors.MissingCollaborators); } -} - -async function setThreadPinned( - permissions: UpdateThreadPermissions, - pinned: boolean | undefined, - toUpdate: Partial, -) { - if (typeof pinned !== 'undefined') { - validatePermissions(permissions, { - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); - - toUpdate.pinned = pinned; + if (add.length > 0 || remove.length > 0) { + const authorized = actor.user.isAdmin || auth.is_author; + if (!authorized) + throw new InvalidActor(actor, 'Must be super admin or author'); } + + return { add, remove }; } - -async function setThreadLocked( - permissions: UpdateThreadPermissions, - locked: boolean | undefined, - toUpdate: Partial, +function getAdminOrModeratorPatch( + actor: Actor, + auth: AuthContext, + { pinned, spam }: z.infer, ) { - if (typeof locked !== 'undefined') { - validatePermissions(permissions, { - isThreadOwner: true, - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); + const patch: Partial = {}; - toUpdate.read_only = locked; - toUpdate.locked_at = locked - ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) - : null; - } -} + typeof pinned !== 'undefined' && (patch.pinned = pinned); - -async function setThreadArchived( - permissions: UpdateThreadPermissions, - archive: boolean | undefined, - toUpdate: Partial, -) { - if (typeof archive !== 'undefined') { - validatePermissions(permissions, { - isThreadOwner: true, - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); + typeof spam !== 'undefined' && + (patch.marked_as_spam_at = spam ? new Date() : null); - toUpdate.archived_at = archive - ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) - : null; + if (Object.keys(patch).length > 0) { + const authorized = + actor.user.isAdmin || ['admin', 'moderator'].includes(auth.address!.role); + if (!authorized) + throw new InvalidActor(actor, 'Must be admin or moderator'); } + return patch; } - -async function setThreadSpam( - permissions: UpdateThreadPermissions, - spam: boolean | undefined, - toUpdate: Partial, +async function getAdminOrModeratorOrOwnerPatch( + actor: Actor, + auth: AuthContext, + { + locked, + archived, + stage, + topic_id, + }: z.infer, ) { - if (typeof spam !== 'undefined') { - validatePermissions(permissions, { - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); + const patch: Partial = {}; - toUpdate.marked_as_spam_at = spam - ? (Sequelize.literal('CURRENT_TIMESTAMP') as any) - : null; + if (typeof locked !== 'undefined') { + patch.read_only = locked; + patch.locked_at = locked ? new Date() : null; } -} - -async function setThreadStage( - permissions: UpdateThreadPermissions, - stage: string | undefined, - community: CommunityInstance, - allAnalyticsOptions: TrackOptions[], - toUpdate: Partial, -) { - if (typeof stage !== 'undefined') { - validatePermissions(permissions, { - isThreadOwner: true, - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); + typeof archived !== 'undefined' && + (patch.archived_at = archived ? new Date() : null); - // fetch available stages - let customStages = []; - try { - const communityStages = community.custom_stages; - if (Array.isArray(communityStages)) { - customStages = Array.from(communityStages) - .map((s) => s.toString()) - .filter((s) => s); - } - if (customStages.length === 0) { - customStages = [ - 'discussion', - 'proposal_in_review', - 'voting', - 'passed', - 'failed', - ]; - } - } catch (e) { - throw new AppError(Errors.FailedToParse); - } + if (typeof stage !== 'undefined') { + const community = await models.Community.findByPk(auth.community_id!); + mustExist('Community', community); - // validate stage - if (!customStages.includes(stage)) { - throw new AppError(Errors.InvalidStage); - } + const custom_stages = + community.custom_stages.length > 0 + ? community.custom_stages + : ['discussion', 'proposal_in_review', 'voting', 'passed', 'failed']; - toUpdate.stage = stage; + if (!custom_stages.includes(stage)) + throw new InvalidInput(UpdateThreadErrors.InvalidStage); - allAnalyticsOptions.push({ - event: MixpanelCommunityInteractionEvent.UPDATE_STAGE, - }); + patch.stage = stage; } -} - -async function setThreadTopic( - permissions: UpdateThreadPermissions, - community: CommunityInstance, - topicId: number, - models: DB, - isContestThread: boolean, - toUpdate: Partial, -) { - if (typeof topicId !== 'undefined') { - if (isContestThread) { - throw new AppError(Errors.ContestLock); - } - validatePermissions(permissions, { - isThreadOwner: true, - isMod: true, - isAdmin: true, - isSuperAdmin: true, - }); + if (typeof topic_id !== 'undefined') { const topic = await models.Topic.findOne({ - where: { - id: topicId, - community_id: community.id, - }, + where: { id: topic_id, community_id: auth.community_id! }, }); + mustExist('Topic', topic); - if (!topic) { - throw new AppError(Errors.InvalidTopic); - } - toUpdate.topic_id = topic.id; + patch.topic_id = topic_id; } + + if (Object.keys(patch).length > 0) { + const authorized = + actor.user.isAdmin || + ['admin', 'moderator'].includes(auth.address!.role) || + auth.is_author; + if (!authorized) + throw new InvalidActor(actor, 'Must be admin, moderator, or author'); + } + return patch; } - -async function updateThreadCollaborators( - permissions: UpdateThreadPermissions, - thread: ThreadInstance, - collaborators: - | { - toAdd?: number[]; - toRemove?: number[]; - } - | undefined, - isContestThread: boolean, - models: DB, - transaction: Transaction, -) { - const { toAdd, toRemove } = collaborators || {}; - if (Array.isArray(toAdd) || Array.isArray(toRemove)) { - if (isContestThread) { - throw new AppError(Errors.ContestLock); - } - - validatePermissions(permissions, { - isThreadOwner: true, - isSuperAdmin: true, - }); +export function UpdateThread(): Command< + typeof schemas.UpdateThread, + AuthContext +> { + return { + ...schemas.UpdateThread, + auth: [isAuthorized({ collaborators: true })], + body: async ({ actor, payload, auth }) => { + const { address } = mustBeAuthorized(actor, auth); + const { thread_id, discord_meta } = payload; - const toAddUnique = _.uniq(toAdd || []); - const toRemoveUnique = _.uniq(toRemove || []); + // find by thread_id or discord_meta + const thread = await models.Thread.findOne({ + where: thread_id ? { id: thread_id } : { discord_meta }, + }); + if (!thread) throw new InvalidInput(UpdateThreadErrors.ThreadNotFound); + + const content = getContentPatch(thread, payload); + const adminPatch = getAdminOrModeratorPatch(actor, auth!, payload); + const ownerPatch = await getAdminOrModeratorOrOwnerPatch( + actor, + auth!, + payload, + ); + const collaboratorsPatch = await getCollaboratorsPatch( + actor, + auth!, + payload, + ); - // check for overlap between toAdd and toRemove - for (const r of toRemoveUnique) { - if (toAddUnique.includes(r)) { - throw new AppError(Errors.CollaboratorsOverlap); - } - } - - // add collaborators - if (toAddUnique.length > 0) { - const collaboratorAddresses = await models.Address.findAll({ - where: { - community_id: thread.community_id, - id: { - [Op.in]: toAddUnique, - }, - }, + console.log({ + content, + adminPatch, + ownerPatch, }); - if (collaboratorAddresses.length !== toAddUnique.length) { - throw new AppError(Errors.MissingCollaborators); + + // check if patch violates contest locks + if ( + Object.keys(content).length > 0 || + ownerPatch.topic_id || + collaboratorsPatch.add.length > 0 || + collaboratorsPatch.remove.length > 0 + ) { + const contestManagers = await models.sequelize.query( + ` +SELECT cm.contest_address FROM "Threads" t +JOIN "ContestTopics" ct on ct.topic_id = t.topic_id +JOIN "ContestManagers" cm on cm.contest_address = ct.contest_address +WHERE t.id = :thread_id +`, + { + type: QueryTypes.SELECT, + replacements: { + thread_id: thread!.id, + }, + }, + ); + if (contestManagers.length > 0) + throw new InvalidInput(UpdateThreadErrors.ContestLock); } - await Promise.all( - collaboratorAddresses.map(async (address) => { - return models.Collaboration.findOrCreate({ + + // == mutation transaction boundary == + await models.sequelize.transaction(async (transaction) => { + await thread.update( + { + ...content, + ...adminPatch, + ...ownerPatch, + last_edited: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + { transaction }, + ); + + if (collaboratorsPatch.add.length > 0) + await models.Collaboration.bulkCreate( + collaboratorsPatch.add.map((address_id) => ({ + address_id, + thread_id, + })), + { transaction }, + ); + if (collaboratorsPatch.remove.length > 0) { + await models.Collaboration.destroy({ where: { - thread_id: thread.id, - address_id: address.id, + thread_id, + address_id: { + [Op.in]: collaboratorsPatch.remove, + }, }, transaction, }); - }), - ); - } - - // remove collaborators - if (toRemoveUnique.length > 0) { - await models.Collaboration.destroy({ - where: { - thread_id: thread.id, - address_id: { - [Op.in]: toRemoveUnique, - }, - }, - transaction, + } + + // TODO: we can encapsulate address activity in authorization middleware + address.last_active = new Date(); + await address.save({ transaction }); + + if (content.body) { + const currentVersion = await models.ThreadVersionHistory.findOne({ + where: { thread_id }, + order: [['timestamp', 'DESC']], + transaction, + }); + // if the modification was different from the original body, create a version history for it + if (currentVersion?.body !== content.body) { + await models.ThreadVersionHistory.create( + { + thread_id, + address: address.address, + body: content.body, + timestamp: new Date(), + }, + { transaction }, + ); + const mentions = findMentionDiff( + parseUserMentions(currentVersion?.body), + parseUserMentions(decodeURIComponent(content.body)), + ); + mentions && + (await emitMentions(models, transaction, { + authorAddressId: address.id!, + authorUserId: actor.user.id!, + authorAddress: address.address, + mentions, + thread, + community_id: thread.community_id, + })); + } + } }); - } - } -} + // == end of transaction boundary == -*/ + // TODO: should we make a query out of this, or do we have one already? + return ( + await models.Thread.findOne({ + where: { id: thread_id }, + include: [ + { + model: models.Address, + as: 'Address', + include: [ + { + model: models.User, + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + { + model: models.Address, + as: 'collaborators', + include: [ + { + model: models.User, + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + { model: models.Topic, as: 'topic' }, + { + model: models.Reaction, + as: 'reactions', + include: [ + { + model: models.Address, + required: true, + include: [ + { + model: models.User, + required: true, + attributes: ['id', 'profile'], + }, + ], + }, + ], + }, + { + model: models.Comment, + limit: 3, // This could me made configurable, atm we are using 3 recent comments with threads in frontend. + order: [['created_at', 'DESC']], + attributes: [ + 'id', + 'address_id', + 'text', + ['plaintext', 'plainText'], + 'created_at', + 'updated_at', + 'deleted_at', + 'marked_as_spam_at', + 'discord_meta', + ], + include: [ + { + model: models.Address, + attributes: ['address'], + include: [ + { + model: models.User, + attributes: ['profile'], + }, + ], + }, + ], + }, + { + model: models.ThreadVersionHistory, + }, + ], + }) + )?.toJSON(); + }, + }; +} diff --git a/libs/model/test/thread/thread-lifecycle.spec.ts b/libs/model/test/thread/thread-lifecycle.spec.ts index ce82375e314..d1d242d0aa5 100644 --- a/libs/model/test/thread/thread-lifecycle.spec.ts +++ b/libs/model/test/thread/thread-lifecycle.spec.ts @@ -31,6 +31,7 @@ import { CreateThread, CreateThreadReaction, CreateThreadReactionErrors, + UpdateThread, } from '../../src/thread'; import { getCommentDepth } from '../../src/utils/getCommentDepth'; @@ -207,63 +208,19 @@ describe('Thread lifecycle', () => { describe('updates', () => { it('should patch update thread attributes', async () => { - // const address = { - // id: 1, - // address: '0x1234', - // role: 'admin', - // community_id: 'ethereum', - // verified: true, - // update: async () => ({}), - // }; - // const attributes: UpdateThreadOptions = { - // user: { - // getAddresses: async () => [address], - // } as any, - // address: address as any, - // threadId: 1, - // title: 'hello', - // body: 'wasup', - // url: 'https://example.com', - // }; - // const db: any = { - // Thread: { - // findOne: async () => ({ - // Address: address, - // address_id: address.id, - // update: () => ({ id: 1, created_at: Date.now() }), - // toJSON: () => ({}), - // }), - // update: () => ({ id: 1, created_at: Date.now() }), - // }, - // ThreadVersionHistory: { - // create: () => null, - // findOne: () => null, - // }, - // Topic: { - // findOne: async () => ({ - // id: 1, - // }), - // }, - // // for findAllRoles - // Address: { - // findAll: async () => [address], - // }, - // Community: { - // findByPk: async () => ({ id: 'ethereum' }), - // }, - // sequelize: { - // transaction: async () => ({ - // rollback: async () => ({}), - // commit: async () => ({}), - // }), - // query: () => new Promise((resolve) => resolve([])), - // }, - // }; - // const serverThreadsController = new ServerThreadsController(db); - // const [updatedThread, analyticsOptions] = - // await serverThreadsController.updateThread(attributes); - // expect(updatedThread).to.be.ok; - // expect(analyticsOptions).to.have.length(0); + const body = { + title: 'hello', + body: 'wasup', + url: 'https://example.com', + }; + const updated = await command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread!.id, + ...body, + }, + }); + expect(updated).to.contain(body); }); }); diff --git a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts index d84cd7c12e2..87075fd564f 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts @@ -95,7 +95,7 @@ export const buildUpdateThreadInput = async ({ // for editing thread archived status ...(archived !== undefined && { archived }), // for editing thread topic - ...(topicId !== undefined && { topicId }), + ...(topicId !== undefined && { topic_id: topicId }), // for editing thread collaborators ...(collaborators !== undefined && { collaborators }), ...toCanvasSignedDataApiArgs(canvasSignedData), diff --git a/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx b/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx index 50aa3f33be0..49fac5e4598 100644 --- a/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/change_thread_topic_modal.tsx @@ -50,7 +50,7 @@ export const ChangeThreadTopicModal = ({ const handleSaveChanges = async () => { try { const input = await buildUpdateThreadInput({ - communityId: app.activeChainId(), + communityId: app.activeChainId() || '', address: user.activeAccount?.address || '', threadId: thread.id, topicId: activeTopic.id, diff --git a/packages/commonwealth/server/api/threads.ts b/packages/commonwealth/server/api/threads.ts index 3973e0e25f8..8c809029c39 100644 --- a/packages/commonwealth/server/api/threads.ts +++ b/packages/commonwealth/server/api/threads.ts @@ -7,7 +7,13 @@ export const trpcRouter = trpc.router({ MixpanelCommunityInteractionEvent.CREATE_THREAD, ({ community_id }) => ({ community: community_id }), ]), - updateThread: trpc.command(Thread.UpdateThread, trpc.Tag.Thread), + updateThread: trpc.command(Thread.UpdateThread, trpc.Tag.Thread, (input) => + Promise.resolve( + input.stage !== undefined + ? [MixpanelCommunityInteractionEvent.UPDATE_STAGE, {}] + : undefined, + ), + ), createThreadReaction: trpc.command( Thread.CreateThreadReaction, trpc.Tag.Thread, From db3875c1d6e258cdf7ecc42d8db2776f33204dce Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 15:47:56 +0300 Subject: [PATCH 03/30] decode db content script init --- packages/commonwealth/package.json | 5 +- .../commonwealth/scripts/decode-db-content.ts | 352 ++++++++++++++++++ pnpm-lock.yaml | 14 + 3 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 packages/commonwealth/scripts/decode-db-content.ts diff --git a/packages/commonwealth/package.json b/packages/commonwealth/package.json index ac18a842452..56f8d38891f 100644 --- a/packages/commonwealth/package.json +++ b/packages/commonwealth/package.json @@ -156,6 +156,7 @@ "buffer": "^6.0.3", "cheerio": "1.0.0-rc.3", "clsx": "^1.2.1", + "commonwealth-mdxeditor": "^0.0.3", "compression": "^1.7.4", "connect-session-sequelize": "^7.1.1", "cookie-parser": "^1.4.4", @@ -211,6 +212,7 @@ "process": "^0.11.10", "protobufjs": "^6.1.13", "quill": "^1.3.7", + "quill-delta-to-markdown": "^0.6.0", "quill-image-drop-and-paste": "^1.0.4", "quill-magic-url": "^4.2.0", "quill-mention": "^2.2.7", @@ -259,8 +261,7 @@ "web3-validator": "2.0.5", "yargs": "^17.7.2", "zod": "^3.22.4", - "zustand": "^4.3.8", - "commonwealth-mdxeditor": "^0.0.3" + "zustand": "^4.3.8" }, "devDependencies": { "@ethersproject/keccak256": "5.7.0", diff --git a/packages/commonwealth/scripts/decode-db-content.ts b/packages/commonwealth/scripts/decode-db-content.ts new file mode 100644 index 00000000000..d8f83448553 --- /dev/null +++ b/packages/commonwealth/scripts/decode-db-content.ts @@ -0,0 +1,352 @@ +import { dispose, logger } from '@hicommonwealth/core'; +import { models } from '@hicommonwealth/model'; +import { deltaToMarkdown } from 'quill-delta-to-markdown'; +import { LOCK, Op, QueryTypes } from 'sequelize'; + +const log = logger(import.meta); +const THREAD_BATCH_SIZE = 1_000; +const queryCase = 'WHEN id = ? THEN ?'; + +function decodeContent(content: string) { + // decode if URI encoded + let decodedContent: string; + try { + decodedContent = decodeURIComponent(content); + } catch { + decodedContent = content; + } + + // convert to Markdown if Quill Delta format + let rawMarkdown: string; + try { + const delta = JSON.parse(decodedContent); + if ('ops' in delta) { + rawMarkdown = deltaToMarkdown(delta.ops); + } else { + rawMarkdown = delta; + } + } catch (e) { + rawMarkdown = decodedContent; + } + + return rawMarkdown; +} + +async function decodeThreads(lastId: number = 0) { + let lastThreadId = lastId; + while (true) { + const transaction = await models.sequelize.transaction(); + try { + const threads = await models.Thread.findAll({ + attributes: ['id', 'title', 'body'], + where: { + id: { + [Op.gt]: lastThreadId, + }, + }, + order: [['id', 'ASC']], + limit: THREAD_BATCH_SIZE, + lock: LOCK.UPDATE, + transaction, + }); + if (threads.length === 0) { + await transaction.rollback(); + break; + } + + lastThreadId = threads.at(-1)!.id!; + + let queryTitleCases = ''; + let queryBodyCases = ''; + const titleReplacements: (number | string)[] = []; + const bodyReplacements: (number | string)[] = []; + const threadIds: number[] = []; + for (const { id, title, body } of threads) { + if (titleReplacements.length > 0) { + queryTitleCases += ',\n'; + queryBodyCases += ',\n'; + } + const decodedTitle = decodeContent(title); + queryTitleCases += queryCase; + titleReplacements.push(id!, decodedTitle); + + const decodedBody = body ? decodeContent(body) : ''; + queryBodyCases += queryCase; + bodyReplacements.push(id!, decodedBody); + + threadIds.push(id!); + } + + if (threadIds.length > 0) { + await models.sequelize.query( + ` + UPDATE "Threads" + SET title = CASE + ${queryTitleCases} + END, + body = CASE + ${queryBodyCases} + END + WHERE id IN (?); + `, + { + replacements: [ + ...titleReplacements, + ...bodyReplacements, + threadIds, + ], + type: QueryTypes.BULKUPDATE, + transaction, + }, + ); + } + await transaction.commit(); + log.info( + `Successfully decoded comments ${threads[0].id} to ${threads.at(-1)!.id}`, + ); + } catch (e) { + await transaction.rollback(); + } + } +} + +async function decodeThreadVersionHistory(lastId: number = 0) { + let lastVersionHistoryId = lastId; + while (true) { + const transaction = await models.sequelize.transaction(); + try { + const threads = await models.ThreadVersionHistory.findAll({ + attributes: ['id', 'body'], + where: { + id: { + [Op.gt]: lastVersionHistoryId, + }, + }, + order: [['id', 'ASC']], + limit: THREAD_BATCH_SIZE, + lock: LOCK.UPDATE, + transaction, + }); + + if (threads.length === 0) { + await transaction.rollback(); + break; + } + + lastVersionHistoryId = threads.at(-1)!.id!; + + let queryCases = ''; + const replacements: (number | string)[] = []; + const threadVersionIds: number[] = []; + for (const { id, body } of threads) { + const decodedBody = decodeContent(body); + if (body === decodedBody) continue; + if (replacements.length > 0) queryCases += ',\n'; + queryCases += queryCase; + replacements.push(id!, decodedBody); + threadVersionIds.push(id!); + } + + if (replacements.length > 0) { + await models.sequelize.query( + ` + UPDATE "ThreadVersionHistories" + SET body = CASE + ${queryCases} + END + WHERE id IN (?); + `, + { + replacements: [...replacements, threadVersionIds], + type: QueryTypes.BULKUPDATE, + transaction, + }, + ); + } + await transaction.commit(); + log.info( + `Successfully decoded comments ${threads[0].id} to ${threads.at(-1)!.id}`, + ); + } catch (e) { + await transaction.rollback(); + } + } +} + +async function decodeCommentVersionHistory(lastId: number = 0) { + let lastCommentVersionId = lastId; + while (true) { + const transaction = await models.sequelize.transaction(); + try { + const comments = await models.CommentVersionHistory.findAll({ + attributes: ['id', 'text'], + where: { + id: { + [Op.gt]: lastCommentVersionId, + }, + }, + order: [['id', 'ASC']], + limit: THREAD_BATCH_SIZE, + lock: LOCK.UPDATE, + transaction, + }); + + if (comments.length === 0) { + await transaction.rollback(); + break; + } + + lastCommentVersionId = comments.at(-1)!.id!; + + let queryCases = ''; + const replacements: (number | string)[] = []; + const commentVersionIds: number[] = []; + for (const { id, text } of comments) { + const decodedBody = decodeContent(text); + if (text === decodedBody) continue; + if (replacements.length > 0) queryCases += ',\n'; + queryCases += queryCase; + replacements.push(id!, decodedBody); + commentVersionIds.push(id!); + } + + if (replacements.length > 0) { + await models.sequelize.query( + ` + UPDATE "CommentVersionHistories" + SET text = CASE + ${queryCases} + END + WHERE id IN (?); + `, + { + replacements: [...replacements, commentVersionIds], + type: QueryTypes.BULKUPDATE, + transaction, + }, + ); + } + await transaction.commit(); + log.info( + 'Successfully decoded comment version histories' + + ` ${comments[0].id} to ${comments.at(-1)!.id}`, + ); + } catch (e) { + await transaction.rollback(); + } + } +} + +async function decodeComments(lastId: number = 0) { + let lastCommentId = lastId; + while (true) { + const transaction = await models.sequelize.transaction(); + try { + const comments = await models.Comment.findAll({ + attributes: ['id', 'text'], + where: { + id: { + [Op.gt]: lastCommentId, + }, + }, + order: [['id', 'ASC']], + limit: THREAD_BATCH_SIZE, + lock: LOCK.UPDATE, + transaction, + }); + + if (comments.length === 0) { + await transaction.rollback(); + break; + } + + lastCommentId = comments.at(-1)!.id!; + + let queryCases = ''; + const replacements: (number | string)[] = []; + const commentIds: number[] = []; + for (const { id, text } of comments) { + const decodedBody = decodeContent(text); + if (text === decodedBody) continue; + if (replacements.length > 0) queryCases += ',\n'; + queryCases += queryCase; + replacements.push(id!, decodedBody); + commentIds.push(id!); + } + + if (replacements.length > 0) { + await models.sequelize.query( + ` + UPDATE "Comments" + SET text = CASE + ${queryCases} + END + WHERE id IN (?); + `, + { + replacements: [...replacements, commentIds], + type: QueryTypes.BULKUPDATE, + transaction, + }, + ); + } + await transaction.commit(); + log.info( + `Successfully decoded comments ${comments[0].id} to ${comments.at(-1)!.id}`, + ); + } catch (e) { + await transaction.rollback(); + } + } +} + +async function main() { + const acceptedArgs = [ + 'threads', + 'thread-versions', + 'comments', + 'comment-versions', + ]; + if (!acceptedArgs.includes(process.argv[2])) { + log.error(`Must provide one of: ${JSON.stringify(acceptedArgs)}`); + } + + let lastId = 0; + if (process.argv[3]) { + lastId = parseInt(process.argv[3]); + } + + switch (process.argv[2]) { + case 'threads': + await decodeThreads(lastId); + log.info('Thread decoding finished.'); + break; + case 'thread-versions': + await decodeThreadVersionHistory(lastId); + log.info('Thread version history decoding finished.'); + break; + case 'comments': + await decodeComments(lastId); + log.info('Comment decoding finished.'); + break; + case 'comment-versions': + await decodeCommentVersionHistory(lastId); + log.info('Comment version history decoding finished'); + break; + default: + log.error('Invalid argument!'); + } +} + +if (import.meta.url.endsWith(process.argv[1])) { + main() + .then(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispose()('EXIT', true); + }) + .catch((err) => { + console.error(err); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispose()('ERROR', true); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7db9e1eda06..b63f840766f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1261,6 +1261,9 @@ importers: quill: specifier: ^1.3.6 version: 1.3.7 + quill-delta-to-markdown: + specifier: ^0.6.0 + version: 0.6.0 quill-image-drop-and-paste: specifier: ^1.0.4 version: 1.3.0 @@ -20188,6 +20191,13 @@ packages: } engines: { node: '>=8' } + quill-delta-to-markdown@0.6.0: + resolution: + { + integrity: sha512-GwNMwSvXsH8G2o6EhvoTnIwEcN8RMbtklSjuyXYdPAXAhseMiQfehngcTwVZCz/3Fn4iu1CxlpLIKoBA9AjgdQ==, + } + engines: { node: '>=6.4.0' } + quill-delta@3.6.3: resolution: { @@ -39973,6 +39983,10 @@ snapshots: quick-lru@4.0.1: {} + quill-delta-to-markdown@0.6.0: + dependencies: + lodash: 4.17.21 + quill-delta@3.6.3: dependencies: deep-equal: 1.1.2 From fd61f82f7b2e44920653b326f705e707a32df681 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 16:43:11 +0300 Subject: [PATCH 04/30] script fixes --- .../commonwealth/scripts/decode-db-content.ts | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/commonwealth/scripts/decode-db-content.ts b/packages/commonwealth/scripts/decode-db-content.ts index d8f83448553..b257929b4c9 100644 --- a/packages/commonwealth/scripts/decode-db-content.ts +++ b/packages/commonwealth/scripts/decode-db-content.ts @@ -1,11 +1,11 @@ import { dispose, logger } from '@hicommonwealth/core'; import { models } from '@hicommonwealth/model'; import { deltaToMarkdown } from 'quill-delta-to-markdown'; -import { LOCK, Op, QueryTypes } from 'sequelize'; +import { Op, QueryTypes } from 'sequelize'; const log = logger(import.meta); -const THREAD_BATCH_SIZE = 1_000; -const queryCase = 'WHEN id = ? THEN ?'; +const BATCH_SIZE = 10; +const queryCase = 'WHEN id = ? THEN ? '; function decodeContent(content: string) { // decode if URI encoded @@ -29,7 +29,7 @@ function decodeContent(content: string) { rawMarkdown = decodedContent; } - return rawMarkdown; + return rawMarkdown.trim(); } async function decodeThreads(lastId: number = 0) { @@ -45,8 +45,8 @@ async function decodeThreads(lastId: number = 0) { }, }, order: [['id', 'ASC']], - limit: THREAD_BATCH_SIZE, - lock: LOCK.UPDATE, + limit: BATCH_SIZE, + lock: transaction.LOCK.UPDATE, transaction, }); if (threads.length === 0) { @@ -62,10 +62,6 @@ async function decodeThreads(lastId: number = 0) { const bodyReplacements: (number | string)[] = []; const threadIds: number[] = []; for (const { id, title, body } of threads) { - if (titleReplacements.length > 0) { - queryTitleCases += ',\n'; - queryBodyCases += ',\n'; - } const decodedTitle = decodeContent(title); queryTitleCases += queryCase; titleReplacements.push(id!, decodedTitle); @@ -102,10 +98,13 @@ async function decodeThreads(lastId: number = 0) { } await transaction.commit(); log.info( - `Successfully decoded comments ${threads[0].id} to ${threads.at(-1)!.id}`, + 'Successfully decoded threads' + + ` ${threads[0].id} to ${threads.at(-1)!.id}`, ); } catch (e) { + log.error('Failed to update', e); await transaction.rollback(); + break; } } } @@ -123,8 +122,8 @@ async function decodeThreadVersionHistory(lastId: number = 0) { }, }, order: [['id', 'ASC']], - limit: THREAD_BATCH_SIZE, - lock: LOCK.UPDATE, + limit: BATCH_SIZE, + lock: transaction.LOCK.UPDATE, transaction, }); @@ -141,7 +140,6 @@ async function decodeThreadVersionHistory(lastId: number = 0) { for (const { id, body } of threads) { const decodedBody = decodeContent(body); if (body === decodedBody) continue; - if (replacements.length > 0) queryCases += ',\n'; queryCases += queryCase; replacements.push(id!, decodedBody); threadVersionIds.push(id!); @@ -165,10 +163,13 @@ async function decodeThreadVersionHistory(lastId: number = 0) { } await transaction.commit(); log.info( - `Successfully decoded comments ${threads[0].id} to ${threads.at(-1)!.id}`, + 'Successfully decoded thread version histories ' + + `${threads[0].id} to ${threads.at(-1)!.id}`, ); } catch (e) { + log.error('Failed to update', e); await transaction.rollback(); + break; } } } @@ -186,8 +187,8 @@ async function decodeCommentVersionHistory(lastId: number = 0) { }, }, order: [['id', 'ASC']], - limit: THREAD_BATCH_SIZE, - lock: LOCK.UPDATE, + limit: BATCH_SIZE, + lock: transaction.LOCK.UPDATE, transaction, }); @@ -204,7 +205,6 @@ async function decodeCommentVersionHistory(lastId: number = 0) { for (const { id, text } of comments) { const decodedBody = decodeContent(text); if (text === decodedBody) continue; - if (replacements.length > 0) queryCases += ',\n'; queryCases += queryCase; replacements.push(id!, decodedBody); commentVersionIds.push(id!); @@ -232,7 +232,9 @@ async function decodeCommentVersionHistory(lastId: number = 0) { ` ${comments[0].id} to ${comments.at(-1)!.id}`, ); } catch (e) { + log.error('Failed to update', e); await transaction.rollback(); + break; } } } @@ -250,8 +252,8 @@ async function decodeComments(lastId: number = 0) { }, }, order: [['id', 'ASC']], - limit: THREAD_BATCH_SIZE, - lock: LOCK.UPDATE, + limit: BATCH_SIZE, + lock: transaction.LOCK.UPDATE, transaction, }); @@ -268,7 +270,6 @@ async function decodeComments(lastId: number = 0) { for (const { id, text } of comments) { const decodedBody = decodeContent(text); if (text === decodedBody) continue; - if (replacements.length > 0) queryCases += ',\n'; queryCases += queryCase; replacements.push(id!, decodedBody); commentIds.push(id!); @@ -292,10 +293,13 @@ async function decodeComments(lastId: number = 0) { } await transaction.commit(); log.info( - `Successfully decoded comments ${comments[0].id} to ${comments.at(-1)!.id}`, + 'Successfully decoded comments' + + ` ${comments[0].id} to ${comments.at(-1)!.id}`, ); } catch (e) { + log.error('Failed to update', e); await transaction.rollback(); + break; } } } From 03002c5c4c905cfde8d214c8bb66e66c56318661 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 17:01:14 +0300 Subject: [PATCH 05/30] include deleted threads/comments --- packages/commonwealth/scripts/decode-db-content.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/commonwealth/scripts/decode-db-content.ts b/packages/commonwealth/scripts/decode-db-content.ts index b257929b4c9..27a909155a4 100644 --- a/packages/commonwealth/scripts/decode-db-content.ts +++ b/packages/commonwealth/scripts/decode-db-content.ts @@ -48,6 +48,7 @@ async function decodeThreads(lastId: number = 0) { limit: BATCH_SIZE, lock: transaction.LOCK.UPDATE, transaction, + paranoid: false, }); if (threads.length === 0) { await transaction.rollback(); @@ -255,6 +256,7 @@ async function decodeComments(lastId: number = 0) { limit: BATCH_SIZE, lock: transaction.LOCK.UPDATE, transaction, + paranoid: false, }); if (comments.length === 0) { From ca3a8e1a9538141df5976f7f251eb0cc2498fd72 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 17:39:24 +0300 Subject: [PATCH 06/30] fix thread/comment viewing --- libs/shared/src/utils.ts | 8 ++++++++ .../commonwealth/client/scripts/models/Comment.ts | 3 ++- packages/commonwealth/client/scripts/models/Thread.ts | 11 +---------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/shared/src/utils.ts b/libs/shared/src/utils.ts index 5b1b9147835..fb4982bf7d1 100644 --- a/libs/shared/src/utils.ts +++ b/libs/shared/src/utils.ts @@ -308,3 +308,11 @@ export function getWebhookDestination(webhookUrl = ''): string { return destination; } + +export function getDecodedString(str: string) { + try { + return decodeURIComponent(str); + } catch (err) { + return str; + } +} diff --git a/packages/commonwealth/client/scripts/models/Comment.ts b/packages/commonwealth/client/scripts/models/Comment.ts index 4c67a0d7f62..a7a6ea0d974 100644 --- a/packages/commonwealth/client/scripts/models/Comment.ts +++ b/packages/commonwealth/client/scripts/models/Comment.ts @@ -1,3 +1,4 @@ +import { getDecodedString } from '@hicommonwealth/shared'; import type momentType from 'moment'; import moment, { Moment } from 'moment'; import AddressInfo from './AddressInfo'; @@ -64,7 +65,7 @@ export class Comment { const versionHistory = CommentVersionHistories; this.communityId = community_id; this.author = Address?.address || author; - this.text = deleted_at?.length > 0 ? '[deleted]' : decodeURIComponent(text); + this.text = deleted_at?.length > 0 ? '[deleted]' : getDecodedString(text); this.plaintext = deleted_at?.length > 0 ? '[deleted]' : plaintext; this.versionHistory = versionHistory; this.threadId = thread_id; diff --git a/packages/commonwealth/client/scripts/models/Thread.ts b/packages/commonwealth/client/scripts/models/Thread.ts index 6e662c6a507..45c920241a8 100644 --- a/packages/commonwealth/client/scripts/models/Thread.ts +++ b/packages/commonwealth/client/scripts/models/Thread.ts @@ -4,7 +4,7 @@ import { ContestManager, ContestScore, } from '@hicommonwealth/schemas'; -import { ProposalType } from '@hicommonwealth/shared'; +import { ProposalType, getDecodedString } from '@hicommonwealth/shared'; import { UserProfile, addressToUserProfile } from 'models/MinimumProfile'; import moment, { Moment } from 'moment'; import { z } from 'zod'; @@ -14,15 +14,6 @@ import Topic from './Topic'; import type { IUniqueId } from './interfaces'; import type { ThreadKind, ThreadStage } from './types'; -function getDecodedString(str: string) { - try { - return decodeURIComponent(str); - } catch (err) { - console.error(`Could not decode str: "${str}"`); - return str; - } -} - function processAssociatedContests( associatedContests?: AssociatedContest[] | null, contestActions?: ContestActionT[] | null, From 568e17e731812e3790d2d797960f04405936e1af Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 18:09:14 +0300 Subject: [PATCH 07/30] fix thread/comment creation/editing --- libs/model/package.json | 1 + .../src/comment/CreateComment.command.ts | 5 +- libs/model/src/thread/CreateThread.command.ts | 4 +- libs/model/src/utils/decodeContent.ts | 27 + libs/model/src/utils/index.ts | 1 + .../state/api/comments/createComment.ts | 2 +- .../scripts/state/api/comments/editComment.ts | 2 +- .../scripts/state/api/threads/createThread.ts | 4 +- .../scripts/state/api/threads/editThread.ts | 4 +- packages/commonwealth/package.json | 1 - pnpm-lock.yaml | 644 +++++++++++++----- 11 files changed, 523 insertions(+), 172 deletions(-) create mode 100644 libs/model/src/utils/decodeContent.ts diff --git a/libs/model/package.json b/libs/model/package.json index 07828a26579..c26cb5268cc 100644 --- a/libs/model/package.json +++ b/libs/model/package.json @@ -47,6 +47,7 @@ "node-fetch": "2", "node-object-hash": "^3.0.0", "pg": "^8.11.3", + "quill-delta-to-markdown": "^0.6.0", "sequelize": "^6.32.1", "umzug": "^3.7.0", "uuid": "^9.0.1", diff --git a/libs/model/src/comment/CreateComment.command.ts b/libs/model/src/comment/CreateComment.command.ts index 0a93333b6d4..1bb77fe1f1a 100644 --- a/libs/model/src/comment/CreateComment.command.ts +++ b/libs/model/src/comment/CreateComment.command.ts @@ -1,5 +1,5 @@ import { EventNames, InvalidState, type Command } from '@hicommonwealth/core'; -import { getCommentSearchVector } from '@hicommonwealth/model'; +import { decodeContent, getCommentSearchVector } from '@hicommonwealth/model'; import * as schemas from '@hicommonwealth/schemas'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; @@ -10,7 +10,6 @@ import { emitMentions, parseUserMentions, quillToPlain, - sanitizeQuillText, uniqueMentions, } from '../utils'; import { getCommentDepth } from '../utils/getCommentDepth'; @@ -53,7 +52,7 @@ export function CreateComment(): Command< throw new InvalidState(CreateCommentErrors.NestingTooDeep); } - const text = sanitizeQuillText(payload.text); + const text = decodeContent(payload.text); const plaintext = quillToPlain(text); const mentions = uniqueMentions(parseUserMentions(text)); diff --git a/libs/model/src/thread/CreateThread.command.ts b/libs/model/src/thread/CreateThread.command.ts index 85fdc438644..ad829ad1727 100644 --- a/libs/model/src/thread/CreateThread.command.ts +++ b/libs/model/src/thread/CreateThread.command.ts @@ -17,10 +17,10 @@ import { mustBeAuthorized } from '../middleware/guards'; import { getThreadSearchVector } from '../models/thread'; import { tokenBalanceCache } from '../services'; import { + decodeContent, emitMentions, parseUserMentions, quillToPlain, - sanitizeQuillText, uniqueMentions, } from '../utils'; @@ -112,7 +112,7 @@ export function CreateThread(): Command< checkContestLimits(activeContestManagers, actor.address!); } - const body = sanitizeQuillText(payload.body); + const body = decodeContent(payload.body); const plaintext = kind === 'discussion' ? quillToPlain(body) : body; const mentions = uniqueMentions(parseUserMentions(body)); diff --git a/libs/model/src/utils/decodeContent.ts b/libs/model/src/utils/decodeContent.ts new file mode 100644 index 00000000000..cd26725de4c --- /dev/null +++ b/libs/model/src/utils/decodeContent.ts @@ -0,0 +1,27 @@ +// @ts-expect-error quill-delta-to-markdown doesn't have types +import { deltaToMarkdown } from 'quill-delta-to-markdown'; + +export function decodeContent(content: string) { + // decode if URI encoded + let decodedContent: string; + try { + decodedContent = decodeURIComponent(content); + } catch { + decodedContent = content; + } + + // convert to Markdown if Quill Delta format + let rawMarkdown: string; + try { + const delta = JSON.parse(decodedContent); + if ('ops' in delta) { + rawMarkdown = deltaToMarkdown(delta.ops); + } else { + rawMarkdown = delta; + } + } catch (e) { + rawMarkdown = decodedContent; + } + + return rawMarkdown.trim(); +} diff --git a/libs/model/src/utils/index.ts b/libs/model/src/utils/index.ts index 5cd5465f3c2..fbc7ab03523 100644 --- a/libs/model/src/utils/index.ts +++ b/libs/model/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './decodeContent'; export * from './denormalizedCountUtils'; export * from './getDelta'; export * from './parseUserMentions'; diff --git a/packages/commonwealth/client/scripts/state/api/comments/createComment.ts b/packages/commonwealth/client/scripts/state/api/comments/createComment.ts index 7de2fdb66c7..10c976e8200 100644 --- a/packages/commonwealth/client/scripts/state/api/comments/createComment.ts +++ b/packages/commonwealth/client/scripts/state/api/comments/createComment.ts @@ -36,7 +36,7 @@ export const buildCreateCommentInput = async ({ return { thread_id: threadId, parent_id: parentCommentId ?? undefined, - text: encodeURIComponent(unescapedText), + text: unescapedText, ...toCanvasSignedDataApiArgs(canvasSignedData), }; }; diff --git a/packages/commonwealth/client/scripts/state/api/comments/editComment.ts b/packages/commonwealth/client/scripts/state/api/comments/editComment.ts index a285b6c4e7a..f8c8c8613a8 100644 --- a/packages/commonwealth/client/scripts/state/api/comments/editComment.ts +++ b/packages/commonwealth/client/scripts/state/api/comments/editComment.ts @@ -38,7 +38,7 @@ export const buildUpdateCommentInput = async ({ author_community_id: communityId, comment_id: commentId, community_id: communityId, - text: encodeURIComponent(updatedBody), + text: updatedBody, jwt: userStore.getState().jwt, ...toCanvasSignedDataApiArgs(canvasSignedData), }; diff --git a/packages/commonwealth/client/scripts/state/api/threads/createThread.ts b/packages/commonwealth/client/scripts/state/api/threads/createThread.ts index 1c2d6e705d6..c5c25c45a03 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/createThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/createThread.ts @@ -41,8 +41,8 @@ export const buildCreateThreadInput = async ({ return { community_id: communityId, topic_id: topic.id, - title: encodeURIComponent(title), - body: encodeURIComponent(body ?? ''), + title: title, + body: body ?? '', kind, stage, url, diff --git a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts index ec41143be8f..c2f5451b7d2 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts @@ -84,8 +84,8 @@ const editThread = async ({ jwt: userStore.getState().jwt, // for edit profile ...(url && { url }), - ...(newBody && { body: encodeURIComponent(newBody) }), - ...(newTitle && { title: encodeURIComponent(newTitle) }), + ...(newBody && { body: newBody }), + ...(newTitle && { title: newTitle }), ...(authorProfile && { author: JSON.stringify(authorProfile) }), // for editing thread locked status ...(readOnly !== undefined && { locked: readOnly }), diff --git a/packages/commonwealth/package.json b/packages/commonwealth/package.json index 56f8d38891f..6d9e87a1226 100644 --- a/packages/commonwealth/package.json +++ b/packages/commonwealth/package.json @@ -212,7 +212,6 @@ "process": "^0.11.10", "protobufjs": "^6.1.13", "quill": "^1.3.7", - "quill-delta-to-markdown": "^0.6.0", "quill-image-drop-and-paste": "^1.0.4", "quill-magic-url": "^4.2.0", "quill-mention": "^2.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b63f840766f..965f24db9e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -724,6 +724,9 @@ importers: pg: specifier: ^8.11.3 version: 8.11.5 + quill-delta-to-markdown: + specifier: ^0.6.0 + version: 0.6.0 sequelize: specifier: ^6.32.1 version: 6.37.3(pg@8.11.5) @@ -1026,10 +1029,10 @@ importers: version: 1.91.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@tanstack/react-query': specifier: ^4.29.7 - version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) '@tanstack/react-query-devtools': specifier: ^4.29.7 - version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-table': specifier: ^8.9.7 version: 8.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1044,7 +1047,7 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/react-query': specifier: ^10.45.1 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react-helmet-async': specifier: ^1.0.3 version: 1.0.3(react@18.3.1) @@ -1261,9 +1264,6 @@ importers: quill: specifier: ^1.3.6 version: 1.3.7 - quill-delta-to-markdown: - specifier: ^0.6.0 - version: 0.6.0 quill-image-drop-and-paste: specifier: ^1.0.4 version: 1.3.0 @@ -1278,7 +1278,7 @@ importers: version: 18.3.1 react-beautiful-dnd: specifier: ^13.1.1 - version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) react-device-detect: specifier: ^2.2.3 version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2004,13 +2004,6 @@ packages: } engines: { node: '>=6.9.0' } - '@babel/generator@7.24.5': - resolution: - { - integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==, - } - engines: { node: '>=6.9.0' } - '@babel/generator@7.25.6': resolution: { @@ -2079,20 +2072,6 @@ packages: } engines: { node: '>=6.9.0' } - '@babel/helper-function-name@7.23.0': - resolution: - { - integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==, - } - engines: { node: '>=6.9.0' } - - '@babel/helper-hoist-variables@7.22.5': - resolution: - { - integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==, - } - engines: { node: '>=6.9.0' } - '@babel/helper-member-expression-to-functions@7.24.8': resolution: { @@ -2114,15 +2093,6 @@ packages: } engines: { node: '>=6.9.0' } - '@babel/helper-module-transforms@7.24.5': - resolution: - { - integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==, - } - engines: { node: '>=6.9.0' } - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.25.2': resolution: { @@ -2164,13 +2134,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-simple-access@7.24.5': - resolution: - { - integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==, - } - engines: { node: '>=6.9.0' } - '@babel/helper-simple-access@7.24.7': resolution: { @@ -2185,13 +2148,6 @@ packages: } engines: { node: '>=6.9.0' } - '@babel/helper-split-export-declaration@7.24.5': - resolution: - { - integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==, - } - engines: { node: '>=6.9.0' } - '@babel/helper-string-parser@7.24.1': resolution: { @@ -3189,13 +3145,6 @@ packages: } engines: { node: '>=6.9.0' } - '@babel/traverse@7.24.5': - resolution: - { - integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==, - } - engines: { node: '>=6.9.0' } - '@babel/traverse@7.25.6': resolution: { @@ -21090,6 +21039,7 @@ packages: { integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, } + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true ripemd160-min@0.0.6: @@ -25406,7 +25356,7 @@ snapshots: '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 - picocolors: 1.0.0 + picocolors: 1.0.1 '@babel/compat-data@7.24.4': {} @@ -25415,15 +25365,15 @@ snapshots: '@babel/core@7.24.5': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.6 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.24.5) '@babel/helpers': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 convert-source-map: 2.0.0 debug: 4.3.5 gensync: 1.0.0-beta.2 @@ -25432,13 +25382,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.24.5': - dependencies: - '@babel/types': 7.24.5 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - '@babel/generator@7.25.6': dependencies: '@babel/types': 7.25.6 @@ -25486,6 +25429,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-regexp-features-plugin@7.25.2': + dependencies: + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + optional: true + '@babel/helper-create-regexp-features-plugin@7.25.2(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25493,6 +25443,17 @@ snapshots: regexpu-core: 5.3.2 semver: 6.3.1 + '@babel/helper-define-polyfill-provider@0.6.2': + dependencies: + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + debug: 4.3.5 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25506,15 +25467,6 @@ snapshots: '@babel/helper-environment-visitor@7.22.20': {} - '@babel/helper-function-name@7.23.0': - dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.5 - - '@babel/helper-hoist-variables@7.22.5': - dependencies: - '@babel/types': 7.24.5 - '@babel/helper-member-expression-to-functions@7.24.8': dependencies: '@babel/traverse': 7.25.6 @@ -25533,15 +25485,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5)': - dependencies: - '@babel/core': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.24.3 - '@babel/helper-simple-access': 7.24.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/helper-validator-identifier': 7.24.5 - '@babel/helper-module-transforms@7.25.2(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25558,6 +25501,15 @@ snapshots: '@babel/helper-plugin-utils@7.24.8': {} + '@babel/helper-remap-async-to-generator@7.25.0': + dependencies: + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-wrap-function': 7.25.0 + '@babel/traverse': 7.25.6 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/helper-remap-async-to-generator@7.25.0(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25576,10 +25528,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-simple-access@7.24.5': - dependencies: - '@babel/types': 7.24.5 - '@babel/helper-simple-access@7.24.7': dependencies: '@babel/traverse': 7.25.6 @@ -25594,10 +25542,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-split-export-declaration@7.24.5': - dependencies: - '@babel/types': 7.24.5 - '@babel/helper-string-parser@7.24.1': {} '@babel/helper-string-parser@7.24.8': {} @@ -25620,9 +25564,9 @@ snapshots: '@babel/helpers@7.24.5': dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -25638,7 +25582,7 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.0 + picocolors: 1.0.1 '@babel/parser@7.24.5': dependencies: @@ -25683,6 +25627,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-async-generator-functions@7.20.7': + dependencies: + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0 + '@babel/plugin-syntax-async-generators': 7.8.4 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25701,12 +25655,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-proposal-export-default-from@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-export-default-from': 7.24.7 + optional: true + '@babel/plugin-proposal-export-default-from@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-export-default-from': 7.24.7(@babel/core@7.24.5) + '@babel/plugin-proposal-logical-assignment-operators@7.20.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4 + optional: true + '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25719,12 +25685,27 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-proposal-numeric-separator@7.18.6': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-numeric-separator': 7.10.4 + optional: true + '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.5) + '@babel/plugin-proposal-object-rest-spread@7.20.7': + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-object-rest-spread': 7.8.3 + '@babel/plugin-transform-parameters': 7.24.7 + optional: true + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.24.5)': dependencies: '@babel/compat-data': 7.24.4 @@ -25734,6 +25715,12 @@ snapshots: '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.5) + '@babel/plugin-proposal-optional-catch-binding@7.18.6': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3 + optional: true + '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25753,6 +25740,11 @@ snapshots: dependencies: '@babel/core': 7.24.5 + '@babel/plugin-syntax-async-generators@7.8.4': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25768,11 +25760,21 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-dynamic-import@7.8.3': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-export-default-from@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-export-default-from@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25813,6 +25815,11 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25823,16 +25830,31 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-numeric-separator@7.10.4': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-object-rest-spread@7.8.3': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25843,6 +25865,11 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-private-property-in-object@7.14.5': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25864,6 +25891,11 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.24.5) '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-arrow-functions@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25879,6 +25911,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-to-generator@7.24.7': + dependencies: + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-remap-async-to-generator': 7.25.0 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25893,6 +25934,11 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-block-scoping@7.25.0': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-block-scoping@7.25.0(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25915,6 +25961,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.25.4': + dependencies: + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-replace-supers': 7.25.0(@babel/core@7.24.5) + '@babel/traverse': 7.25.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-classes@7.25.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25927,12 +25985,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-computed-properties@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/template': 7.25.0 + optional: true + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 '@babel/template': 7.25.0 + '@babel/plugin-transform-destructuring@7.24.8': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-destructuring@7.24.8(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -25989,6 +26058,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-function-name@7.25.1': + dependencies: + '@babel/helper-compilation-targets': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/traverse': 7.25.6 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-function-name@7.25.1(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26004,6 +26082,11 @@ snapshots: '@babel/helper-plugin-utils': 7.24.8 '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-transform-literals@7.25.2': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-literals@7.25.2(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26055,6 +26138,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': + dependencies: + '@babel/helper-create-regexp-features-plugin': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26109,11 +26198,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-parameters@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-private-methods@7.25.4': + dependencies: + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.8 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-private-methods@7.25.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26122,6 +26224,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-private-property-in-object@7.24.7': + dependencies: + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.25.4(@babel/core@7.24.5) + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-private-property-in-object': 7.14.5 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26137,21 +26249,47 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-display-name@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-jsx-self@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-react-jsx-self@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-jsx-source@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-react-jsx-source@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-react-jsx@7.25.2': + dependencies: + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.5) + '@babel/types': 7.25.6 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26174,6 +26312,18 @@ snapshots: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-runtime@7.25.4': + dependencies: + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + babel-plugin-polyfill-corejs2: 0.4.11 + babel-plugin-polyfill-corejs3: 0.10.6 + babel-plugin-polyfill-regenerator: 0.6.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-runtime@7.25.4(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26186,11 +26336,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-shorthand-properties@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-spread@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + optional: true + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26199,6 +26362,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-sticky-regex@7.24.7': + dependencies: + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26236,6 +26404,12 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.25.2(@babel/core@7.24.5) '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-transform-unicode-regex@7.24.7': + dependencies: + '@babel/helper-create-regexp-features-plugin': 7.25.2 + '@babel/helper-plugin-utils': 7.24.8 + optional: true + '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.5)': dependencies: '@babel/core': 7.24.5 @@ -26394,21 +26568,6 @@ snapshots: '@babel/parser': 7.25.6 '@babel/types': 7.25.6 - '@babel/traverse@7.24.5': - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/generator': 7.24.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 - debug: 4.3.5 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.25.6': dependencies: '@babel/code-frame': 7.24.7 @@ -30365,7 +30524,7 @@ snapshots: nocache: 3.0.4 pretty-format: 26.6.2 serve-static: 1.15.0 - ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - encoding @@ -30419,6 +30578,14 @@ snapshots: '@react-native/assets-registry@0.74.81': {} + '@react-native/babel-plugin-codegen@0.74.81': + dependencies: + '@react-native/codegen': 0.74.81 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/babel-plugin-codegen@0.74.81(@babel/preset-env@7.25.4(@babel/core@7.24.5))': dependencies: '@react-native/codegen': 0.74.81(@babel/preset-env@7.25.4(@babel/core@7.24.5)) @@ -30426,6 +30593,55 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-preset@0.74.81': + dependencies: + '@babel/plugin-proposal-async-generator-functions': 7.20.7 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-proposal-export-default-from': 7.24.7 + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7 + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-proposal-numeric-separator': 7.18.6 + '@babel/plugin-proposal-object-rest-spread': 7.20.7 + '@babel/plugin-proposal-optional-catch-binding': 7.18.6 + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.5) + '@babel/plugin-syntax-dynamic-import': 7.8.3 + '@babel/plugin-syntax-export-default-from': 7.24.7 + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) + '@babel/plugin-transform-arrow-functions': 7.24.7 + '@babel/plugin-transform-async-to-generator': 7.24.7 + '@babel/plugin-transform-block-scoping': 7.25.0 + '@babel/plugin-transform-classes': 7.25.4 + '@babel/plugin-transform-computed-properties': 7.24.7 + '@babel/plugin-transform-destructuring': 7.24.8 + '@babel/plugin-transform-flow-strip-types': 7.25.2(@babel/core@7.24.5) + '@babel/plugin-transform-function-name': 7.25.1 + '@babel/plugin-transform-literals': 7.25.2 + '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.24.5) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7 + '@babel/plugin-transform-parameters': 7.24.7 + '@babel/plugin-transform-private-methods': 7.25.4 + '@babel/plugin-transform-private-property-in-object': 7.24.7 + '@babel/plugin-transform-react-display-name': 7.24.7 + '@babel/plugin-transform-react-jsx': 7.25.2 + '@babel/plugin-transform-react-jsx-self': 7.24.7 + '@babel/plugin-transform-react-jsx-source': 7.24.7 + '@babel/plugin-transform-runtime': 7.25.4 + '@babel/plugin-transform-shorthand-properties': 7.24.7 + '@babel/plugin-transform-spread': 7.24.7 + '@babel/plugin-transform-sticky-regex': 7.24.7 + '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.24.5) + '@babel/plugin-transform-unicode-regex': 7.24.7 + '@babel/template': 7.25.0 + '@react-native/babel-plugin-codegen': 0.74.81 + babel-plugin-transform-flow-enums: 0.0.2 + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/babel-preset@0.74.81(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))': dependencies: '@babel/core': 7.24.5 @@ -30475,6 +30691,19 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/codegen@0.74.81': + dependencies: + '@babel/parser': 7.25.6 + glob: 7.2.3 + hermes-parser: 0.19.1 + invariant: 2.2.4 + jscodeshift: 0.14.0 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + optional: true + '@react-native/codegen@0.74.81(@babel/preset-env@7.25.4(@babel/core@7.24.5))': dependencies: '@babel/parser': 7.24.5 @@ -30510,6 +30739,29 @@ snapshots: - supports-color - utf-8-validate + '@react-native/community-cli-plugin@0.74.81(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + '@react-native-community/cli-server-api': 13.6.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native-community/cli-tools': 13.6.4 + '@react-native/dev-middleware': 0.74.81(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native/metro-babel-transformer': 0.74.81 + chalk: 4.1.2 + execa: 5.1.1 + metro: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro-config: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro-core: 0.80.10 + node-fetch: 2.7.0 + querystring: 0.2.1 + readline: 1.3.0 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + optional: true + '@react-native/debugger-frontend@0.74.81': {} '@react-native/dev-middleware@0.74.81(bufferutil@4.0.8)(utf-8-validate@5.0.10)': @@ -30537,6 +30789,16 @@ snapshots: '@react-native/js-polyfills@0.74.81': {} + '@react-native/metro-babel-transformer@0.74.81': + dependencies: + '@react-native/babel-preset': 0.74.81 + hermes-parser: 0.19.1 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/metro-babel-transformer@0.74.81(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))': dependencies: '@babel/core': 7.24.5 @@ -30558,12 +30820,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 - '@react-native/virtualized-lists@0.74.81(@types/react@18.3.1)(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + '@react-native/virtualized-lists@0.74.81(@types/react@18.3.1)(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 18.3.1 - react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) optionalDependencies: '@types/react': 18.3.1 optional: true @@ -31580,10 +31842,10 @@ snapshots: - '@types/react' - react-dom - '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/match-sorter-utils': 8.15.1 - '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) superjson: 1.13.3 @@ -31598,14 +31860,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-native: 0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) - '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': dependencies: '@tanstack/query-core': 4.36.1 react: 18.3.1 use-sync-external-store: 1.2.2(react@18.3.1) optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) '@tanstack/react-store@0.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -31713,9 +31975,9 @@ snapshots: dependencies: '@trpc/server': 10.45.2 - '@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/server': 10.45.2 react: 18.3.1 @@ -33715,20 +33977,20 @@ snapshots: axios@0.21.4: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 transitivePeerDependencies: - debug axios@0.27.2: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 form-data: 4.0.0 transitivePeerDependencies: - debug axios@1.6.8: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33753,6 +34015,15 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.8 + babel-plugin-polyfill-corejs2@0.4.11: + dependencies: + '@babel/compat-data': 7.25.4 + '@babel/helper-define-polyfill-provider': 0.6.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + optional: true + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): dependencies: '@babel/compat-data': 7.24.4 @@ -33762,6 +34033,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.10.6: + dependencies: + '@babel/helper-define-polyfill-provider': 0.6.2 + core-js-compat: 3.38.1 + transitivePeerDependencies: + - supports-color + optional: true + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.24.5): dependencies: '@babel/core': 7.24.5 @@ -33770,6 +34049,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.2: + dependencies: + '@babel/helper-define-polyfill-provider': 0.6.2 + transitivePeerDependencies: + - supports-color + optional: true + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.5): dependencies: '@babel/core': 7.24.5 @@ -33777,6 +34063,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-transform-flow-enums@0.0.2: + dependencies: + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.5) + transitivePeerDependencies: + - '@babel/core' + optional: true + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.24.5): dependencies: '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.24.5) @@ -34524,7 +34817,7 @@ snapshots: connect-session-sequelize@7.1.7(sequelize@6.37.3(pg@8.11.5)): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 sequelize: 6.37.3(pg@8.11.5) transitivePeerDependencies: - supports-color @@ -34811,6 +35104,10 @@ snapshots: dependencies: ms: 2.0.0 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 @@ -36260,6 +36557,8 @@ snapshots: transitivePeerDependencies: - encoding + follow-redirects@1.15.6: {} + follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: debug: 4.3.4(supports-color@8.1.1) @@ -36808,7 +37107,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -37421,7 +37720,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.24.2 + '@babel/code-frame': 7.24.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -37509,6 +37808,31 @@ snapshots: jsc-safe-url@0.2.4: {} + jscodeshift@0.14.0: + dependencies: + '@babel/core': 7.24.5 + '@babel/parser': 7.25.6 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.24.5) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.24.5) + '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.24.5) + '@babel/preset-flow': 7.24.7(@babel/core@7.24.5) + '@babel/preset-typescript': 7.24.7(@babel/core@7.24.5) + '@babel/register': 7.24.6(@babel/core@7.24.5) + babel-core: 7.0.0-bridge.0(@babel/core@7.24.5) + chalk: 4.1.2 + flow-parser: 0.245.0 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + neo-async: 2.6.2 + node-dir: 0.1.17 + recast: 0.21.5 + temp: 0.8.4 + write-file-atomic: 2.4.3 + transitivePeerDependencies: + - supports-color + optional: true + jscodeshift@0.14.0(@babel/preset-env@7.25.4(@babel/core@7.24.5)): dependencies: '@babel/core': 7.24.5 @@ -38264,8 +38588,8 @@ snapshots: metro-source-map@0.80.10: dependencies: - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.80.10 @@ -38291,9 +38615,9 @@ snapshots: metro-transform-plugins@0.80.10: dependencies: '@babel/core': 7.24.5 - '@babel/generator': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 + '@babel/generator': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: @@ -38302,9 +38626,9 @@ snapshots: metro-transform-worker@0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.24.5 - '@babel/generator': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.5 + '@babel/generator': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 flow-enums-runtime: 0.0.6 metro: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) metro-babel-transformer: 0.80.10 @@ -38322,13 +38646,13 @@ snapshots: metro@0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: - '@babel/code-frame': 7.24.2 + '@babel/code-frame': 7.24.7 '@babel/core': 7.24.5 - '@babel/generator': 7.24.5 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/generator': 7.25.6 + '@babel/parser': 7.25.6 + '@babel/template': 7.25.0 + '@babel/traverse': 7.25.6 + '@babel/types': 7.25.6 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 @@ -40080,7 +40404,7 @@ snapshots: lodash.flow: 3.5.0 pure-color: 1.3.0 - react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: '@babel/runtime': 7.24.5 css-box-model: 1.2.1 @@ -40088,7 +40412,7 @@ snapshots: raf-schd: 4.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) redux: 4.2.1 use-memo-one: 1.1.3(react@18.3.1) transitivePeerDependencies: @@ -40103,7 +40427,7 @@ snapshots: react-devtools-core@5.3.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: shell-quote: 1.8.1 - ws: 7.5.9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -40261,19 +40585,19 @@ snapshots: - supports-color - utf-8-validate - react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10): + react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native-community/cli': 13.6.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@react-native-community/cli-platform-android': 13.6.4 '@react-native-community/cli-platform-ios': 13.6.4 '@react-native/assets-registry': 0.74.81 - '@react-native/codegen': 0.74.81(@babel/preset-env@7.25.4(@babel/core@7.24.5)) - '@react-native/community-cli-plugin': 0.74.81(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native/codegen': 0.74.81 + '@react-native/community-cli-plugin': 0.74.81(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@react-native/gradle-plugin': 0.74.81 '@react-native/js-polyfills': 0.74.81 '@react-native/normalize-colors': 0.74.81 - '@react-native/virtualized-lists': 0.74.81(@types/react@18.3.1)(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@react-native/virtualized-lists': 0.74.81(@types/react@18.3.1)(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -40336,7 +40660,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: '@babel/runtime': 7.24.5 '@types/react-redux': 7.1.33 @@ -40347,7 +40671,7 @@ snapshots: react-is: 17.0.2 optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.0(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) react-refresh@0.14.2: {} @@ -40932,7 +41256,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 '@types/validator': 13.11.9 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 dottie: 2.0.6 inflection: 1.13.4 lodash: 4.17.21 @@ -41512,7 +41836,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 fast-safe-stringify: 2.1.1 form-data: 3.0.1 formidable: 1.2.6 @@ -41690,7 +42014,7 @@ snapshots: terser@5.31.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.11.3 + acorn: 8.12.1 commander: 2.20.3 source-map-support: 0.5.21 From 08966c7e40da45a5c304416f19dfc204b06491b2 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Wed, 11 Sep 2024 18:15:40 +0300 Subject: [PATCH 08/30] fix thread/comment creation/editing --- .../src/comment/UpdateComment.command.ts | 4 +-- .../commonwealth/scripts/decode-db-content.ts | 28 +------------------ .../server_threads_methods/update_thread.ts | 9 +++--- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/libs/model/src/comment/UpdateComment.command.ts b/libs/model/src/comment/UpdateComment.command.ts index d7033940731..8b8ac69bee0 100644 --- a/libs/model/src/comment/UpdateComment.command.ts +++ b/libs/model/src/comment/UpdateComment.command.ts @@ -4,11 +4,11 @@ import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; import { mustBeAuthorized } from '../middleware/guards'; import { + decodeContent, emitMentions, findMentionDiff, parseUserMentions, quillToPlain, - sanitizeQuillText, uniqueMentions, } from '../utils'; @@ -37,7 +37,7 @@ export function UpdateComment(): Command< }); if (currentVersion?.text !== payload.text) { - const text = sanitizeQuillText(payload.text); + const text = decodeContent(payload.text); const plaintext = quillToPlain(text); const mentions = findMentionDiff( parseUserMentions(currentVersion?.text), diff --git a/packages/commonwealth/scripts/decode-db-content.ts b/packages/commonwealth/scripts/decode-db-content.ts index 27a909155a4..1ff2145278f 100644 --- a/packages/commonwealth/scripts/decode-db-content.ts +++ b/packages/commonwealth/scripts/decode-db-content.ts @@ -1,37 +1,11 @@ import { dispose, logger } from '@hicommonwealth/core'; -import { models } from '@hicommonwealth/model'; -import { deltaToMarkdown } from 'quill-delta-to-markdown'; +import { decodeContent, models } from '@hicommonwealth/model'; import { Op, QueryTypes } from 'sequelize'; const log = logger(import.meta); const BATCH_SIZE = 10; const queryCase = 'WHEN id = ? THEN ? '; -function decodeContent(content: string) { - // decode if URI encoded - let decodedContent: string; - try { - decodedContent = decodeURIComponent(content); - } catch { - decodedContent = content; - } - - // convert to Markdown if Quill Delta format - let rawMarkdown: string; - try { - const delta = JSON.parse(decodedContent); - if ('ops' in delta) { - rawMarkdown = deltaToMarkdown(delta.ops); - } else { - rawMarkdown = delta; - } - } catch (e) { - rawMarkdown = decodedContent; - } - - return rawMarkdown.trim(); -} - async function decodeThreads(lastId: number = 0) { let lastThreadId = lastId; while (true) { diff --git a/packages/commonwealth/server/controllers/server_threads_methods/update_thread.ts b/packages/commonwealth/server/controllers/server_threads_methods/update_thread.ts index eca7e1144f8..89311c04760 100644 --- a/packages/commonwealth/server/controllers/server_threads_methods/update_thread.ts +++ b/packages/commonwealth/server/controllers/server_threads_methods/update_thread.ts @@ -7,6 +7,7 @@ import { ThreadAttributes, ThreadInstance, UserInstance, + decodeContent, emitMentions, findMentionDiff, parseUserMentions, @@ -246,7 +247,7 @@ export async function __updateThread( { thread_id: threadId!, address: address.address, - body, + body: decodeContent(body), timestamp: new Date(), }, { @@ -487,12 +488,12 @@ async function setThreadAttributes( if (thread.kind === 'discussion' && (!body || !body.trim())) { throw new AppError(Errors.NoBody); } - toUpdate.body = body; + toUpdate.body = decodeContent(body); toUpdate.plaintext = (() => { try { - return renderQuillDeltaToText(JSON.parse(decodeURIComponent(body))); + return renderQuillDeltaToText(JSON.parse(body)); } catch (e) { - return decodeURIComponent(body); + return body; } })(); } From f77e356eafc0290fd1e184caa4c66c541c83577b Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 11:25:19 -0400 Subject: [PATCH 09/30] add more tests to get coverage over 98% --- .../src/comment/UpdateComment.command.ts | 4 +- .../src/community/UpdateCommunity.command.ts | 14 +- libs/model/src/middleware/authorization.ts | 28 ++- libs/model/src/models/thread.ts | 4 - libs/model/src/thread/UpdateThread.command.ts | 10 +- .../community/community-lifecycle.spec.ts | 142 +++++++++-- .../test/thread/thread-lifecycle.spec.ts | 223 ++++++++++++++++-- libs/schemas/src/entities/thread.schemas.ts | 3 + 8 files changed, 357 insertions(+), 71 deletions(-) diff --git a/libs/model/src/comment/UpdateComment.command.ts b/libs/model/src/comment/UpdateComment.command.ts index d7033940731..3920b043f01 100644 --- a/libs/model/src/comment/UpdateComment.command.ts +++ b/libs/model/src/comment/UpdateComment.command.ts @@ -23,9 +23,9 @@ export function UpdateComment(): Command< const { address } = mustBeAuthorized(actor, auth); const { comment_id, discord_meta } = payload; - // find by comment_id or discord_meta + // find discord_meta first if present const comment = await models.Comment.findOne({ - where: comment_id ? { id: comment_id } : { discord_meta }, + where: discord_meta ? { discord_meta } : { id: comment_id }, include: [{ model: models.Thread, required: true }], }); if (!comment) throw new InvalidInput('Comment not found'); diff --git a/libs/model/src/community/UpdateCommunity.command.ts b/libs/model/src/community/UpdateCommunity.command.ts index 38c7b74079d..7bac6507e50 100644 --- a/libs/model/src/community/UpdateCommunity.command.ts +++ b/libs/model/src/community/UpdateCommunity.command.ts @@ -148,14 +148,14 @@ export function UpdateCommunity(): Command< custom_stages && (community.custom_stages = custom_stages); await community.save(); - - // Suggested solution for serializing BigInts - // https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-1006086291 - (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = - function () { - return this.toString(); - }; return community.toJSON(); }, }; } + +// // Suggested solution for serializing BigInts +// // https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-1006086291 +// (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = +// function () { +// return this.toString(); +// }; diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index c934ed34407..40cef5a2396 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -29,6 +29,7 @@ export type AuthContext = { comment?: CommentInstance | null; author_address_id?: number; is_author?: boolean; + is_collaborator?: boolean; }; export type AuthHandler = Handler< @@ -255,17 +256,28 @@ export function isAuthorized({ }): AuthHandler { return async (ctx) => { if (ctx.actor.user.isAdmin) return; + const auth = await authorizeAddress(ctx, roles); - auth.is_author = auth.address!.id === auth.author_address_id; if (auth.address!.is_banned) throw new BannedActor(ctx.actor); + + auth.is_author = auth.address!.id === auth.author_address_id; if (auth.is_author) return; - if (action && auth.address!.role === 'member') - await isTopicMember(ctx.actor, auth, action); - if (collaborators) { - const found = auth.thread?.collaborators?.find( - (a) => a.address === ctx.actor.address, - ); - if (!found) throw new InvalidActor(ctx.actor, 'Not authorized'); + + if (auth.address!.role === 'member') { + if (action) { + await isTopicMember(ctx.actor, auth, action); + return; + } + + if (collaborators) { + const found = auth.thread?.collaborators?.find( + ({ address }) => address === ctx.actor.address, + ); + auth.is_collaborator = !!found; + if (auth.is_collaborator) return; + } + + throw new InvalidActor(ctx.actor, 'Not authorized member'); } }; } diff --git a/libs/model/src/models/thread.ts b/libs/model/src/models/thread.ts index 03e73887642..87c933e71aa 100644 --- a/libs/model/src/models/thread.ts +++ b/libs/model/src/models/thread.ts @@ -3,17 +3,13 @@ import { Thread } from '@hicommonwealth/schemas'; import Sequelize from 'sequelize'; import { z } from 'zod'; import { emitEvent, getThreadContestManagers } from '../utils'; -import type { AddressAttributes } from './address'; import type { CommunityAttributes } from './community'; -import type { ReactionAttributes } from './reaction'; import type { ThreadSubscriptionAttributes } from './thread_subscriptions'; import type { ModelInstance } from './types'; export type ThreadAttributes = z.infer & { // associations Community?: CommunityAttributes; - collaborators?: AddressAttributes[]; - reactions?: ReactionAttributes[]; subscriptions?: ThreadSubscriptionAttributes[]; }; diff --git a/libs/model/src/thread/UpdateThread.command.ts b/libs/model/src/thread/UpdateThread.command.ts index 1b65c35fba6..7f4060460c7 100644 --- a/libs/model/src/thread/UpdateThread.command.ts +++ b/libs/model/src/thread/UpdateThread.command.ts @@ -178,9 +178,9 @@ export function UpdateThread(): Command< const { address } = mustBeAuthorized(actor, auth); const { thread_id, discord_meta } = payload; - // find by thread_id or discord_meta + // find by discord_meta first if present const thread = await models.Thread.findOne({ - where: thread_id ? { id: thread_id } : { discord_meta }, + where: discord_meta ? { discord_meta } : { id: thread_id }, }); if (!thread) throw new InvalidInput(UpdateThreadErrors.ThreadNotFound); @@ -197,12 +197,6 @@ export function UpdateThread(): Command< payload, ); - console.log({ - content, - adminPatch, - ownerPatch, - }); - // check if patch violates contest locks if ( Object.keys(content).length > 0 || diff --git a/libs/model/test/community/community-lifecycle.spec.ts b/libs/model/test/community/community-lifecycle.spec.ts index 44c4f4c50b4..8467cdb4d8e 100644 --- a/libs/model/test/community/community-lifecycle.spec.ts +++ b/libs/model/test/community/community-lifecycle.spec.ts @@ -1,11 +1,12 @@ import { Actor, + InvalidInput, InvalidState, command, dispose, query, } from '@hicommonwealth/core'; -import { ChainBase, ChainType } from '@hicommonwealth/shared'; +import { ALL_COMMUNITIES, ChainBase, ChainType } from '@hicommonwealth/shared'; import { Chance } from 'chance'; import { afterAll, assert, beforeAll, describe, expect, test } from 'vitest'; import { @@ -28,7 +29,9 @@ const chance = Chance(); describe('Community lifecycle', () => { let ethNode: ChainNodeAttributes, edgewareNode: ChainNodeAttributes; let community: CommunityAttributes; - let actor: Actor; + let superAdminActor: Actor, adminActor: Actor; + let baseCommunityId: string; + const custom_domain = 'custom'; const group_payload = { id: '', metadata: { @@ -46,7 +49,8 @@ describe('Community lifecycle', () => { const [_edgewareNode] = await seed('ChainNode', { name: 'Edgeware Mainnet', }); - const [user] = await seed('User', { isAdmin: true }); + const [superadmin] = await seed('User', { isAdmin: true }); + const [admin] = await seed('User', { isAdmin: false }); const [base] = await seed('Community', { chain_node_id: _ethNode!.id!, base: ChainBase.Ethereum, @@ -56,17 +60,31 @@ describe('Community lifecycle', () => { Addresses: [ { role: 'member', - user_id: user!.id, + user_id: superadmin!.id, + }, + { + role: 'admin', + user_id: admin!.id, }, ], + custom_domain, }); + baseCommunityId = base?.id!; ethNode = _ethNode!; edgewareNode = _edgewareNode!; - actor = { - user: { id: user!.id!, email: user!.email!, isAdmin: user!.isAdmin! }, + superAdminActor = { + user: { + id: superadmin!.id!, + email: superadmin!.email!, + isAdmin: superadmin!.isAdmin!, + }, address: base?.Addresses?.at(0)?.address, }; + adminActor = { + user: { id: admin!.id!, email: admin!.email!, isAdmin: admin!.isAdmin! }, + address: base?.Addresses?.at(1)?.address, + }; }); afterAll(async () => { @@ -76,7 +94,7 @@ describe('Community lifecycle', () => { test('should create community', async () => { const name = chance.name(); const result = await command(CreateCommunity(), { - actor, + actor: superAdminActor, payload: { id: name, type: ChainType.Offchain, @@ -86,14 +104,14 @@ describe('Community lifecycle', () => { base: ChainBase.Ethereum, eth_chain_id: ethNode.eth_chain_id!, social_links: [], - user_address: actor.address!, + user_address: superAdminActor.address!, node_url: ethNode.url, directory_page_enabled: false, tags: [], }, }); expect(result?.community?.id).toBe(name); - expect(result?.admin_address).toBe(actor.address); + expect(result?.admin_address).toBe(superAdminActor.address); // connect results community = result!.community! as CommunityAttributes; group_payload.id = result!.community!.id; @@ -102,7 +120,7 @@ describe('Community lifecycle', () => { describe('groups', () => { test('should fail to query community via has_groups when none exists', async () => { const communityResults = await query(GetCommunities(), { - actor, + actor: superAdminActor, payload: { has_groups: true } as any, }); expect(communityResults?.results).to.have.length(0); @@ -110,7 +128,7 @@ describe('Community lifecycle', () => { test('should create group when none exists', async () => { const results = await command(CreateGroup(), { - actor, + actor: superAdminActor, payload: group_payload, }); expect(results?.groups?.at(0)?.metadata).to.includes( @@ -118,7 +136,7 @@ describe('Community lifecycle', () => { ); const communityResults = await query(GetCommunities(), { - actor, + actor: superAdminActor, payload: { has_groups: true } as any, }); expect(communityResults?.results?.at(0)?.id).to.equal(group_payload.id); @@ -126,14 +144,17 @@ describe('Community lifecycle', () => { test('should fail group creation when group with same id found', async () => { await expect(() => - command(CreateGroup(), { actor, payload: group_payload }), + command(CreateGroup(), { + actor: superAdminActor, + payload: group_payload, + }), ).rejects.toThrow(InvalidState); }); test('should fail group creation when sending invalid topics', async () => { await expect( command(CreateGroup(), { - actor, + actor: superAdminActor, payload: { id: group_payload.id, metadata: { @@ -152,7 +173,7 @@ describe('Community lifecycle', () => { // create max groups for (let i = 1; i < MAX_GROUPS_PER_COMMUNITY; i++) { await command(CreateGroup(), { - actor, + actor: superAdminActor, payload: { id: group_payload.id, metadata: { name: chance.name(), description: chance.sentence() }, @@ -164,7 +185,7 @@ describe('Community lifecycle', () => { await expect(() => command(CreateGroup(), { - actor, + actor: superAdminActor, payload: { id: group_payload.id, metadata: { name: chance.name(), description: chance.sentence() }, @@ -188,7 +209,7 @@ describe('Community lifecycle', () => { test('should update community', async () => { const updated = await command(UpdateCommunity(), { - actor, + actor: superAdminActor, payload: { ...baseRequest, id: community.id, @@ -206,7 +227,7 @@ describe('Community lifecycle', () => { test('should remove directory', async () => { const updated = await command(UpdateCommunity(), { - actor, + actor: superAdminActor, payload: { ...baseRequest, id: community.id, @@ -222,10 +243,89 @@ describe('Community lifecycle', () => { assert.equal(updated?.type, 'chain'); }); + test('should throw if trying to change network', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: superAdminActor, + payload: { + ...baseRequest, + id: community.id, + network: 'cant-change', + }, + }), + ).rejects.toThrow(UpdateCommunityErrors.CantChangeNetwork); + }); + + test('should throw if id is reserved', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: superAdminActor, + payload: { + ...baseRequest, + id: ALL_COMMUNITIES, + namespace: 'tempNamespace', + chain_node_id: 1263, + }, + }), + ).rejects.toThrow(UpdateCommunityErrors.ReservedId); + }); + + test('should throw if custom domain is taken', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: superAdminActor, + payload: { + ...baseRequest, + id: community.id, + custom_domain, + }, + }), + ).rejects.toThrow(UpdateCommunityErrors.CustomDomainIsTaken); + }); + + test('should throw if not super admin when changing custom domain', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: adminActor, + payload: { + ...baseRequest, + id: baseCommunityId, + custom_domain: 'new-custom-domain', + }, + }), + ).rejects.toThrow(UpdateCommunityErrors.CantChangeCustomDomain); + }); + + test('should throw if snapshot not found', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: adminActor, + payload: { + ...baseRequest, + id: community.id, + snapshot: ['not-found'], + }, + }), + ).rejects.toThrow(InvalidInput); + }); + + test('should throw if custom domain is invalid', async () => { + await expect(() => + command(UpdateCommunity(), { + actor: superAdminActor, + payload: { + ...baseRequest, + id: community.id, + custom_domain: 'commonwealth', + }, + }), + ).rejects.toThrow(UpdateCommunityErrors.InvalidCustomDomain); + }); + test('should throw if namespace present but no transaction hash', async () => { await expect(() => command(UpdateCommunity(), { - actor, + actor: superAdminActor, payload: { ...baseRequest, id: community.id, @@ -239,7 +339,7 @@ describe('Community lifecycle', () => { test('should throw if actor is not admin', async () => { await expect(() => command(UpdateCommunity(), { - actor, + actor: superAdminActor, payload: { ...baseRequest, id: community.id, @@ -255,7 +355,7 @@ describe('Community lifecycle', () => { test.skip('should throw if chain node of community does not match supported chain', async () => { await expect(() => command(UpdateCommunity(), { - actor, + actor: superAdminActor, payload: { ...baseRequest, id: community.id, diff --git a/libs/model/test/thread/thread-lifecycle.spec.ts b/libs/model/test/thread/thread-lifecycle.spec.ts index d1d242d0aa5..3005a4997de 100644 --- a/libs/model/test/thread/thread-lifecycle.spec.ts +++ b/libs/model/test/thread/thread-lifecycle.spec.ts @@ -12,11 +12,12 @@ import { command, dispose, } from '@hicommonwealth/core'; -import { AddressAttributes } from '@hicommonwealth/model'; -import { PermissionEnum } from '@hicommonwealth/schemas'; +import type { AddressAttributes } from '@hicommonwealth/model'; +import { Community, PermissionEnum, Thread } from '@hicommonwealth/schemas'; import { Chance } from 'chance'; import { afterEach } from 'node:test'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { z } from 'zod'; import { CreateComment, CreateCommentErrors, @@ -32,13 +33,18 @@ import { CreateThreadReaction, CreateThreadReactionErrors, UpdateThread, + UpdateThreadErrors, } from '../../src/thread'; import { getCommentDepth } from '../../src/utils/getCommentDepth'; const chance = Chance(); describe('Thread lifecycle', () => { - let thread, archived, read_only, comment; + let community: z.infer, + thread: z.infer, + archived, + read_only, + comment; const roles = ['admin', 'member', 'nonmember', 'banned', 'rejected'] as const; const addresses = {} as Record<(typeof roles)[number], AddressAttributes>; const actors = {} as Record<(typeof roles)[number], Actor>; @@ -68,7 +74,7 @@ describe('Thread lifecycle', () => { profile: { name: role }, isAdmin: role === 'admin', })); - const [community] = await seed('Community', { + const [_community] = await seed('Community', { chain_node_id: node!.id!, active: true, profile_count: 1, @@ -88,6 +94,7 @@ describe('Thread lifecycle', () => { vote_weight, }, ], + custom_stages: ['one', 'two'], }); await seed('GroupPermission', { group_id: threadGroupId, @@ -102,6 +109,7 @@ describe('Thread lifecycle', () => { allowed_actions: [PermissionEnum.CREATE_COMMENT], }); + community = _community!; roles.forEach((role) => { const user = users[role]; const address = community!.Addresses!.find((a) => a.user_id === user.id); @@ -186,13 +194,15 @@ describe('Thread lifecycle', () => { roles.forEach((role) => { if (!authorizationTests[role]) { it(`should create thread as ${role}`, async () => { - thread = await command(CreateThread(), { + const _thread = await command(CreateThread(), { actor: actors[role], payload, }); - expect(thread?.title).to.equal(title); - expect(thread?.body).to.equal(body); - expect(thread?.stage).to.equal(stage); + expect(_thread?.title).to.equal(title); + expect(_thread?.body).to.equal(body); + expect(_thread?.stage).to.equal(stage); + // capture as admin author for other tests + if (!thread) thread = _thread!; }); } else { it(`should reject create thread as ${role}`, async () => { @@ -207,21 +217,192 @@ describe('Thread lifecycle', () => { }); describe('updates', () => { - it('should patch update thread attributes', async () => { + it('should patch content', async () => { const body = { title: 'hello', body: 'wasup', - url: 'https://example.com', + canvas_hash: '', + canvas_signed_data: '', }; const updated = await command(UpdateThread(), { actor: actors.admin, payload: { - thread_id: thread!.id, + thread_id: thread.id!, ...body, }, }); expect(updated).to.contain(body); }); + + it('should add collaborators', async () => { + const body = { + collaborators: { + toAdd: [ + addresses.member.id!, + addresses.rejected.id!, + addresses.banned.id!, + ], + toRemove: [], + }, + }; + const updated = await command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + ...body, + }, + }); + expect(updated?.collaborators?.length).to.eq(3); + }); + + it('should remove collaborator', async () => { + const body = { + collaborators: { + toRemove: [addresses.banned.id!], + }, + }; + const updated = await command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + ...body, + }, + }); + expect(updated?.collaborators?.length).to.eq(2); + }); + + it('should fail when thread not found by discord_meta', async () => { + await expect( + command(UpdateThread(), { + actor: actors.member, + payload: { + thread_id: thread.id!, + discord_meta: { + message_id: '', + channel_id: '', + user: { id: '', username: '' }, + }, + }, + }), + ).rejects.toThrowError(UpdateThreadErrors.ThreadNotFound); + }); + + it('should fail when collaborators overlap', async () => { + await expect( + command(UpdateThread(), { + actor: actors.member, + payload: { + thread_id: thread.id!, + collaborators: { + toAdd: [addresses.banned.id!], + toRemove: [addresses.banned.id!], + }, + }, + }), + ).rejects.toThrowError(UpdateThreadErrors.CollaboratorsOverlap); + }); + + it('should fail when not admin or author', async () => { + await expect( + command(UpdateThread(), { + actor: actors.member, + payload: { + thread_id: thread.id!, + collaborators: { + toRemove: [addresses.banned.id!], + }, + }, + }), + ).rejects.toThrowError('Must be super admin or author'); + }); + + it('should fail when collaborator not found', async () => { + await expect( + command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + collaborators: { + toAdd: [999999999], + }, + }, + }), + ).rejects.toThrowError(UpdateThreadErrors.MissingCollaborators); + }); + + it('should patch admin or moderator attributes', async () => { + const body = { + pinned: true, + spam: true, + }; + const updated = await command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + ...body, + }, + }); + expect(updated?.pinned).to.eq(true); + expect(updated?.marked_as_spam_at).toBeDefined; + }); + + it('should fail when collaborator actor non admin/moderator', async () => { + await expect( + command(UpdateThread(), { + actor: actors.rejected, + payload: { + thread_id: thread.id!, + pinned: false, + spam: false, + }, + }), + ).rejects.toThrowError('Must be admin or moderator'); + }); + + it('should patch admin or moderator or owner attributes', async () => { + const body = { + locked: false, + archived: false, + stage: community.custom_stages.at(0), + topic_id: thread.topic_id!, + }; + const updated = await command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + ...body, + }, + }); + expect(updated?.locked_at).toBeUndefined; + expect(updated?.archived_at).toBeUndefined; + expect(updated?.stage).to.eq(community.custom_stages.at(0)); + expect(updated?.topic_id).to.eq(thread.topic_id); + }); + + it('should fail when invalid stage is sent', async () => { + await expect( + command(UpdateThread(), { + actor: actors.admin, + payload: { + thread_id: thread.id!, + stage: 'invalid', + }, + }), + ).rejects.toThrowError(UpdateThreadErrors.InvalidStage); + }); + + it('should fail when collaborator actor non admin/moderator/owner', async () => { + await expect( + command(UpdateThread(), { + actor: actors.rejected, + payload: { + thread_id: thread.id!, + locked: true, + archived: true, + }, + }), + ).rejects.toThrowError('Must be admin, moderator, or author'); + }); }); describe('comments', () => { @@ -230,7 +411,7 @@ describe('Thread lifecycle', () => { comment = await command(CreateComment(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, text, }, }); @@ -246,7 +427,7 @@ describe('Thread lifecycle', () => { command(CreateComment(), { actor: actors.member, payload: { - thread_id: thread!.id + 5, + thread_id: thread.id! + 5, text: 'hi', }, }), @@ -258,7 +439,7 @@ describe('Thread lifecycle', () => { command(CreateComment(), { actor: actors.nonmember, payload: { - thread_id: thread!.id, + thread_id: thread.id!, text: 'hi', }, }), @@ -294,7 +475,7 @@ describe('Thread lifecycle', () => { command(CreateComment(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, parent_id: 1234567890, text: 'hi', }, @@ -309,7 +490,7 @@ describe('Thread lifecycle', () => { comment = await command(CreateComment(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, parent_id, text: `level${i}`, }, @@ -327,7 +508,7 @@ describe('Thread lifecycle', () => { command(CreateComment(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, parent_id, text: 'hi', }, @@ -374,7 +555,7 @@ describe('Thread lifecycle', () => { const reaction = await command(CreateThreadReaction(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, reaction: 'like', }, }); @@ -391,7 +572,7 @@ describe('Thread lifecycle', () => { command(CreateThreadReaction(), { actor: actors.member, payload: { - thread_id: thread!.id, + thread_id: thread.id!, reaction: 'like', }, }), @@ -403,7 +584,7 @@ describe('Thread lifecycle', () => { command(CreateThreadReaction(), { actor: actors.member, payload: { - thread_id: thread!.id + 5, + thread_id: thread.id! + 5, reaction: 'like', }, }), @@ -415,7 +596,7 @@ describe('Thread lifecycle', () => { command(CreateThreadReaction(), { actor: actors.nonmember, payload: { - thread_id: thread!.id, + thread_id: thread.id!, reaction: 'like', }, }), diff --git a/libs/schemas/src/entities/thread.schemas.ts b/libs/schemas/src/entities/thread.schemas.ts index 4984c1fc871..19f9a90b500 100644 --- a/libs/schemas/src/entities/thread.schemas.ts +++ b/libs/schemas/src/entities/thread.schemas.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { DiscordMetaSchema, linksSchema, PG_INT } from '../utils'; +import { Reaction } from './reaction.schemas'; import { Topic } from './topic.schemas'; import { Address } from './user.schemas'; @@ -48,6 +49,8 @@ export const Thread = z.object({ // associations Address: Address.nullish(), topic: Topic.nullish(), + collaborators: Address.array().nullish(), + reactions: Reaction.array().nullish(), }); export const ThreadVersionHistory = z.object({ From 98cbae4e061ebd34e360fdca9a52809bdf594406 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 12:05:21 -0400 Subject: [PATCH 10/30] fix lints --- .../ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx index 0e4a722cd64..d91f90a1273 100644 --- a/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx +++ b/packages/commonwealth/client/scripts/views/pages/discussions/ThreadCard/ThreadOptions/AdminActions/AdminActions.tsx @@ -173,7 +173,7 @@ export const AdminActions = ({ const isSpam = !thread.markedAsSpamAt; try { const input = await buildUpdateThreadInput({ - communityId: app.activeChainId(), + communityId: app.activeChainId() || '', threadId: thread.id, spam: isSpam, address: user.activeAccount?.address || '', @@ -199,7 +199,7 @@ export const AdminActions = ({ address: user.activeAccount?.address || '', threadId: thread.id, readOnly: !thread.readOnly, - communityId: app.activeChainId(), + communityId: app.activeChainId() || '', }); editThread(input) .then(() => { From 36599834553ec4faca4ce99a5a4bf693c1d750d8 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 12:07:51 -0400 Subject: [PATCH 11/30] fix lints --- libs/model/test/community/community-lifecycle.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/model/test/community/community-lifecycle.spec.ts b/libs/model/test/community/community-lifecycle.spec.ts index 8467cdb4d8e..37039741575 100644 --- a/libs/model/test/community/community-lifecycle.spec.ts +++ b/libs/model/test/community/community-lifecycle.spec.ts @@ -70,7 +70,7 @@ describe('Community lifecycle', () => { custom_domain, }); - baseCommunityId = base?.id!; + baseCommunityId = base!.id!; ethNode = _ethNode!; edgewareNode = _edgewareNode!; superAdminActor = { From 5608aa7bf1f0b249091257dd89c3d7c540058d8f Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 12:26:17 -0400 Subject: [PATCH 12/30] fix lints --- .../scripts/state/api/threads/editThread.ts | 2 +- .../modals/update_proposal_status_modal.tsx | 278 +++++++++--------- 2 files changed, 143 insertions(+), 137 deletions(-) diff --git a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts index 87075fd564f..a5e249cde46 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/editThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/editThread.ts @@ -119,7 +119,7 @@ const useEditThreadMutation = ({ const { checkForSessionKeyRevalidationErrors } = useAuthModalStore(); return trpc.thread.updateThread.useMutation({ - onSuccess: async (updated) => { + onSuccess: (updated) => { // @ts-expect-error StrictNullChecks const updatedThread = new Thread(updated); // Update community level thread counters variables diff --git a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx index 244326d73f7..82a47f3c2f6 100644 --- a/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/update_proposal_status_modal.tsx @@ -109,60 +109,29 @@ export const UpdateProposalStatusModal = ({ onAction: true, }); - const handleSaveChanges = async () => { + const handleSaveChanges = () => { // set stage - const input = await buildUpdateThreadInput({ + buildUpdateThreadInput({ address: user.activeAccount?.address || '', communityId: app.activeChainId() || '', threadId: thread.id, - // @ts-expect-error - stage: tempStage, - }); - editThread(input) - .then(() => { - let links = thread.links; - const { toAdd, toDelete } = getAddedAndDeleted( - tempSnapshotProposals, - getInitialSnapshots(thread), - ); + stage: tempStage!, + }) + .then((input) => { + editThread(input) + .then(() => { + let links = thread.links; + const { toAdd, toDelete } = getAddedAndDeleted( + tempSnapshotProposals, + getInitialSnapshots(thread), + ); - if (toAdd.length > 0) { - if (app.chain.meta?.snapshot_spaces?.length === 1) { - const enrichedSnapshot = { - id: `${app.chain.meta?.snapshot_spaces?.[0]}/${toAdd[0].id}`, - title: toAdd[0].title, - }; - return addThreadLinks({ - communityId: app.activeChainId() || '', - threadId: thread.id, - links: [ - { - source: LinkSource.Snapshot, - identifier: String(enrichedSnapshot.id), - title: enrichedSnapshot.title, - }, - ], - isPWA: isAddedToHomeScreen, - }).then((updatedThread) => { - links = updatedThread.links; - return { toDelete, links }; - }); - } else { - return loadMultipleSpacesData(app.chain.meta?.snapshot_spaces) - .then((data) => { - let enrichedSnapshot; - for (const { space: _space, proposals } of data) { - const matchingSnapshot = proposals.find( - (sn) => sn.id === toAdd[0].id, - ); - if (matchingSnapshot) { - enrichedSnapshot = { - id: `${_space.id}/${toAdd[0].id}`, - title: toAdd[0].title, - }; - break; - } - } + if (toAdd.length > 0) { + if (app.chain.meta?.snapshot_spaces?.length === 1) { + const enrichedSnapshot = { + id: `${app.chain.meta?.snapshot_spaces?.[0]}/${toAdd[0].id}`, + title: toAdd[0].title, + }; return addThreadLinks({ communityId: app.activeChainId() || '', threadId: thread.id, @@ -174,99 +143,136 @@ export const UpdateProposalStatusModal = ({ }, ], isPWA: isAddedToHomeScreen, + }).then((updatedThread) => { + links = updatedThread.links; + return { toDelete, links }; }); - }) - .then((updatedThread) => { + } else { + return loadMultipleSpacesData(app.chain.meta?.snapshot_spaces) + .then((data) => { + let enrichedSnapshot; + for (const { space: _space, proposals } of data) { + const matchingSnapshot = proposals.find( + (sn) => sn.id === toAdd[0].id, + ); + if (matchingSnapshot) { + enrichedSnapshot = { + id: `${_space.id}/${toAdd[0].id}`, + title: toAdd[0].title, + }; + break; + } + } + return addThreadLinks({ + communityId: app.activeChainId() || '', + threadId: thread.id, + links: [ + { + source: LinkSource.Snapshot, + identifier: String(enrichedSnapshot.id), + title: enrichedSnapshot.title, + }, + ], + isPWA: isAddedToHomeScreen, + }); + }) + .then((updatedThread) => { + links = updatedThread.links; + return { toDelete, links }; + }); + } + } else { + return { toDelete, links }; + } + }) + .then(({ toDelete, links }) => { + if (toDelete.length > 0) { + return deleteThreadLinks({ + communityId: app.activeChainId() || '', + threadId: thread.id, + links: toDelete.map((sn) => ({ + source: LinkSource.Snapshot, + identifier: String(sn.id), + })), + }).then((updatedThread) => { + // eslint-disable-next-line no-param-reassign + links = updatedThread.links; + return links; + }); + } else { + return links; + } + }) + .catch((err) => { + const error = + err.response.data.error || + 'Failed to update stage. Make sure one is selected.'; + notifyError(error); + throw new Error(error); + }) + .then((links) => { + const { toAdd, toDelete } = getAddedAndDeleted( + tempCosmosProposals, + getInitialCosmosProposals(thread), + 'identifier', + ); + + if (toAdd.length > 0) { + return addThreadLinks({ + communityId: app.activeChainId() || '', + threadId: thread.id, + links: toAdd.map(({ identifier, title }) => ({ + source: LinkSource.Proposal, + identifier: identifier, + title: title, + })), + isPWA: isAddedToHomeScreen, + }).then((updatedThread) => { + // eslint-disable-next-line no-param-reassign links = updatedThread.links; return { toDelete, links }; }); - } - } else { - return { toDelete, links }; - } - }) - .then(({ toDelete, links }) => { - if (toDelete.length > 0) { - return deleteThreadLinks({ - communityId: app.activeChainId() || '', - threadId: thread.id, - links: toDelete.map((sn) => ({ - source: LinkSource.Snapshot, - identifier: String(sn.id), - })), - }).then((updatedThread) => { - // eslint-disable-next-line no-param-reassign - links = updatedThread.links; - return links; - }); - } else { - return links; - } - }) - .catch((err) => { - const error = - err.response.data.error || - 'Failed to update stage. Make sure one is selected.'; - notifyError(error); - throw new Error(error); - }) - .then((links) => { - const { toAdd, toDelete } = getAddedAndDeleted( - tempCosmosProposals, - getInitialCosmosProposals(thread), - 'identifier', - ); + } else { + return { toDelete, links }; + } + }) + .then(({ toDelete, links }) => { + if (toDelete.length > 0) { + return deleteThreadLinks({ + communityId: app.activeChainId() || '', + threadId: thread.id, + links: toDelete.map(({ identifier }) => ({ + source: LinkSource.Proposal, + identifier: String(identifier), + })), + }).then((updatedThread) => { + // eslint-disable-next-line no-param-reassign + links = updatedThread.links; + return links; + }); + } else { + return links; + } + }) + .catch((err) => { + console.log(err); + notifyError('Failed to update linked proposals'); + }) + .then((links) => { + trackAnalytics({ + event: + MixpanelCommunityInteractionEvent.LINK_PROPOSAL_BUTTON_PRESSED, + isPWA: isAddedToHomeScreen, + }); - if (toAdd.length > 0) { - return addThreadLinks({ - communityId: app.activeChainId() || '', - threadId: thread.id, - links: toAdd.map(({ identifier, title }) => ({ - source: LinkSource.Proposal, - identifier: identifier, - title: title, - })), - isPWA: isAddedToHomeScreen, - }).then((updatedThread) => { - // eslint-disable-next-line no-param-reassign - links = updatedThread.links; - return { toDelete, links }; + // @ts-expect-error + onChangeHandler?.(tempStage, links); + onModalClose(); + }) + .catch((err) => { + console.log(err); + notifyError('An unexpected error occurred'); }); - } else { - return { toDelete, links }; - } - }) - .then(({ toDelete, links }) => { - if (toDelete.length > 0) { - return deleteThreadLinks({ - communityId: app.activeChainId() || '', - threadId: thread.id, - links: toDelete.map(({ identifier }) => ({ - source: LinkSource.Proposal, - identifier: String(identifier), - })), - }).then((updatedThread) => { - // eslint-disable-next-line no-param-reassign - links = updatedThread.links; - return links; - }); - } else { - return links; - } - }) - .catch((err) => { - console.log(err); - notifyError('Failed to update linked proposals'); - }) - .then((links) => { - trackAnalytics({ - event: MixpanelCommunityInteractionEvent.LINK_PROPOSAL_BUTTON_PRESSED, - isPWA: isAddedToHomeScreen, - }); - - // @ts-expect-error - onChangeHandler?.(tempStage, links); - onModalClose(); }) .catch((err) => { console.log(err); From 5722daf138d7d80d6d0b900b6231879b95bf588d Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 13:57:50 -0400 Subject: [PATCH 13/30] remove comment --- libs/model/src/community/UpdateCommunity.command.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/model/src/community/UpdateCommunity.command.ts b/libs/model/src/community/UpdateCommunity.command.ts index 9ee9ddcad31..4d4cd34d955 100644 --- a/libs/model/src/community/UpdateCommunity.command.ts +++ b/libs/model/src/community/UpdateCommunity.command.ts @@ -126,10 +126,3 @@ export function UpdateCommunity(): Command< }, }; } - -// // Suggested solution for serializing BigInts -// // https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-1006086291 -// (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = -// function () { -// return this.toString(); -// }; From 9812361d307af1de2f0f51f64106b0c1a196823d Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 14:05:54 -0400 Subject: [PATCH 14/30] fix tests --- .../community/community-lifecycle.spec.ts | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/libs/model/test/community/community-lifecycle.spec.ts b/libs/model/test/community/community-lifecycle.spec.ts index 37039741575..744811c9229 100644 --- a/libs/model/test/community/community-lifecycle.spec.ts +++ b/libs/model/test/community/community-lifecycle.spec.ts @@ -243,19 +243,6 @@ describe('Community lifecycle', () => { assert.equal(updated?.type, 'chain'); }); - test('should throw if trying to change network', async () => { - await expect(() => - command(UpdateCommunity(), { - actor: superAdminActor, - payload: { - ...baseRequest, - id: community.id, - network: 'cant-change', - }, - }), - ).rejects.toThrow(UpdateCommunityErrors.CantChangeNetwork); - }); - test('should throw if id is reserved', async () => { await expect(() => command(UpdateCommunity(), { @@ -270,32 +257,6 @@ describe('Community lifecycle', () => { ).rejects.toThrow(UpdateCommunityErrors.ReservedId); }); - test('should throw if custom domain is taken', async () => { - await expect(() => - command(UpdateCommunity(), { - actor: superAdminActor, - payload: { - ...baseRequest, - id: community.id, - custom_domain, - }, - }), - ).rejects.toThrow(UpdateCommunityErrors.CustomDomainIsTaken); - }); - - test('should throw if not super admin when changing custom domain', async () => { - await expect(() => - command(UpdateCommunity(), { - actor: adminActor, - payload: { - ...baseRequest, - id: baseCommunityId, - custom_domain: 'new-custom-domain', - }, - }), - ).rejects.toThrow(UpdateCommunityErrors.CantChangeCustomDomain); - }); - test('should throw if snapshot not found', async () => { await expect(() => command(UpdateCommunity(), { @@ -309,19 +270,6 @@ describe('Community lifecycle', () => { ).rejects.toThrow(InvalidInput); }); - test('should throw if custom domain is invalid', async () => { - await expect(() => - command(UpdateCommunity(), { - actor: superAdminActor, - payload: { - ...baseRequest, - id: community.id, - custom_domain: 'commonwealth', - }, - }), - ).rejects.toThrow(UpdateCommunityErrors.InvalidCustomDomain); - }); - test('should throw if namespace present but no transaction hash', async () => { await expect(() => command(UpdateCommunity(), { From 841b95809e09a2f2ef2431eae534edf94d1bf634 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 14:10:01 -0400 Subject: [PATCH 15/30] fix tests --- libs/model/test/community/community-lifecycle.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/model/test/community/community-lifecycle.spec.ts b/libs/model/test/community/community-lifecycle.spec.ts index 744811c9229..f86c105ff86 100644 --- a/libs/model/test/community/community-lifecycle.spec.ts +++ b/libs/model/test/community/community-lifecycle.spec.ts @@ -30,7 +30,6 @@ describe('Community lifecycle', () => { let ethNode: ChainNodeAttributes, edgewareNode: ChainNodeAttributes; let community: CommunityAttributes; let superAdminActor: Actor, adminActor: Actor; - let baseCommunityId: string; const custom_domain = 'custom'; const group_payload = { id: '', @@ -70,7 +69,6 @@ describe('Community lifecycle', () => { custom_domain, }); - baseCommunityId = base!.id!; ethNode = _ethNode!; edgewareNode = _edgewareNode!; superAdminActor = { From 05b371312762781a479d5ce0c36da3ed27024a61 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 14:22:15 -0400 Subject: [PATCH 16/30] fix tests --- libs/model/test/email/digest-email-lifecycle.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/model/test/email/digest-email-lifecycle.spec.ts b/libs/model/test/email/digest-email-lifecycle.spec.ts index e8a7976fd07..343631ec3db 100644 --- a/libs/model/test/email/digest-email-lifecycle.spec.ts +++ b/libs/model/test/email/digest-email-lifecycle.spec.ts @@ -153,8 +153,14 @@ describe('Digest email lifecycle', () => { expect(res![communityTwo!.id!]!.length).to.equal(1); delete threadOne?.Address; + delete threadOne?.collaborators; + delete threadOne?.reactions; delete threadTwo?.Address; + delete threadTwo?.collaborators; + delete threadTwo?.reactions; delete threadFour?.Address; + delete threadFour?.collaborators; + delete threadFour?.reactions; expect(res![communityOne!.id!]![0]!).to.deep.equal({ name: communityOne!.name, From 571a06978b6855e05e9b78fc36f1ca9cb816468f Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Wed, 11 Sep 2024 15:11:13 -0400 Subject: [PATCH 17/30] fix tests --- .../integration/api/thread.update.spec.ts | 165 ++++++++---------- 1 file changed, 74 insertions(+), 91 deletions(-) diff --git a/packages/commonwealth/test/integration/api/thread.update.spec.ts b/packages/commonwealth/test/integration/api/thread.update.spec.ts index e1d582669de..fe8abf8d002 100644 --- a/packages/commonwealth/test/integration/api/thread.update.spec.ts +++ b/packages/commonwealth/test/integration/api/thread.update.spec.ts @@ -8,6 +8,7 @@ import type { import { dispose } from '@hicommonwealth/core'; import chai from 'chai'; import chaiHttp from 'chai-http'; +import { Express } from 'express'; import jwt from 'jsonwebtoken'; import { afterAll, beforeAll, describe, test } from 'vitest'; import { TestServer, testServer } from '../../../server-test'; @@ -16,6 +17,19 @@ import { config } from '../../../server/config'; chai.use(chaiHttp); const { expect } = chai; +async function update( + app: Express, + address: string, + payload: Record, +) { + return chai.request + .agent(app) + .post(`/api/v1/UpdateThread`) + .set('Accept', 'application/json') + .set('address', address) + .send(payload); +} + describe('Thread Patch Update', () => { const chain = 'ethereum'; @@ -105,38 +119,31 @@ describe('Thread Patch Update', () => { sign: userSession.sign, }); - const res = await chai.request - .agent(server.app) - // @ts-expect-error StrictNullChecks - .patch(`/api/threads/${thread.id}`) - .set('Accept', 'application/json') - .send({ - // @ts-expect-error StrictNullChecks - author_chain: thread.community_id, - // @ts-expect-error StrictNullChecks - chain: thread.community_id, - address: userAddress, - topicId, - jwt: userJWT, - title: 'newTitle', - body: 'newBody', - stage: 'voting', - locked: true, - archived: true, - }); + const res = await update(server.app, userAddress, { + thread_id: thread!.id, + author_chain: thread!.community_id, + chain: thread!.community_id, + address: userAddress, + topicId, + jwt: userJWT, + title: 'newTitle', + body: 'newBody', + stage: 'voting', + locked: true, + archived: true, + }); expect(res.status).to.equal(200); - expect(res.body.result).to.contain({ - // @ts-expect-error StrictNullChecks - id: thread.id, + expect(res.body).to.contain({ + id: thread!.id, community_id: 'ethereum', title: 'newTitle', body: 'newBody', stage: 'voting', }); // expect(res.body.result.topic.name).to.equal('newTopic'); - expect(res.body.result.locked).to.not.be.null; - expect(res.body.result.archived).to.not.be.null; + expect(res.body.locked).to.not.be.null; + expect(res.body.archived).to.not.be.null; }); test('should not allow non-admin to set pinned or spam', async () => { @@ -154,42 +161,30 @@ describe('Thread Patch Update', () => { }); { - const res = await chai.request - .agent(server.app) - // @ts-expect-error StrictNullChecks - .patch(`/api/threads/${thread.id}`) - .set('Accept', 'application/json') - .send({ - // @ts-expect-error StrictNullChecks - author_chain: thread.community_id, - // @ts-expect-error StrictNullChecks - chain: thread.community_id, - address: userAddress, - body: 'body1', - jwt: userJWT, - pinned: true, - topicId, - }); - expect(res.status).to.equal(400); + const res = await update(server.app, userAddress, { + thread_id: thread!.id, + author_chain: thread!.community_id, + chain: thread!.community_id, + address: userAddress, + body: 'body1', + jwt: userJWT, + pinned: true, + topicId, + }); + expect(res.status).to.equal(401); } { - const res = await chai.request - .agent(server.app) - // @ts-expect-error StrictNullChecks - .patch(`/api/threads/${thread.id}`) - .set('Accept', 'application/json') - .send({ - // @ts-expect-error StrictNullChecks - author_chain: thread.community_id, - // @ts-expect-error StrictNullChecks - chain: thread.community_id, - address: userAddress, - jwt: userJWT, - spam: true, - topicId, - }); - expect(res.status).to.equal(400); + const res = await update(server.app, userAddress, { + thread_id: thread!.id, + author_chain: thread!.community_id, + chain: thread!.community_id, + address: userAddress, + jwt: userJWT, + spam: true, + topicId, + }); + expect(res.status).to.equal(401); } }); @@ -210,46 +205,34 @@ describe('Thread Patch Update', () => { // admin sets thread as pinned { - const res = await chai.request - .agent(server.app) - // @ts-expect-error StrictNullChecks - .patch(`/api/threads/${thread.id}`) - .set('Accept', 'application/json') - .send({ - // @ts-expect-error StrictNullChecks - author_chain: thread.community_id, - // @ts-expect-error StrictNullChecks - chain: thread.community_id, - address: adminAddress, - jwt: adminJWT, - body: 'body1', - pinned: true, - topicId, - }); + const res = await update(server.app, adminAddress, { + thread_id: thread!.id, + author_chain: thread!.community_id, + chain: thread!.community_id, + address: adminAddress, + jwt: adminJWT, + body: 'body1', + pinned: true, + topicId, + }); expect(res.status).to.equal(200); - expect(res.body.result.pinned).to.be.true; + expect(res.body.pinned).to.be.true; } // admin sets thread as spam { - const res = await chai.request - .agent(server.app) - // @ts-expect-error StrictNullChecks - .patch(`/api/threads/${thread.id}`) - .set('Accept', 'application/json') - .send({ - // @ts-expect-error StrictNullChecks - author_chain: thread.community_id, - // @ts-expect-error StrictNullChecks - chain: thread.community_id, - address: adminAddress, - jwt: adminJWT, - body: 'body1', - spam: true, - topicId, - }); + const res = await update(server.app, adminAddress, { + thread_id: thread!.id, + author_chain: thread!.community_id, + chain: thread!.community_id, + address: adminAddress, + jwt: adminJWT, + body: 'body1', + spam: true, + topicId, + }); expect(res.status).to.equal(200); - expect(!!res.body.result.marked_as_spam_at).to.be.true; + expect(!!res.body.marked_as_spam_at).to.be.true; } }); }); From 18acf3c528c351391ad4b35cd60f650095df4017 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Sep 2024 09:12:23 -0400 Subject: [PATCH 18/30] fix disco bot integration --- libs/schemas/src/commands/thread.schemas.ts | 3 + packages/discord-bot/package.json | 2 +- .../src/discord-consumer/handlers.ts | 4 + pnpm-lock.yaml | 376 ++++++++++-------- 4 files changed, 229 insertions(+), 156 deletions(-) diff --git a/libs/schemas/src/commands/thread.schemas.ts b/libs/schemas/src/commands/thread.schemas.ts index 2d762986c93..69b1b045e11 100644 --- a/libs/schemas/src/commands/thread.schemas.ts +++ b/libs/schemas/src/commands/thread.schemas.ts @@ -42,6 +42,9 @@ export const UpdateThread = { .optional(), canvas_signed_data: z.string().optional(), canvas_hash: z.string().optional(), + + // discord bot integration + community_id: z.string().optional(), discord_meta: DiscordMetaSchema.optional(), }), output: Thread, diff --git a/packages/discord-bot/package.json b/packages/discord-bot/package.json index fb6c5fa2cbb..440ee9bd91f 100644 --- a/packages/discord-bot/package.json +++ b/packages/discord-bot/package.json @@ -24,7 +24,7 @@ "@hicommonwealth/model": "workspace:*", "@hicommonwealth/shared": "workspace:*", "axios": "^1.3.4", - "discord.js": "^14.11.0", + "discord.js": "^14.16.2", "moment": "^2.23.0", "pg": "^8.11.3", "sequelize": "^6.32.1", diff --git a/packages/discord-bot/src/discord-consumer/handlers.ts b/packages/discord-bot/src/discord-consumer/handlers.ts index 7d839a37eb6..6eb2e84ad78 100644 --- a/packages/discord-bot/src/discord-consumer/handlers.ts +++ b/packages/discord-bot/src/discord-consumer/handlers.ts @@ -45,12 +45,16 @@ export async function handleThreadMessages( case 'thread-title-update': await axios.patch(`${bot_path}/threads`, { ...sharedReqData, + thread_id: 0, // required in command + community_id: topic.community_id, // to auth command title: encodeURIComponent(message.title), }); break; case 'thread-body-update': await axios.patch(`${bot_path}/threads`, { ...sharedReqData, + thread_id: 0, // required in command + community_id: topic.community_id, // to auth command body: encodeURIComponent( `[Go to Discord post](https://discord.com/channels/${message.guild_id}/${message.channel_id}) \n\n` + message.content + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7db9e1eda06..3439e17fb36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -729,7 +729,7 @@ importers: version: 6.37.3(pg@8.11.5) umzug: specifier: ^3.7.0 - version: 3.8.0(@types/node@20.12.10) + version: 3.8.0(@types/node@22.5.4) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -1452,8 +1452,8 @@ importers: specifier: ^1.3.4 version: 1.6.8 discord.js: - specifier: ^14.11.0 - version: 14.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) + specifier: ^14.16.2 + version: 14.16.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) moment: specifier: ^2.23.0 version: 2.30.1 @@ -3172,6 +3172,13 @@ packages: } engines: { node: '>=6.9.0' } + '@babel/runtime@7.25.6': + resolution: + { + integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==, + } + engines: { node: '>=6.9.0' } + '@babel/template@7.24.0': resolution: { @@ -3630,12 +3637,12 @@ packages: peerDependencies: postcss-selector-parser: ^6.0.10 - '@discordjs/builders@1.8.1': + '@discordjs/builders@1.9.0': resolution: { - integrity: sha512-GkF+HM01FHy+NSoTaUPR8z44otfQgJ1AIsRxclYGUZDyUbdZEFyD/5QVv2Y1Flx6M+B0bQLzg2M9CJv5lGTqpA==, + integrity: sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==, } - engines: { node: '>=16.11.0' } + engines: { node: '>=18' } '@discordjs/collection@1.5.3': resolution: @@ -3644,38 +3651,38 @@ packages: } engines: { node: '>=16.11.0' } - '@discordjs/collection@2.1.0': + '@discordjs/collection@2.1.1': resolution: { - integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==, + integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==, } engines: { node: '>=18' } - '@discordjs/formatters@0.4.0': + '@discordjs/formatters@0.5.0': resolution: { - integrity: sha512-fJ06TLC1NiruF35470q3Nr1bi95BdvKFAF+T5bNfZJ4bNdqZ3VZ+Ttg6SThqTxm6qumSG3choxLBHMC69WXNXQ==, + integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==, } - engines: { node: '>=16.11.0' } + engines: { node: '>=18' } - '@discordjs/rest@2.3.0': + '@discordjs/rest@2.4.0': resolution: { - integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==, + integrity: sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==, } - engines: { node: '>=16.11.0' } + engines: { node: '>=18' } - '@discordjs/util@1.1.0': + '@discordjs/util@1.1.1': resolution: { - integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==, + integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==, } - engines: { node: '>=16.11.0' } + engines: { node: '>=18' } - '@discordjs/ws@1.1.0': + '@discordjs/ws@1.1.1': resolution: { - integrity: sha512-O97DIeSvfNTn5wz5vaER6ciyUsr7nOqSEtsLoMhhIgeFkhnxLRqSr00/Fpq2/ppLgjDGLbQCDzIK7ilGoB/M7A==, + integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==, } engines: { node: '>=16.11.0' } @@ -4984,6 +4991,12 @@ packages: integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==, } + '@jridgewell/sourcemap-codec@1.5.0': + resolution: + { + integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, + } + '@jridgewell/trace-mapping@0.3.25': resolution: { @@ -8109,17 +8122,17 @@ packages: } engines: { node: '>=16' } - '@sapphire/async-queue@1.5.2': + '@sapphire/async-queue@1.5.3': resolution: { - integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==, + integrity: sha512-x7zadcfJGxFka1Q3f8gCts1F0xMwCKbZweM85xECGI0hBTeIZJGGCrHgLggihBoprlQ/hBmDR5LKfIPqnmHM3w==, } engines: { node: '>=v14.0.0', npm: '>=7.0.0' } - '@sapphire/shapeshift@3.9.7': + '@sapphire/shapeshift@4.0.0': resolution: { - integrity: sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==, + integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==, } engines: { node: '>=v16' } @@ -9713,6 +9726,12 @@ packages: integrity: sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==, } + '@types/node@22.5.4': + resolution: + { + integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==, + } + '@types/node@8.10.66': resolution: { @@ -9996,10 +10015,10 @@ packages: integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==, } - '@types/ws@8.5.10': + '@types/ws@8.5.12': resolution: { - integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==, + integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==, } '@types/ws@8.5.3': @@ -10220,10 +10239,10 @@ packages: integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==, } - '@vladfrangu/async_event_emitter@2.2.4': + '@vladfrangu/async_event_emitter@2.4.6': resolution: { - integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==, + integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==, } engines: { node: '>=v14.0.0', npm: '>=7.0.0' } @@ -10742,10 +10761,10 @@ packages: } engines: { node: '>=0.4.0' } - acorn-walk@8.3.3: + acorn-walk@8.3.4: resolution: { - integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==, + integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==, } engines: { node: '>=0.4.0' } @@ -12786,10 +12805,10 @@ packages: engines: { node: '>=4' } hasBin: true - cssstyle@4.0.1: + cssstyle@4.1.0: resolution: { - integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==, + integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==, } engines: { node: '>=18' } @@ -12914,6 +12933,18 @@ packages: supports-color: optional: true + debug@4.3.7: + resolution: + { + integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==, + } + engines: { node: '>=6.0' } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decache@3.1.0: resolution: { @@ -13240,12 +13271,18 @@ packages: integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==, } - discord.js@14.15.2: + discord-api-types@0.37.97: resolution: { - integrity: sha512-wGD37YCaTUNprtpqMIRuNiswwsvSWXrHykBSm2SAosoTYut0VUDj9yo9t4iLtMKvuhI49zYkvKc2TNdzdvpJhg==, + integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==, } - engines: { node: '>=16.11.0' } + + discord.js@14.16.2: + resolution: + { + integrity: sha512-VGNi9WE2dZIxYM8/r/iatQQ+3LT8STW4hhczJOwm+DBeHq66vsKDCk8trChNCB01sMO9crslYuEMeZl2d7r3xw==, + } + engines: { node: '>=18' } dns-packet@5.6.1: resolution: @@ -18726,10 +18763,10 @@ packages: integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==, } - nwsapi@2.2.10: + nwsapi@2.2.12: resolution: { - integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==, + integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==, } nyc@15.1.0: @@ -21147,6 +21184,12 @@ packages: integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==, } + rrweb-cssom@0.7.1: + resolution: + { + integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==, + } + rrweb-snapshot@2.0.0-alpha.13: resolution: { @@ -22836,6 +22879,12 @@ packages: integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==, } + tslib@2.7.0: + resolution: + { + integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==, + } + tslint@5.20.1: resolution: { @@ -23163,6 +23212,12 @@ packages: integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, } + undici-types@6.19.8: + resolution: + { + integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, + } + undici@5.28.4: resolution: { @@ -23170,12 +23225,12 @@ packages: } engines: { node: '>=14.0' } - undici@6.13.0: + undici@6.19.8: resolution: { - integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==, + integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==, } - engines: { node: '>=18.0' } + engines: { node: '>=18.17' } unenv@1.9.0: resolution: @@ -24114,6 +24169,13 @@ packages: } engines: { node: '>=4.0.0' } + websocket@1.0.35: + resolution: + { + integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==, + } + engines: { node: '>=4.0.0' } + whatwg-encoding@3.1.1: resolution: { @@ -26372,6 +26434,10 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.2 @@ -27023,48 +27089,48 @@ snapshots: dependencies: postcss-selector-parser: 6.0.16 - '@discordjs/builders@1.8.1': + '@discordjs/builders@1.9.0': dependencies: - '@discordjs/formatters': 0.4.0 - '@discordjs/util': 1.1.0 - '@sapphire/shapeshift': 3.9.7 - discord-api-types: 0.37.83 + '@discordjs/formatters': 0.5.0 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.37.97 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 - tslib: 2.6.2 + tslib: 2.7.0 '@discordjs/collection@1.5.3': {} - '@discordjs/collection@2.1.0': {} + '@discordjs/collection@2.1.1': {} - '@discordjs/formatters@0.4.0': + '@discordjs/formatters@0.5.0': dependencies: - discord-api-types: 0.37.83 + discord-api-types: 0.37.97 - '@discordjs/rest@2.3.0': + '@discordjs/rest@2.4.0': dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.2 + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.3 '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.2.4 - discord-api-types: 0.37.83 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.37.97 magic-bytes.js: 1.10.0 - tslib: 2.6.2 - undici: 6.13.0 + tslib: 2.7.0 + undici: 6.19.8 - '@discordjs/util@1.1.0': {} + '@discordjs/util@1.1.1': {} - '@discordjs/ws@1.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)': + '@discordjs/ws@1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.3)': dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.2 - '@types/ws': 8.5.10 - '@vladfrangu/async_event_emitter': 2.2.4 + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.4.0 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.3 + '@types/ws': 8.5.12 + '@vladfrangu/async_event_emitter': 2.4.6 discord-api-types: 0.37.83 - tslib: 2.6.2 + tslib: 2.7.0 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil @@ -28051,6 +28117,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -28059,7 +28127,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@keplr-wallet/types@0.11.64': dependencies: @@ -29245,7 +29313,7 @@ snapshots: '@polkadot/api-derive@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/api': 6.0.5 '@polkadot/rpc-core': 6.0.5 '@polkadot/types': 6.0.5 @@ -29258,7 +29326,7 @@ snapshots: '@polkadot/api@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/api-derive': 6.0.5 '@polkadot/keyring': 7.9.2(@polkadot/util-crypto@7.9.2(@polkadot/util@7.9.2))(@polkadot/util@7.9.2) '@polkadot/rpc-core': 6.0.5 @@ -29322,7 +29390,7 @@ snapshots: '@polkadot/keyring@7.9.2(@polkadot/util-crypto@7.9.2(@polkadot/util@7.9.2))(@polkadot/util@7.9.2)': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/util': 7.9.2 '@polkadot/util-crypto': 7.9.2(@polkadot/util@7.9.2) @@ -29334,11 +29402,11 @@ snapshots: '@polkadot/networks@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/rpc-core@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/rpc-provider': 6.0.5 '@polkadot/types': 6.0.5 '@polkadot/util': 7.9.2 @@ -29391,7 +29459,7 @@ snapshots: '@polkadot/rpc-provider@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/types': 6.0.5 '@polkadot/util': 7.9.2 '@polkadot/util-crypto': 7.9.2(@polkadot/util@7.9.2) @@ -29443,7 +29511,7 @@ snapshots: '@polkadot/types-known@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/networks': 7.9.2 '@polkadot/types': 6.0.5 '@polkadot/util': 7.9.2 @@ -29482,7 +29550,7 @@ snapshots: '@polkadot/types@6.0.5': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/util': 7.9.2 '@polkadot/util-crypto': 7.9.2(@polkadot/util@7.9.2) rxjs: 7.8.1 @@ -29502,7 +29570,7 @@ snapshots: '@polkadot/util-crypto@7.9.2(@polkadot/util@7.9.2)': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/networks': 7.9.2 '@polkadot/util': 7.9.2 '@polkadot/wasm-crypto': 4.6.1(@polkadot/util@7.9.2)(@polkadot/x-randomvalues@7.9.2) @@ -29531,7 +29599,7 @@ snapshots: '@polkadot/util@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-textdecoder': 7.9.2 '@polkadot/x-textencoder': 7.9.2 '@types/bn.js': 4.11.6 @@ -29548,7 +29616,7 @@ snapshots: '@polkadot/wasm-crypto-asmjs@4.6.1(@polkadot/util@7.9.2)': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/util': 7.9.2 '@polkadot/wasm-crypto-asmjs@7.3.2(@polkadot/util@12.6.2)': @@ -29568,7 +29636,7 @@ snapshots: '@polkadot/wasm-crypto-wasm@4.6.1(@polkadot/util@7.9.2)': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/util': 7.9.2 '@polkadot/wasm-crypto-wasm@7.3.2(@polkadot/util@12.6.2)': @@ -29579,7 +29647,7 @@ snapshots: '@polkadot/wasm-crypto@4.6.1(@polkadot/util@7.9.2)(@polkadot/x-randomvalues@7.9.2)': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/util': 7.9.2 '@polkadot/wasm-crypto-asmjs': 4.6.1(@polkadot/util@7.9.2) '@polkadot/wasm-crypto-wasm': 4.6.1(@polkadot/util@7.9.2) @@ -29614,7 +29682,7 @@ snapshots: '@polkadot/x-fetch@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-global': 7.9.2 '@types/node-fetch': 2.6.11 node-fetch: 2.7.0 @@ -29627,7 +29695,7 @@ snapshots: '@polkadot/x-global@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-randomvalues@12.6.2(@polkadot/util@12.6.2)(@polkadot/wasm-util@7.3.2(@polkadot/util@12.6.2))': dependencies: @@ -29638,7 +29706,7 @@ snapshots: '@polkadot/x-randomvalues@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-global': 7.9.2 '@polkadot/x-textdecoder@12.6.2': @@ -29648,7 +29716,7 @@ snapshots: '@polkadot/x-textdecoder@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-global': 7.9.2 '@polkadot/x-textencoder@12.6.2': @@ -29658,7 +29726,7 @@ snapshots: '@polkadot/x-textencoder@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-global': 7.9.2 '@polkadot/x-ws@12.6.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)': @@ -29672,10 +29740,10 @@ snapshots: '@polkadot/x-ws@7.9.2': dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 '@polkadot/x-global': 7.9.2 '@types/websocket': 1.0.10 - websocket: 1.0.34 + websocket: 1.0.35 transitivePeerDependencies: - supports-color @@ -30670,7 +30738,7 @@ snapshots: dependencies: rrweb-snapshot: 2.0.0-alpha.13 - '@rushstack/node-core-library@4.2.0(@types/node@20.12.10)': + '@rushstack/node-core-library@4.2.0(@types/node@22.5.4)': dependencies: fs-extra: 7.0.1 import-lazy: 4.0.0 @@ -30679,18 +30747,18 @@ snapshots: semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: - '@types/node': 20.12.10 + '@types/node': 22.5.4 - '@rushstack/terminal@0.10.2(@types/node@20.12.10)': + '@rushstack/terminal@0.10.2(@types/node@22.5.4)': dependencies: - '@rushstack/node-core-library': 4.2.0(@types/node@20.12.10) + '@rushstack/node-core-library': 4.2.0(@types/node@22.5.4) supports-color: 8.1.1 optionalDependencies: - '@types/node': 20.12.10 + '@types/node': 22.5.4 - '@rushstack/ts-command-line@4.19.3(@types/node@20.12.10)': + '@rushstack/ts-command-line@4.19.3(@types/node@22.5.4)': dependencies: - '@rushstack/terminal': 0.10.2(@types/node@20.12.10) + '@rushstack/terminal': 0.10.2(@types/node@22.5.4) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -30739,9 +30807,9 @@ snapshots: '@safe-global/safe-gateway-typescript-sdk@3.22.2': {} - '@sapphire/async-queue@1.5.2': {} + '@sapphire/async-queue@1.5.3': {} - '@sapphire/shapeshift@3.9.7': + '@sapphire/shapeshift@4.0.0': dependencies: fast-deep-equal: 3.1.3 lodash: 4.17.21 @@ -31538,7 +31606,7 @@ snapshots: '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.2 + tslib: 2.7.0 optional: true '@swc/types@0.1.7': @@ -32025,6 +32093,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@22.5.4': + dependencies: + undici-types: 6.19.8 + '@types/node@8.10.66': {} '@types/normalize-package-data@2.4.4': {} @@ -32193,15 +32265,15 @@ snapshots: '@types/websocket@1.0.10': dependencies: - '@types/node': 20.12.10 + '@types/node': 22.5.4 '@types/ws@7.4.7': dependencies: '@types/node': 20.12.10 - '@types/ws@8.5.10': + '@types/ws@8.5.12': dependencies: - '@types/node': 20.12.10 + '@types/node': 22.5.4 '@types/ws@8.5.3': dependencies: @@ -32428,7 +32500,7 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 - '@vladfrangu/async_event_emitter@2.2.4': {} + '@vladfrangu/async_event_emitter@2.4.6': {} '@wagmi/connectors@4.3.10(@types/react@18.3.1)(@wagmi/core@2.13.4(@types/react@18.3.1)(react@18.3.1)(typescript@5.4.5)(viem@2.21.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.21.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6))(zod@3.23.6)': dependencies: @@ -33383,7 +33455,7 @@ snapshots: acorn-walk@8.3.2: {} - acorn-walk@8.3.3: + acorn-walk@8.3.4: dependencies: acorn: 8.12.1 @@ -33662,7 +33734,7 @@ snapshots: ast-types@0.15.2: dependencies: - tslib: 2.6.2 + tslib: 2.7.0 astral-regex@1.0.0: {} @@ -34745,9 +34817,9 @@ snapshots: cssesc@3.0.0: {} - cssstyle@4.0.1: + cssstyle@4.1.0: dependencies: - rrweb-cssom: 0.6.0 + rrweb-cssom: 0.7.1 csstype@3.1.3: {} @@ -34811,6 +34883,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.3.7: + dependencies: + ms: 2.1.3 + decache@3.1.0: dependencies: find: 0.2.9 @@ -35003,20 +35079,22 @@ snapshots: discord-api-types@0.37.83: {} - discord.js@14.15.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): + discord-api-types@0.37.97: {} + + discord.js@14.16.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): dependencies: - '@discordjs/builders': 1.8.1 + '@discordjs/builders': 1.9.0 '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.4.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@discordjs/ws': 1.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + '@discordjs/formatters': 0.5.0 + '@discordjs/rest': 2.4.0 + '@discordjs/util': 1.1.1 + '@discordjs/ws': 1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.37.83 + discord-api-types: 0.37.97 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 - tslib: 2.6.2 - undici: 6.13.0 + tslib: 2.7.0 + undici: 6.19.8 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -36791,7 +36869,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -36841,7 +36919,7 @@ snapshots: https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.5 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -37530,38 +37608,9 @@ snapshots: dependencies: jsdom: 24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - jsdom@24.0.0: - dependencies: - cssstyle: 4.0.1 - data-urls: 5.0.0 - decimal.js: 10.4.3 - form-data: 4.0.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.10 - parse5: 7.1.2 - rrweb-cssom: 0.6.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.0.0 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - optional: true - jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: - cssstyle: 4.0.1 + cssstyle: 4.1.0 data-urls: 5.0.0 decimal.js: 10.4.3 form-data: 4.0.0 @@ -37569,7 +37618,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.10 + nwsapi: 2.2.12 parse5: 7.1.2 rrweb-cssom: 0.6.0 saxes: 6.0.0 @@ -39039,7 +39088,7 @@ snapshots: numeral@2.0.6: {} - nwsapi@2.2.10: {} + nwsapi@2.2.12: {} nyc@15.1.0: dependencies: @@ -40131,7 +40180,7 @@ snapshots: react-i18next@13.5.0(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 html-parse-stringify: 3.0.1 i18next: 23.11.5 react: 18.3.1 @@ -40512,7 +40561,7 @@ snapshots: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.6.2 + tslib: 2.7.0 rechoir@0.6.2: dependencies: @@ -40564,7 +40613,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.24.5 + '@babel/runtime': 7.25.6 regexp-tree@0.1.27: {} @@ -40756,6 +40805,8 @@ snapshots: rrweb-cssom@0.6.0: {} + rrweb-cssom@0.7.1: {} + rrweb-snapshot@2.0.0-alpha.13: {} rrweb-snapshot@2.0.0-alpha.4: {} @@ -41866,7 +41917,7 @@ snapshots: '@tsconfig/node16': 1.0.4 '@types/node': 20.12.10 acorn: 8.12.1 - acorn-walk: 8.3.3 + acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -41908,6 +41959,8 @@ snapshots: tslib@2.6.2: {} + tslib@2.7.0: {} + tslint@5.20.1(typescript@5.4.5): dependencies: '@babel/code-frame': 7.24.2 @@ -42082,9 +42135,9 @@ snapshots: dependencies: bluebird: 3.7.2 - umzug@3.8.0(@types/node@20.12.10): + umzug@3.8.0(@types/node@22.5.4): dependencies: - '@rushstack/ts-command-line': 4.19.3(@types/node@20.12.10) + '@rushstack/ts-command-line': 4.19.3(@types/node@22.5.4) emittery: 0.13.1 fast-glob: 3.3.2 pony-cause: 2.1.11 @@ -42105,11 +42158,13 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.19.8: {} + undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 - undici@6.13.0: {} + undici@6.19.8: {} unenv@1.9.0: dependencies: @@ -42490,7 +42545,7 @@ snapshots: why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.12.10 - jsdom: 24.0.0 + jsdom: 24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - less - lightningcss @@ -43039,6 +43094,17 @@ snapshots: transitivePeerDependencies: - supports-color + websocket@1.0.35: + dependencies: + bufferutil: 4.0.8 + debug: 2.6.9 + es5-ext: 0.10.64 + typedarray-to-buffer: 3.1.5 + utf-8-validate: 5.0.10 + yaeti: 0.0.6 + transitivePeerDependencies: + - supports-color + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 From 6ebbbc2cc191faadb978d2f169c2ac9d28524f02 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Sep 2024 10:02:12 -0400 Subject: [PATCH 19/30] fix typing --- .../src/discord-listener/discordListener.ts | 24 ++++++++----------- .../src/discord-listener/handlers.ts | 15 +++++++++--- .../discord-bot/src/discord-listener/util.ts | 9 +++++-- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/discord-bot/src/discord-listener/discordListener.ts b/packages/discord-bot/src/discord-listener/discordListener.ts index 329400def8f..beefe74dccb 100644 --- a/packages/discord-bot/src/discord-listener/discordListener.ts +++ b/packages/discord-bot/src/discord-listener/discordListener.ts @@ -10,7 +10,6 @@ import { Broker, broker, logger, stats } from '@hicommonwealth/core'; import { Client, IntentsBitField, - Message, MessageType, ThreadChannel, } from 'discord.js'; @@ -93,23 +92,20 @@ async function startDiscordListener() { }, ); - client.on('messageDelete', async (message: Message) => { + client.on('messageDelete', async (message) => { await handleMessage(controller, client, message, 'comment-delete'); }); - client.on( - 'messageUpdate', - async (oldMessage: Message, newMessage: Message) => { - await handleMessage( - controller, - client, - newMessage, - newMessage.nonce ? 'comment-update' : 'thread-body-update', - ); - }, - ); + client.on('messageUpdate', async (_, newMessage) => { + await handleMessage( + controller, + client, + newMessage, + newMessage.nonce ? 'comment-update' : 'thread-body-update', + ); + }); - client.on('messageCreate', async (message: Message) => { + client.on('messageCreate', async (message) => { // this conditional prevents handling of messages like ChannelNameChanged which // are emitted inside a thread but which we do not want to replicate in the CW thread. // Thread/channel name changes are handled in threadUpdate since the that event comes diff --git a/packages/discord-bot/src/discord-listener/handlers.ts b/packages/discord-bot/src/discord-listener/handlers.ts index bc16d256bc9..fe00f078389 100644 --- a/packages/discord-bot/src/discord-listener/handlers.ts +++ b/packages/discord-bot/src/discord-listener/handlers.ts @@ -5,7 +5,13 @@ import { logger, } from '@hicommonwealth/core'; import { DiscordAction } from '@hicommonwealth/model'; -import { Client, Message, ThreadChannel } from 'discord.js'; +import { + Client, + Message, + OmitPartialGroupDMChannel, + PartialMessage, + ThreadChannel, +} from 'discord.js'; import { getImageUrls } from '../discord-listener/util'; import { getForumLinkedTopic } from '../util'; @@ -14,7 +20,10 @@ const log = logger(import.meta); export async function handleMessage( controller: Broker, client: Client, - message: Partial, + message: + | OmitPartialGroupDMChannel | PartialMessage> + | Message + | PartialMessage, action: DiscordAction, ) { log.info( @@ -28,7 +37,7 @@ export async function handleMessage( if (channel?.type !== 11 && channel?.type !== 15) return; // must be thread channel(all forum posts are Threads) const parent_id = - channel?.type === 11 ? channel.parentId : channel.id ?? '0'; + channel?.type === 11 ? channel.parentId : (channel.id ?? '0'); // Only process messages from relevant channels const topicId = await getForumLinkedTopic(parent_id); diff --git a/packages/discord-bot/src/discord-listener/util.ts b/packages/discord-bot/src/discord-listener/util.ts index 1743fedc84c..9c97cd6e7b9 100644 --- a/packages/discord-bot/src/discord-listener/util.ts +++ b/packages/discord-bot/src/discord-listener/util.ts @@ -1,6 +1,11 @@ -import { Message } from 'discord.js'; +import { Message, OmitPartialGroupDMChannel, PartialMessage } from 'discord.js'; -export function getImageUrls(message: Partial) { +export function getImageUrls( + message: + | OmitPartialGroupDMChannel | PartialMessage> + | Message + | PartialMessage, +) { if (!message.attachments) return []; const attachments = [...message.attachments.values()]; From a1da989be3a1a8dfa3c1a8a046854f790bfcfa39 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Sep 2024 15:29:01 -0400 Subject: [PATCH 20/30] fix auth issue --- libs/model/src/middleware/authorization.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 40cef5a2396..3ac41fd367f 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -255,9 +255,15 @@ export function isAuthorized({ collaborators?: boolean; }): AuthHandler { return async (ctx) => { - if (ctx.actor.user.isAdmin) return; + const isSuperAdmin = ctx.actor.user.isAdmin; + + const auth = await authorizeAddress( + ctx, + isSuperAdmin ? ['admin', 'moderator', 'member'] : roles, + ); + + if (isSuperAdmin) return; - const auth = await authorizeAddress(ctx, roles); if (auth.address!.is_banned) throw new BannedActor(ctx.actor); auth.is_author = auth.address!.id === auth.author_address_id; From 1e5a5c26ed1d13897bdadf634d1c570ee0d56be3 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Sep 2024 15:41:31 -0400 Subject: [PATCH 21/30] small renaming for clarity and to avoid lint error --- libs/model/src/middleware/authorization.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 3ac41fd367f..c30fc7f7b08 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -70,14 +70,14 @@ export class RejectedMember extends InvalidActor { } /** - * Prepares authorization context + * Builds authorization context * * @param actor command actor * @param payload command payload * @param auth authorization context * @param roles roles filter */ -async function authorizeAddress( +async function buildAuth( ctx: Context, roles: Role[], ): Promise { @@ -255,14 +255,14 @@ export function isAuthorized({ collaborators?: boolean; }): AuthHandler { return async (ctx) => { - const isSuperAdmin = ctx.actor.user.isAdmin; + const isAdmin = ctx.actor.user.isAdmin; - const auth = await authorizeAddress( + const auth = await buildAuth( ctx, - isSuperAdmin ? ['admin', 'moderator', 'member'] : roles, + isAdmin ? ['admin', 'moderator', 'member'] : roles, ); - if (isSuperAdmin) return; + if (isAdmin) return; if (auth.address!.is_banned) throw new BannedActor(ctx.actor); From 7ea75c538ce11708a1ea4f0446a4d1d50499af15 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Thu, 12 Sep 2024 15:59:31 -0400 Subject: [PATCH 22/30] will fail via auth --- libs/model/test/community/community-lifecycle.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/model/test/community/community-lifecycle.spec.ts b/libs/model/test/community/community-lifecycle.spec.ts index f86c105ff86..6b35f7abfa5 100644 --- a/libs/model/test/community/community-lifecycle.spec.ts +++ b/libs/model/test/community/community-lifecycle.spec.ts @@ -252,7 +252,7 @@ describe('Community lifecycle', () => { chain_node_id: 1263, }, }), - ).rejects.toThrow(UpdateCommunityErrors.ReservedId); + ).rejects.toThrow(); }); test('should throw if snapshot not found', async () => { From 34fbbfe222da4695c20016b974f7ffc28dac0684 Mon Sep 17 00:00:00 2001 From: Marcin Date: Fri, 13 Sep 2024 12:37:56 +0200 Subject: [PATCH 23/30] Fixed crash when topic is updated --- .../TopicDetails/ManageTopicsSection/ManageTopicsSection.tsx | 2 +- .../TopicsOld/ManageTopicsSectionOld/ManageTopicsSectionOld.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Topics/TopicDetails/ManageTopicsSection/ManageTopicsSection.tsx b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Topics/TopicDetails/ManageTopicsSection/ManageTopicsSection.tsx index 0ffe50fc468..d4c889867d6 100644 --- a/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Topics/TopicDetails/ManageTopicsSection/ManageTopicsSection.tsx +++ b/packages/commonwealth/client/scripts/views/pages/CommunityManagement/Topics/TopicDetails/ManageTopicsSection/ManageTopicsSection.tsx @@ -162,7 +162,7 @@ export const ManageTopicsSection = () => { buttonHeight="med" onClick={handleReversion} disabled={initialFeaturedTopics.every( - (value, index) => value.id === featuredTopics[index].id, + (value, index) => value.id === featuredTopics?.[index]?.id, )} /> { buttonHeight="med" onClick={handleReversion} disabled={initialFeaturedTopics.every( - (value, index) => value.id === featuredTopics[index].id, + (value, index) => value.id === featuredTopics?.[index]?.id, )} /> Date: Fri, 13 Sep 2024 15:25:15 +0300 Subject: [PATCH 24/30] fix lockfile --- pnpm-lock.yaml | 317 +++++++++++++++++++------------------------------ 1 file changed, 120 insertions(+), 197 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a615fd7da8..762173857f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,7 +172,7 @@ importers: version: 3.7.0(@swc/helpers@0.5.5)(vite@5.2.12(@types/node@20.12.10)(sass@1.77.0)(terser@5.31.0)) '@vitest/coverage-istanbul': specifier: ^1.6.0 - version: 1.6.0(vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(sass@1.77.0)(terser@5.31.0)) + version: 1.6.0(vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0)(sass@1.77.0)(terser@5.31.0)) chai: specifier: ^4.3.6 version: 4.4.1 @@ -310,7 +310,7 @@ importers: version: 4.3.2(typescript@5.4.5)(vite@5.2.12(@types/node@20.12.10)(sass@1.77.0)(terser@5.31.0)) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.12.10)(jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(sass@1.77.0)(terser@5.31.0) + version: 1.6.0(@types/node@20.12.10)(jsdom@24.0.0)(sass@1.77.0)(terser@5.31.0) wait-on: specifier: ^7.2.0 version: 7.2.0 @@ -504,7 +504,7 @@ importers: version: 6.11.4 web3: specifier: ^4.7.0 - version: 4.8.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) + version: 4.8.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6) web3-core: specifier: ^4.3.2 version: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -550,7 +550,7 @@ importers: version: 16.4.5 ethers: specifier: 5.7.2 - version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) + version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -724,6 +724,9 @@ importers: pg: specifier: ^8.11.3 version: 8.11.5 + quill-delta-to-markdown: + specifier: ^0.6.0 + version: 0.6.0 sequelize: specifier: ^6.32.1 version: 6.37.3(pg@8.11.5) @@ -1032,10 +1035,10 @@ importers: version: 1.91.8(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@tanstack/react-query': specifier: ^4.29.7 - version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) '@tanstack/react-query-devtools': specifier: ^4.29.7 - version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-table': specifier: ^8.9.7 version: 8.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1050,7 +1053,7 @@ importers: version: 10.45.2(@trpc/server@10.45.2) '@trpc/react-query': specifier: ^10.45.1 - version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react-helmet-async': specifier: ^1.0.3 version: 1.0.3(react@18.3.1) @@ -1281,7 +1284,7 @@ importers: version: 18.3.1 react-beautiful-dnd: specifier: ^13.1.1 - version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) react-device-detect: specifier: ^2.2.3 version: 2.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -20101,6 +20104,13 @@ packages: } engines: { node: '>=8' } + quill-delta-to-markdown@0.6.0: + resolution: + { + integrity: sha512-GwNMwSvXsH8G2o6EhvoTnIwEcN8RMbtklSjuyXYdPAXAhseMiQfehngcTwVZCz/3Fn4iu1CxlpLIKoBA9AjgdQ==, + } + engines: { node: '>=6.4.0' } + quill-delta@3.6.3: resolution: { @@ -24825,7 +24835,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-bucket-endpoint': 3.577.0 '@aws-sdk/middleware-expect-continue': 3.577.0 '@aws-sdk/middleware-flexible-checksums': 3.577.0 @@ -24886,7 +24896,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -24975,7 +24985,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/core': 3.576.0 - '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 '@aws-sdk/middleware-logger': 3.577.0 '@aws-sdk/middleware-recursion-detection': 3.577.0 @@ -25043,12 +25053,12 @@ snapshots: '@smithy/util-stream': 3.0.1 tslib: 2.6.2 - '@aws-sdk/credential-provider-ini@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0)': + '@aws-sdk/credential-provider-ini@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0)': dependencies: '@aws-sdk/client-sts': 3.577.0 '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 @@ -25060,13 +25070,13 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0)': + '@aws-sdk/credential-provider-node@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0)': dependencies: '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-http': 3.577.0 - '@aws-sdk/credential-provider-ini': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/credential-provider-ini': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/credential-provider-process': 3.577.0 - '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/credential-provider-web-identity': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/credential-provider-imds': 3.0.0 @@ -25087,10 +25097,10 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/credential-provider-sso@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))': + '@aws-sdk/credential-provider-sso@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: '@aws-sdk/client-sso': 3.577.0 - '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)) + '@aws-sdk/token-providers': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 @@ -25238,7 +25248,7 @@ snapshots: '@smithy/types': 3.0.0 tslib: 2.6.2 - '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0))': + '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) '@aws-sdk/types': 3.577.0 @@ -27325,32 +27335,6 @@ snapshots: - bufferutil - utf-8-validate - '@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)': - dependencies: - '@ethersproject/abstract-provider': 5.7.0 - '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 - '@ethersproject/base64': 5.7.0 - '@ethersproject/basex': 5.7.0 - '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 - '@ethersproject/constants': 5.7.0 - '@ethersproject/hash': 5.7.0 - '@ethersproject/logger': 5.7.0 - '@ethersproject/networks': 5.7.1 - '@ethersproject/properties': 5.7.0 - '@ethersproject/random': 5.7.0 - '@ethersproject/rlp': 5.7.0 - '@ethersproject/sha2': 5.7.0 - '@ethersproject/strings': 5.7.0 - '@ethersproject/transactions': 5.7.0 - '@ethersproject/web': 5.7.1 - bech32: 1.1.4 - ws: 7.4.6(bufferutil@4.0.8)(utf-8-validate@6.0.3) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@ethersproject/random@5.7.0': dependencies: '@ethersproject/bytes': 5.7.0 @@ -30417,6 +30401,16 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@react-native/virtualized-lists@0.74.81(@types/react@18.3.1)(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 18.3.1 + react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + optionalDependencies: + '@types/react': 18.3.1 + optional: true + '@redis/bloom@1.0.2(@redis/client@1.2.0)': dependencies: '@redis/client': 1.2.0 @@ -31429,10 +31423,10 @@ snapshots: - '@types/react' - react-dom - '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/match-sorter-utils': 8.15.1 - '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) superjson: 1.13.3 @@ -31447,6 +31441,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-native: 0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + dependencies: + '@tanstack/query-core': 4.36.1 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + '@tanstack/react-store@0.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/store': 0.3.1 @@ -31553,9 +31556,9 @@ snapshots: dependencies: '@trpc/server': 10.45.2 - '@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@tanstack/react-query': 4.36.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/server': 10.45.2 react: 18.3.1 @@ -32234,7 +32237,7 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-istanbul@1.6.0(vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(sass@1.77.0)(terser@5.31.0))': + '@vitest/coverage-istanbul@1.6.0(vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0)(sass@1.77.0)(terser@5.31.0))': dependencies: debug: 4.3.4(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 @@ -32245,7 +32248,7 @@ snapshots: magicast: 0.3.4 picocolors: 1.0.0 test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.12.10)(jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(sass@1.77.0)(terser@5.31.0) + vitest: 1.6.0(@types/node@20.12.10)(jsdom@24.0.0)(sass@1.77.0)(terser@5.31.0) transitivePeerDependencies: - supports-color @@ -35672,42 +35675,6 @@ snapshots: - bufferutil - utf-8-validate - ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.3): - dependencies: - '@ethersproject/abi': 5.7.0 - '@ethersproject/abstract-provider': 5.7.0 - '@ethersproject/abstract-signer': 5.7.0 - '@ethersproject/address': 5.7.0 - '@ethersproject/base64': 5.7.0 - '@ethersproject/basex': 5.7.0 - '@ethersproject/bignumber': 5.7.0 - '@ethersproject/bytes': 5.7.0 - '@ethersproject/constants': 5.7.0 - '@ethersproject/contracts': 5.7.0 - '@ethersproject/hash': 5.7.0 - '@ethersproject/hdnode': 5.7.0 - '@ethersproject/json-wallets': 5.7.0 - '@ethersproject/keccak256': 5.7.0 - '@ethersproject/logger': 5.7.0 - '@ethersproject/networks': 5.7.1 - '@ethersproject/pbkdf2': 5.7.0 - '@ethersproject/properties': 5.7.0 - '@ethersproject/providers': 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@ethersproject/random': 5.7.0 - '@ethersproject/rlp': 5.7.0 - '@ethersproject/sha2': 5.7.0 - '@ethersproject/signing-key': 5.7.0 - '@ethersproject/solidity': 5.7.0 - '@ethersproject/strings': 5.7.0 - '@ethersproject/transactions': 5.7.0 - '@ethersproject/units': 5.7.0 - '@ethersproject/wallet': 5.7.0 - '@ethersproject/web': 5.7.1 - '@ethersproject/wordlists': 5.7.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - ethers@6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -37405,7 +37372,7 @@ snapshots: dependencies: jsdom: 24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + jsdom@24.0.0: dependencies: cssstyle: 4.0.1 data-urls: 5.0.0 @@ -37426,14 +37393,15 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + optional: true - jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3): + jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: cssstyle: 4.0.1 data-urls: 5.0.0 @@ -37454,13 +37422,12 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - optional: true jsesc@0.5.0: {} @@ -39846,6 +39813,10 @@ snapshots: quick-lru@4.0.1: {} + quill-delta-to-markdown@0.6.0: + dependencies: + lodash: 4.17.21 + quill-delta@3.6.3: dependencies: deep-equal: 1.1.2 @@ -39939,7 +39910,7 @@ snapshots: lodash.flow: 3.5.0 pure-color: 1.3.0 - react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: '@babel/runtime': 7.24.5 css-box-model: 1.2.1 @@ -39947,7 +39918,7 @@ snapshots: raf-schd: 4.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) redux: 4.2.1 use-memo-one: 1.1.3(react@18.3.1) transitivePeerDependencies: @@ -40120,6 +40091,57 @@ snapshots: - supports-color - utf-8-validate + react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native-community/cli': 13.6.4(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native-community/cli-platform-android': 13.6.4 + '@react-native-community/cli-platform-ios': 13.6.4 + '@react-native/assets-registry': 0.74.81 + '@react-native/codegen': 0.74.81(@babel/preset-env@7.25.4(@babel/core@7.24.5)) + '@react-native/community-cli-plugin': 0.74.81(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native/gradle-plugin': 0.74.81 + '@react-native/js-polyfills': 0.74.81 + '@react-native/normalize-colors': 0.74.81 + '@react-native/virtualized-lists': 0.74.81(@types/react@18.3.1)(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + base64-js: 1.5.1 + chalk: 4.1.2 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + jsc-android: 250231.0.0 + memoize-one: 5.2.1 + metro-runtime: 0.80.10 + metro-source-map: 0.80.10 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + pretty-format: 26.6.2 + promise: 8.3.0 + react: 18.3.1 + react-devtools-core: 5.3.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + react-shallow-renderer: 16.15.0(react@18.3.1) + regenerator-runtime: 0.13.11 + scheduler: 0.24.0-canary-efb381bbf-20230505 + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + ws: 6.2.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.1 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + optional: true + react-popper-tooltip@4.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.5 @@ -40144,7 +40166,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react-native@0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: '@babel/runtime': 7.24.5 '@types/react-redux': 7.1.33 @@ -40155,7 +40177,7 @@ snapshots: react-is: 17.0.2 optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-native: 0.74.0(@babel/core@7.24.5)(@babel/preset-env@7.25.4(@babel/core@7.24.5))(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.0(@babel/core@7.24.5)(@types/react@18.3.1)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) react-refresh@0.14.2: {} @@ -42282,7 +42304,7 @@ snapshots: sass: 1.77.0 terser: 5.31.0 - vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(sass@1.77.0)(terser@5.31.0): + vitest@1.6.0(@types/node@20.12.10)(jsdom@24.0.0)(sass@1.77.0)(terser@5.31.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -42306,7 +42328,7 @@ snapshots: why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.12.10 - jsdom: 24.0.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + jsdom: 24.0.0 transitivePeerDependencies: - less - lightningcss @@ -42497,22 +42519,6 @@ snapshots: web3-utils: 4.2.3 web3-validator: 2.0.5 - web3-eth-contract@4.4.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10): - dependencies: - web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-errors: 1.1.4 - web3-eth: 4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-eth-abi: 4.2.1(typescript@5.4.5)(zod@3.23.6) - web3-types: 1.6.0 - web3-utils: 4.2.3 - web3-validator: 2.0.5 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - web3-eth-contract@4.4.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6): dependencies: web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -42545,24 +42551,6 @@ snapshots: - utf-8-validate - zod - web3-eth-ens@4.2.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10): - dependencies: - '@adraffy/ens-normalize': 1.10.1 - web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-errors: 1.1.4 - web3-eth: 4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-eth-contract: 4.4.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-net: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-types: 1.6.0 - web3-utils: 4.2.3 - web3-validator: 2.0.5 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - web3-eth-ens@4.2.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -42611,21 +42599,6 @@ snapshots: web3-utils: 4.2.3 web3-validator: 2.0.5 - web3-eth-personal@4.0.8(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10): - dependencies: - web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-eth: 4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-rpc-methods: 1.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-types: 1.6.0 - web3-utils: 4.2.3 - web3-validator: 2.0.5 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - web3-eth-personal@4.0.8(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6): dependencies: web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -42656,26 +42629,6 @@ snapshots: - utf-8-validate - zod - web3-eth@4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10): - dependencies: - setimmediate: 1.0.5 - web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-errors: 1.1.4 - web3-eth-abi: 4.2.1(typescript@5.4.5)(zod@3.23.6) - web3-eth-accounts: 4.1.2 - web3-net: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-providers-ws: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-rpc-methods: 1.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-types: 1.6.0 - web3-utils: 4.2.3 - web3-validator: 2.0.5 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - web3-eth@4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6): dependencies: setimmediate: 1.0.5 @@ -42844,31 +42797,6 @@ snapshots: web3-types: 1.6.0 zod: 3.23.6 - web3@4.8.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10): - dependencies: - web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-errors: 1.1.4 - web3-eth: 4.6.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-eth-abi: 4.2.1(typescript@5.4.5)(zod@3.23.6) - web3-eth-accounts: 4.1.2 - web3-eth-contract: 4.4.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-eth-ens: 4.2.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-eth-iban: 4.0.7 - web3-eth-personal: 4.0.8(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10) - web3-net: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-providers-http: 4.1.0 - web3-providers-ws: 4.0.7(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-rpc-methods: 1.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - web3-types: 1.6.0 - web3-utils: 4.2.3 - web3-validator: 2.0.5 - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - web3@4.8.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.6): dependencies: web3-core: 4.3.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -43092,11 +43020,6 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 5.0.10 - ws@7.4.6(bufferutil@4.0.8)(utf-8-validate@6.0.3): - optionalDependencies: - bufferutil: 4.0.8 - utf-8-validate: 6.0.3 - ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.8 From c396ff659201629b636e92e6fc6ecd416bd96894 Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Fri, 13 Sep 2024 09:22:57 -0400 Subject: [PATCH 25/30] resolve pr questions --- libs/model/src/middleware/authorization.ts | 15 ++++++--- libs/model/src/thread/UpdateThread.command.ts | 32 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index c30fc7f7b08..730261ede68 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -80,6 +80,7 @@ export class RejectedMember extends InvalidActor { async function buildAuth( ctx: Context, roles: Role[], + collaborators = false, ): Promise { const { actor, payload } = ctx; if (!actor.address) @@ -127,13 +128,16 @@ async function buildAuth( auth.topic_id = auth.comment.Thread!.topic_id; auth.author_address_id = auth.comment.address_id; } else { + const include = collaborators + ? { + model: models.Address, + as: 'collaborators', + required: false, + } + : undefined; auth.thread = await models.Thread.findOne({ where: { id: auth.thread_id }, - include: { - model: models.Address, - as: 'collaborators', - required: false, - }, + include, }); if (!auth.thread) throw new InvalidInput('Must provide a valid thread id'); @@ -260,6 +264,7 @@ export function isAuthorized({ const auth = await buildAuth( ctx, isAdmin ? ['admin', 'moderator', 'member'] : roles, + collaborators, ); if (isAdmin) return; diff --git a/libs/model/src/thread/UpdateThread.command.ts b/libs/model/src/thread/UpdateThread.command.ts index 7f4060460c7..d36305e5446 100644 --- a/libs/model/src/thread/UpdateThread.command.ts +++ b/libs/model/src/thread/UpdateThread.command.ts @@ -5,7 +5,7 @@ import { type Command, } from '@hicommonwealth/core'; import * as schemas from '@hicommonwealth/schemas'; -import { Op, QueryTypes, Sequelize } from 'sequelize'; +import { Op, Sequelize } from 'sequelize'; import { z } from 'zod'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; @@ -204,22 +204,22 @@ export function UpdateThread(): Command< collaboratorsPatch.add.length > 0 || collaboratorsPatch.remove.length > 0 ) { - const contestManagers = await models.sequelize.query( - ` -SELECT cm.contest_address FROM "Threads" t -JOIN "ContestTopics" ct on ct.topic_id = t.topic_id -JOIN "ContestManagers" cm on cm.contest_address = ct.contest_address -WHERE t.id = :thread_id -`, - { - type: QueryTypes.SELECT, - replacements: { - thread_id: thread!.id, + const found = await models.Thread.findOne({ + where: { id: thread_id }, + include: [ + { + model: models.ContestTopic, + required: true, + include: [ + { + model: models.ContestManager, + required: true, + }, + ], }, - }, - ); - if (contestManagers.length > 0) - throw new InvalidInput(UpdateThreadErrors.ContestLock); + ], + }); + if (found) throw new InvalidInput(UpdateThreadErrors.ContestLock); } // == mutation transaction boundary == From 41f6c562f0eec550083dc1dfa7523b2c97855ecb Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Fri, 13 Sep 2024 09:50:27 -0400 Subject: [PATCH 26/30] fix contest lock query --- libs/model/src/middleware/authorization.ts | 1 + libs/model/src/middleware/guards.ts | 7 +++++++ libs/model/src/thread/UpdateThread.command.ts | 16 +++++----------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 730261ede68..8a5ae80ac7a 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -126,6 +126,7 @@ async function buildAuth( throw new InvalidInput('Must provide a valid comment id'); auth.community_id = auth.comment.Thread!.community_id; auth.topic_id = auth.comment.Thread!.topic_id; + auth.thread_id = auth.comment.Thread!.id; auth.author_address_id = auth.comment.address_id; } else { const include = collaborators diff --git a/libs/model/src/middleware/guards.ts b/libs/model/src/middleware/guards.ts index 8886316b784..407dacabb8a 100644 --- a/libs/model/src/middleware/guards.ts +++ b/libs/model/src/middleware/guards.ts @@ -80,6 +80,9 @@ export function mustBeAuthorizedThread(actor: Actor, auth?: AuthContext) { return auth as AuthContext & { address: AddressInstance; thread: ThreadInstance; + community_id: string; + topic_id: number; + thread_id: number; }; } @@ -94,5 +97,9 @@ export function mustBeAuthorizedComment(actor: Actor, auth?: AuthContext) { return auth as AuthContext & { address: AddressInstance; comment: ThreadInstance; + community_id: string; + topic_id: number; + thread_id: number; + comment_id: number; }; } diff --git a/libs/model/src/thread/UpdateThread.command.ts b/libs/model/src/thread/UpdateThread.command.ts index d36305e5446..80f4015d886 100644 --- a/libs/model/src/thread/UpdateThread.command.ts +++ b/libs/model/src/thread/UpdateThread.command.ts @@ -9,7 +9,7 @@ import { Op, Sequelize } from 'sequelize'; import { z } from 'zod'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; -import { mustBeAuthorized, mustExist } from '../middleware/guards'; +import { mustBeAuthorizedThread, mustExist } from '../middleware/guards'; import type { ThreadAttributes, ThreadInstance } from '../models/thread'; import { emitMentions, @@ -175,7 +175,7 @@ export function UpdateThread(): Command< ...schemas.UpdateThread, auth: [isAuthorized({ collaborators: true })], body: async ({ actor, payload, auth }) => { - const { address } = mustBeAuthorized(actor, auth); + const { address, topic_id } = mustBeAuthorizedThread(actor, auth); const { thread_id, discord_meta } = payload; // find by discord_meta first if present @@ -204,18 +204,12 @@ export function UpdateThread(): Command< collaboratorsPatch.add.length > 0 || collaboratorsPatch.remove.length > 0 ) { - const found = await models.Thread.findOne({ - where: { id: thread_id }, + const found = await models.ContestTopic.findOne({ + where: { topic_id }, include: [ { - model: models.ContestTopic, + model: models.ContestManager, required: true, - include: [ - { - model: models.ContestManager, - required: true, - }, - ], }, ], }); From 0a302143dbc22b9317e3e0b0525e5f3de8be10ec Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Fri, 13 Sep 2024 10:25:56 -0400 Subject: [PATCH 27/30] refined auth waterfall --- libs/model/src/middleware/authorization.ts | 44 +++++++++++----------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 8a5ae80ac7a..61d319c7dbb 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -237,13 +237,13 @@ export const isSuperAdmin: AuthHandler = async (ctx) => { }; /** - * Validates if actor address is authorized by checking for: - * - **super admin**: Allow all operations when the user is a super admin (god mode) - * - **in roles**: Allow when user is in the provides community roles - * - **not banned**: Reject if user is banned - * - **author**: Allow when the user is the creator of the entity - * - **topic group**: Allow when user has group permissions in topic - * - **collaborators**: Allow collaborators + * Validates if actor's address is authorized by checking in the following order: + * - 1. **in roles**: User address must be in the provided community roles + * - 2. **admin**: Allows all operations when the user is an admin or super admin (god mode, site admin) + * - 3. **not banned**: Reject if address is banned + * - 4. **topic group**: Allows when address has group permissions in topic + * - 5. **author**: Allows when address is the creator of the entity + * - 6. **collaborators**: Allows when address is a collaborator * * @param roles specific community roles - all by default * @param action specific group permission action @@ -268,28 +268,26 @@ export function isAuthorized({ collaborators, ); - if (isAdmin) return; + if (isAdmin || auth.address?.role === 'admin') return; if (auth.address!.is_banned) throw new BannedActor(ctx.actor); + if (action) { + await isTopicMember(ctx.actor, auth, action); + return; + } + auth.is_author = auth.address!.id === auth.author_address_id; if (auth.is_author) return; - if (auth.address!.role === 'member') { - if (action) { - await isTopicMember(ctx.actor, auth, action); - return; - } - - if (collaborators) { - const found = auth.thread?.collaborators?.find( - ({ address }) => address === ctx.actor.address, - ); - auth.is_collaborator = !!found; - if (auth.is_collaborator) return; - } - - throw new InvalidActor(ctx.actor, 'Not authorized member'); + if (collaborators) { + const found = auth.thread?.collaborators?.find( + ({ address }) => address === ctx.actor.address, + ); + auth.is_collaborator = !!found; + if (auth.is_collaborator) return; } + + throw new InvalidActor(ctx.actor, 'Not authorized member'); }; } From 1aeb09087592f2cca679805f19a11cafb887836a Mon Sep 17 00:00:00 2001 From: rotorsoft Date: Fri, 13 Sep 2024 11:32:30 -0400 Subject: [PATCH 28/30] fix author case --- libs/model/src/middleware/authorization.ts | 12 ++-- .../test/thread/thread-lifecycle.spec.ts | 60 ++++++++++--------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/libs/model/src/middleware/authorization.ts b/libs/model/src/middleware/authorization.ts index 61d319c7dbb..a9d3a707f35 100644 --- a/libs/model/src/middleware/authorization.ts +++ b/libs/model/src/middleware/authorization.ts @@ -28,7 +28,7 @@ export type AuthContext = { thread?: ThreadInstance | null; comment?: CommentInstance | null; author_address_id?: number; - is_author?: boolean; + is_author: boolean; is_collaborator?: boolean; }; @@ -101,7 +101,7 @@ async function buildAuth( * 2. Find by thread_id when payload contains thread_id * 3. Find by comment_id when payload contains comment_id */ - const auth: AuthContext = { address: null }; + const auth: AuthContext = { address: null, is_author: false }; (ctx as { auth: AuthContext }).auth = auth; auth.community_id = @@ -158,6 +158,8 @@ async function buildAuth( }); if (!auth.address) throw new InvalidActor(actor, `User is not ${roles} in the community`); + + auth.is_author = auth.address!.id === auth.author_address_id; return auth; } @@ -273,11 +275,11 @@ export function isAuthorized({ if (auth.address!.is_banned) throw new BannedActor(ctx.actor); if (action) { + // waterfall stops here after validating the action await isTopicMember(ctx.actor, auth, action); return; } - auth.is_author = auth.address!.id === auth.author_address_id; if (auth.is_author) return; if (collaborators) { @@ -286,8 +288,10 @@ export function isAuthorized({ ); auth.is_collaborator = !!found; if (auth.is_collaborator) return; + throw new InvalidActor(ctx.actor, 'Not authorized collaborator'); } - throw new InvalidActor(ctx.actor, 'Not authorized member'); + // at this point, the address is either a moderator or member + // without any action or collaboration requirements }; } diff --git a/libs/model/test/thread/thread-lifecycle.spec.ts b/libs/model/test/thread/thread-lifecycle.spec.ts index 3005a4997de..231328ef26c 100644 --- a/libs/model/test/thread/thread-lifecycle.spec.ts +++ b/libs/model/test/thread/thread-lifecycle.spec.ts @@ -183,37 +183,39 @@ describe('Thread lifecycle', () => { await dispose()(); }); - const authorizationTests = { - admin: undefined, - member: undefined, - nonmember: NonMember, - banned: BannedActor, - rejected: RejectedMember, - } as Record<(typeof roles)[number], any>; - - roles.forEach((role) => { - if (!authorizationTests[role]) { - it(`should create thread as ${role}`, async () => { - const _thread = await command(CreateThread(), { - actor: actors[role], - payload, - }); - expect(_thread?.title).to.equal(title); - expect(_thread?.body).to.equal(body); - expect(_thread?.stage).to.equal(stage); - // capture as admin author for other tests - if (!thread) thread = _thread!; - }); - } else { - it(`should reject create thread as ${role}`, async () => { - await expect( - command(CreateThread(), { + describe('create', () => { + const authorizationTests = { + admin: undefined, + member: undefined, + nonmember: NonMember, + banned: BannedActor, + rejected: RejectedMember, + } as Record<(typeof roles)[number], any>; + + roles.forEach((role) => { + if (!authorizationTests[role]) { + it(`should create thread as ${role}`, async () => { + const _thread = await command(CreateThread(), { actor: actors[role], payload, - }), - ).rejects.toThrowError(authorizationTests[role]); - }); - } + }); + expect(_thread?.title).to.equal(title); + expect(_thread?.body).to.equal(body); + expect(_thread?.stage).to.equal(stage); + // capture as admin author for other tests + if (!thread) thread = _thread!; + }); + } else { + it(`should reject create thread as ${role}`, async () => { + await expect( + command(CreateThread(), { + actor: actors[role], + payload, + }), + ).rejects.toThrowError(authorizationTests[role]); + }); + } + }); }); describe('updates', () => { From 301f106f6d6bc695ad47eef197de7cdfcdae2b75 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Mon, 16 Sep 2024 12:34:08 +0300 Subject: [PATCH 29/30] remove comment --- .../client/scripts/state/api/comments/editComment.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/commonwealth/client/scripts/state/api/comments/editComment.ts b/packages/commonwealth/client/scripts/state/api/comments/editComment.ts index f8c8c8613a8..f126911fa38 100644 --- a/packages/commonwealth/client/scripts/state/api/comments/editComment.ts +++ b/packages/commonwealth/client/scripts/state/api/comments/editComment.ts @@ -65,8 +65,6 @@ const useEditCommentMutation = ({ onSuccess: async (updatedComment) => { // @ts-expect-error StrictNullChecks const comment = new Comment(updatedComment); - console.log({ comment, updatedComment }); - // update fetch comments query state with updated comment const key = [ApiEndpoints.FETCH_COMMENTS, communityId, threadId]; queryClient.cancelQueries({ queryKey: key }); From a5653e718377072fc6c46c492b62be59297203f2 Mon Sep 17 00:00:00 2001 From: Timothee Legros Date: Mon, 16 Sep 2024 12:34:46 +0300 Subject: [PATCH 30/30] lockfile resolution --- pnpm-lock.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 256b499ea9d..1dccd7e961d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -724,6 +724,9 @@ importers: pg: specifier: ^8.11.3 version: 8.11.5 + quill-delta-to-markdown: + specifier: ^0.6.0 + version: 0.6.0 sequelize: specifier: ^6.32.1 version: 6.37.3(pg@8.11.5) @@ -20138,6 +20141,13 @@ packages: } engines: { node: '>=8' } + quill-delta-to-markdown@0.6.0: + resolution: + { + integrity: sha512-GwNMwSvXsH8G2o6EhvoTnIwEcN8RMbtklSjuyXYdPAXAhseMiQfehngcTwVZCz/3Fn4iu1CxlpLIKoBA9AjgdQ==, + } + engines: { node: '>=6.4.0' } + quill-delta@3.6.3: resolution: { @@ -39881,6 +39891,10 @@ snapshots: quick-lru@4.0.1: {} + quill-delta-to-markdown@0.6.0: + dependencies: + lodash: 4.17.21 + quill-delta@3.6.3: dependencies: deep-equal: 1.1.2